Easy-to-manage touch screens
무료 오픈소스 터치스크린으로 USB 설정 가이드, 드래그‑드롭 편집기, 로컬 제어를 제공하며 Home Assistant용으로 제작되었습니다
인터랙티브 지도에서 런던 파란 명판을 찾아 작가·예술가·과학자·문화 인물을 확인하고, 근처 명판을 찾으며 탐색 기록을 남깁니다
Explore London's blue plaques on an interactive map. Discover famous writers, artists, scientists and cultural figures, find plaques near you and track what you've uncovered.
라이프스타일HTML · JavaScriptGitHub @nyatasha온라인운영자 미확인
써 보셨어요? 한 번만 눌러 주세요. 로그인은 필요 없어요.
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기Interactive map of all English Heritage blue plaques across London, with iconic people highlighted by Wikipedia popularity.
저장소 README에서 가져옴
README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
A mobile-first London exploration game built on an interactive map of the
English Heritage blue plaque scheme. Explore the map, find plaques near you,
collect the discoveries you make, and watch your London fill in.
Discover the stories hiding around you.README 전문 보기 ↗A mobile-first London exploration game built on an interactive map of the English Heritage blue plaque scheme. Explore the map, find plaques near you, collect the discoveries you make, and watch your London fill in.
Live site: https://nyatasha.github.io/blueplaques/
Discover the stories hiding around you.
Progress lives in this browser's localStorage. There are no accounts, no
backend and no tracking of your location.
No build step and no dependencies — it is static files plus Leaflet from a CDN.
python3 -m http.server 8000
# open http://localhost:8000
A server is required: the app uses native ES modules and fetch, neither of
which works from a file:// URL.
bash tests/run.sh
Three suites, all dependency-free:
| Suite | Runner | Covers |
|---|---|---|
tests/logic.test.mjs | JavaScriptCore (ships with macOS) | XP and levels, Haversine distances, localStorage robustness, derived progress, achievements, quests, recommendation ranking, dataset integrity |
tests/seo.test.py | Python 3 | slug stability and collisions, HTML and URL escaping, generated page content, canonical URLs, relative paths, the internal link graph (category, nearby, walks), category and walk page contents, walk definitions and derived distances, the plaque URL map, sitemap contents |
tests/smoke.html | headless Chrome via tests/cdp.py | boot, marker rendering, layout and touch targets at 390 px, search, the collect flow, tab navigation, filters, categories, reset, ?plaque= deep links |
tests/cdp.py is a small self-contained Chrome DevTools Protocol client; it
also takes screenshots:
python3 tests/cdp.py shot http://localhost:8000/ shot.png 390 844
Three scripts run in order. Each one reads and rewrites
data/plaques.geojson, so run them in sequence:
python3 scripts/fetch_plaques.py # OpenPlaques CSV → GeoJSON
python3 scripts/add_fame.py # + Wikipedia pageviews → fame 0–3
python3 scripts/classify_categories.py # + category + category_label
A fourth script turns the finished dataset into the crawlable half of the site — a page per plaque, a page per category, a page per curated walk, the URL map the app links with, and the sitemap. Run it whenever the data changes:
python3 scripts/generate_seo_pages.py # plaques/ + categories/ + walks/ + data/plaque-urls.json + sitemap.xml
The share card and app icons are generated too, and committed rather than built per deploy. The card carries the plaque count, which it reads from the dataset, so re-run it after a data refresh as well as after a branding change:
python3 scripts/make_social_image.py # assets/social/og-cover.png
fetch plaques → add fame → classify categories → data/plaques.geojson
↓
generate SEO pages → deploy
generate_seo_pages.py also rewrites the plaque count in index.html — in the
Twitter description, the JSON-LD and the <noscript> block — from the dataset
it just read. Those numbers live inside <!--gen:NAME-->…<!--/gen:NAME-->
regions and must not be edited by hand; a refresh would overwrite them anyway.
Every plaque in the OpenPlaques London
datapull of 14 December 2025 (accessed
2026-09-06) tagged into the scheme administered by English Heritage — 1,001
rows, whatever colour the plaque is. The scheme's early plaques are brown
terracotta, stone, bronze, black, grey, white and green, and selecting on
colour == "blue" used to discard 76 of them (decision-log #24).
This is not every plaque of the scheme that exists: Wikipedia's list of
English Heritage blue
plaques
gives 1,018 (accessed 2026-09-06), so the dump itself trails the scheme by
roughly 17. Nor is it every plaque in London — the same dump holds 3,781 across
some 200 erecting bodies. Rows we do not ship are written to
docs/data-quality/excluded-rows.csv with a reason, one line each, so the
shipped count plus the excluded count always equals the dump. That report is an
internal QA artefact — docs/ is not tracked, and it is not served.
Useful flags while working on the classifier:
python3 scripts/classify_categories.py --dry-run # classify, write nothing
python3 scripts/classify_categories.py --report # per-category samples and
# the unclassified list
Classification is deterministic keyword matching over each plaque's
lead_roles and inscription, with a small manual override table for
high-profile figures the rules place badly. No AI runs at build or run
time and the deployed site calls no API.
To refresh the data:
python3 scripts/fetch_plaques.py
python3 scripts/add_fame.py
python3 scripts/classify_categories.py
python3 scripts/generate_seo_pages.py
python3 scripts/make_social_image.py
bash tests/run.sh
git add data/plaques.geojson plaques categories walks sitemap.xml index.html \
404.html assets/social/og-cover.png \
&& git commit -m "refresh plaque data"
Four self-guided routes, each a hand-picked run of plaques that already exist in the dataset and stand close enough together to walk in an afternoon:
| Walk | Area | Stops | Distance | Walking |
|---|---|---|---|---|
| Bloomsbury Writers & Thinkers | Bloomsbury & Fitzrovia, WC1 | 8 | ~3.0 km | ~38 min |
| Chelsea Riverside | Chelsea, SW3 & SW10 | 8 | ~1.7 km | ~21 min |
| Mayfair: Handel to Hendrix | Mayfair & St James's, W1 & SW1 | 7 | ~2.9 km | ~36 min |
| Hampstead Village | Hampstead, NW3 | 8 | ~2.3 km | ~28 min |
data/walks.json is the whole definition. It holds only what is genuinely
editorial — the slug, name, area, the copy, and the stops in walking order, each
a plaque id and one sentence saying why to stop there:
{
"slug": "chelsea-riverside",
"name": "Chelsea Riverside",
"area": "Chelsea, SW3 & SW10",
"icon": "palette",
"tagline": "Oscar Wilde, Bob Marley and a mile of the Thames.",
"intro": "Cheyne Walk and the streets running off it …",
"startHint": "Start near Sloane Square; finish near Chelsea Harbour …",
"stops": [
{ "id": "287", "note": "Wilde lived at 34 Tite Street from 1884 until 1895 …" }
]
}
Adding a walk is adding one object to that array and re-running
scripts/generate_seo_pages.py.
Nothing measurable is stored there. Stop count, distance and walking time are
derived from the plaques' own coordinates — in js/walks.js for the app and in
scripts/generate_seo_pages.py for the pages — so a walk can never claim a
distance the map does not support, and a data refresh re-derives all of it.
Distance is the summed straight-line legs between consecutive stops multiplied
by ROUTE_FACTOR (1.3), the ordinary circuity allowance for a dense urban street
grid, and duration is that at the 80 m/min already in js/config.js. It is an
estimate, so every surface says "about" and the walk pages state the derivation
in their footer. The factor is declared once in each file and
tests/seo.test.py asserts the two agree.
Progress is the collection. A walk's progress is how many of its stops are already collected — there is no walk-specific state, no walk XP and no schema change, so collecting a plaque anywhere moves every walk it belongs to, and a backup link carries walk progress without knowing walks exist.
In the app, Walks is the second tab. Start walk clears the filters, sets
state.filters.walk — the same mechanism themed collections use — and fits the
camera to the stops; the stops themselves are ordinary markers, so there is no
route layer to keep in step with the map. Each stop row opens the usual plaque
sheet, and that sheet links back to any walk the plaque is on.
Stop notes are hand-written to the same standard as the Iconic notes: each is supported by the plaque's own inscription or is not in dispute. Nothing about a named person is asserted that the record does not carry.
Everything in the app is rendered from data/plaques.geojson in JavaScript, so
until now no individual plaque had a URL a search engine could index.
scripts/generate_seo_pages.py gives each one a plain HTML page:
plaques/freddie-mercury-12345/index.html
→ https://nyatasha.github.io/blueplaques/plaques/freddie-mercury-12345/
<name>-<openplaques id>, so it is readable, lowercase,
URL-safe, stable across builds and collision-free even when two plaques
share a name.http(s) URLs in the
dataset are dropped rather than emitted.../../css/tokens.css, ../../?plaque=<id>), which is
what makes them work under the /blueplaques/ project subdirectory.View on interactive map on each page points at /?plaque=<id>. The app
reads that parameter once during boot and, after the dataset and markers are
ready, runs the ordinary selection path — so the camera move and the detail
sheet behave exactly as a tap on the marker does. An unknown or missing id is
ignored and the app starts normally.
The same script writes one landing page per category that has plaques:
categories/writers-literature/index.html
→ https://nyatasha.github.io/blueplaques/categories/writers-literature/
writers → writers-literature), so renaming a category renames its URL
in one place and the URL carries the words people search for.<h1>, a
short intro made only of counted facts — how many plaques, the earliest and
latest year erected, the areas they cluster in — and a two-column list of
every plaque in the category with its dates, role, address and year.The homepage reaches them through plain anchors in the About panel and in the
<noscript> fallback, both in the initial HTML, so a crawler can walk
home → category → plaque without running any JavaScript.
The app's discovery lists — the desktop panel, "Near me", search suggestions and
recent finds in My London — render as <a href="plaques/<slug>/"> rather than
buttons. An ordinary click is still intercepted and opens the plaque on the map;
a modifier-click or middle-click opens the static page in a new tab, and the DOM
carries a real URL for a crawler.
The hrefs come from data/plaque-urls.json, which the generator writes in the
same run as the pages, so the app cannot link to a slug the generator did not
produce. js/data.js loads it alongside the GeoJSON and validates the shape of
every slug; if it is missing the rows fall back to buttons and nothing else
changes.
Each curated walk also has a page a crawler can read, and there is an index of them:
walks/index.html → https://nyatasha.github.io/blueplaques/walks/
walks/chelsea-riverside/ → .../walks/chelsea-riverside/
<ol> of the
stops in order, each with its dates, address, a sentence of context and a link
to that plaque's page.ItemList whose elements point at the plaque pages, so
the markup and the visible list say the same thing./?walk=<slug>, which the app reads
on boot and opens on the Walks tab with the stops on the map.maps/dir/ URL with
the intermediate stops as waypoints and travelmode=walking.sitemap.xml is generated by the same script and lists the homepage, the walks
index, every walk, every category page, then every plaque page, de-duplicated
and each at its own canonical URL. It carries no lastmod, priority or changefreq: nothing in
the pipeline records when a plaque's facts last changed, and an invented date is
worse than none.
The pages, the URL map and the sitemap are all committed, because GitHub Pages serves the repository as-is.
index.html app shell, SEO metadata, crawlable About copy
manifest.json PWA manifest
sitemap.xml generated — homepage + walks + categories + plaque pages
plaques/<slug>/index.html generated — one static page per plaque
categories/<slug>/index.html generated — one landing page per category
walks/index.html generated — the walk index
walks/<slug>/index.html generated — one page per curated walk
css/
tokens.css design tokens — colour, type, space, motion
plaque-page.css the generated plaque, category and walk pages
app.css reset, shell layout, buttons, boot screen
map.css Leaflet surface, markers, clusters, map controls
components.css nav, sheets, cards, badges, screens, toasts
js/
app.js bootstrap, tabs, collect flow, orchestration
config.js constants, categories, XP, level curve, basemap
data.js GeoJSON loading and indexing
state.js app state and every derived value
storage.js versioned, defensive localStorage
map.js Leaflet map, marker states, clustering, camera
search.js search index and grouped suggestions
filters.js filter sheet
plaque-sheet.js discovery detail sheet
quest-screen.js Quest tab
mylondon.js My London tab
quests.js / achievements.js fixed definitions, evaluated from progress
themes.js themed collections, derived from the dataset
walks.js curated walks: resolution, derived distance, progress
walk-screen.js Walks tab — the index and one walk in full
celebrate.js the Iconic collect moment
share-card.js canvas-rendered progress image
install.js Add to Home Screen offer and detection
backup.js save encoding, backup links, merge on restore
location.js geolocation, requested only on user action
recommend.js "find my next discovery" ranking
geo.js Haversine, formatting, directions links
sheet.js bottom-sheet controller
ui.js / icons.js DOM helpers, toasts, inline SVG icons
analytics.js provider-agnostic trackEvent()
static-page.js the generated pages' runtime — analytics and Share
data/iconic-notes.json hand-written notes for Iconic plaques
data/walks.json curated walk definitions — prose plus ordered plaque ids
scripts/ Python data pipeline
tests/ logic, SEO, analytics and smoke suites, CDP client
404.html generated; served by Pages for any unknown path
data/plaques.geojson the dataset
There are no accounts, so a collection lives in one browser. My London → Move to another browser produces a link that carries the whole save:
https://nyatasha.github.io/blueplaques/#restore=lu1-dh.fyz.1-e9.fyz-…
Opening it anywhere offers to restore. Details worth knowing:
__lu.backupUrl() returns the current link from the console.
Collecting a discovery as verified normally requires standing within 100 m of the plaque, which is awkward to exercise from a desk. Load the app with:
http://localhost:8000/?verify=anywhere
and any discovery can be collected as verified — the proximity check is skipped, and it works whether or not the browser has a location fix. While it is active the plaque sheet says "Test mode — collecting as verified without checking your location", so a test find cannot be mistaken for a real one.
It can also be toggled from the console without reloading:
__lu.verifyAnywhere(true) // skip the proximity check
__lu.verifyAnywhere(false) // back to normal
The override is off unless explicitly switched on and is never persisted — dropping the query parameter restores normal behaviour. The test suite asserts both that it defaults to off and that switching it off again restores the proximity check.
Other console helpers on __lu: progress(), events(), open(id),
collect(id, verified), backupUrl() and reset().
Events go to GoatCounter. Nothing is stored
on the visitor's device — no cookies, no localStorage, no fingerprint — so no
consent banner is required, and no coordinates or plaque ids are ever sent.
The generated static pages are measured too, through the same adapter:
js/static-page.js is their whole runtime, and it also powers their Share
button. tests/analytics.test.py reads the adapter and every call site and
fails if they disagree — an event with no case of its own would otherwise send
successfully and silently lose whichever property made it worth recording.
To check the integration locally, GoatCounter ignores localhost unless asked:
python3 -m http.server 8000
# open http://localhost:8000/?gc=local
__lu.events() shows every event with the exact path that was sent, whether or
not the beacon got through.
Full taxonomy, metric derivations and known limits: docs/analytics-plan.md.
Two things are deliberately left unset, each in one documented place:
ENDPOINT in js/analytics.js, pointing at GoatCounter.
Set it to '' to switch analytics off entirely. GoatCounter stores nothing on
the visitor's device, so the site needs no consent banner. The event taxonomy
and how each success metric is derived are in docs/analytics-plan.md.
No product code names a provider — everything calls trackEvent(name, props).BASEMAP in js/config.js. Defaults to Esri's Light Gray
Canvas, which needs no API key. CARTO Positron now watermarks every tile
requested without a key; the comment there explains how to switch back if
you register one.GitHub Pages serves the repository root as-is. Push to the deployment branch
and the site updates — the app itself has no build step. The only generated
output is plaques/, categories/, walks/, data/plaque-urls.json and
sitemap.xml;
regenerate them with python3 scripts/generate_seo_pages.py whenever
data/plaques.geojson changes, and commit the result.
Plaque data from OpenPlaques (public domain). Fame tiers derived from Wikipedia monthly pageview counts. Map tiles © Esri, HERE, Garmin and © OpenStreetMap contributors.
London Uncovered is an independent project and is not affiliated with or endorsed by English Heritage.