Motivation
The embed (apps/embed, <ote-events>) currently offers list and calendar
layouts. A map view would help visitors explore in-person events
geographically, which is especially useful for people who don't know the
local geography well (e.g. "what's happening near me" browsing).
Proposed behavior
Add layout="map" alongside the existing list/calendar values.
Events with location.geo — plotted as pins. This is the only case that
gets an exact pin.
Events with location.venue but no location.geo — do not
geocode them client-side. We considered a live geocoder (Nominatim, as
apps/editor/src/ui/map.ts#geocodeVenue already does) and a bundled
offline city gazetteer, and rejected both for the embed:
- A live geocoding call per pageview/per visitor doesn't scale and conflicts
with Nominatim's usage policy (rate limits, no bulk/automated client use).
- An offline gazetteer avoids the network problem but not the accuracy one:
location.venue is a single free-text string with no structured
city/country, so matching it against a city list is exactly where it's
fragile — common city names collide worldwide (Valencia, Córdoba,
Cambridge, San José, Toledo, Guadalajara...). A confidently wrong pin is
worse than no pin, particularly for the "I don't know this area" use case
this view is meant to serve.
Instead: these events are excluded from the map and surfaced honestly —
a plain on-page notice near the map (e.g. "N events aren't shown on the map
because they don't have coordinates") plus a console.warn for the
integrating developer, both pointing at how to fix the source data (the
geocoding picker already in apps/editor, where a human confirms the pin
before it's published in location.geo). No pin is invented.
Online-only events (no location.venue at all) — these aren't "missing
coordinates", they have no physical location to plot. Don't lump them into
the "missing coordinates" notice above. Give them their own treatment: a
separate section/badge/count (e.g. "N online events") rather than silently
dropping them or conflating them with unmappable in-person events.
Hybrid events (location.geo and onlineUrl) — get a pin, with the
popup/card also noting the online option.
Non-goals
- No client-side/live geocoding of any kind in the embed bundle.
- No exact-vs-approximate pin distinction — an event either has
geo and
gets an exact pin, or it doesn't and isn't plotted.
- No deep-linkable map center/zoom URL state for this iteration — this ships
as one view among the existing layouts, not a standalone map component.
Implementation notes
- Follow the
src/calendar-layout.ts pattern: a separate module, never
statically imported by src/main.ts, loaded via import() only when a
consumer requests layout="map", so the base ote-events.js bundle stays
minimal (per apps/embed/CLAUDE.md).
- Leaflet + OSM tiles is the existing precedent for maps in this repo
(apps/editor/src/ui/map.ts) — reuse that approach/attribution rather than
introducing a new mapping dependency.
- This is a new public layout value → needs a SemVer minor bump in
apps/embed/package.json, a CHANGELOG.md entry, and the rest of the
release checklist in apps/embed/CLAUDE.md.
- Read
apps/embed/CLAUDE.md before starting.
Possible follow-up (separate issue)
A non-blocking packages/validate warning for location.venue present
without location.geo would surface this at publish/CI time instead of only
in the embed at render time, following the existing "absent field = absent +
warning" convention. Not part of this issue's scope.
Motivation
The embed (
apps/embed,<ote-events>) currently offers list and calendarlayouts. A map view would help visitors explore in-person events
geographically, which is especially useful for people who don't know the
local geography well (e.g. "what's happening near me" browsing).
Proposed behavior
Add
layout="map"alongside the existinglist/calendarvalues.Events with
location.geo— plotted as pins. This is the only case thatgets an exact pin.
Events with
location.venuebut nolocation.geo— do notgeocode them client-side. We considered a live geocoder (Nominatim, as
apps/editor/src/ui/map.ts#geocodeVenuealready does) and a bundledoffline city gazetteer, and rejected both for the embed:
with Nominatim's usage policy (rate limits, no bulk/automated client use).
location.venueis a single free-text string with no structuredcity/country, so matching it against a city list is exactly where it'sfragile — common city names collide worldwide (Valencia, Córdoba,
Cambridge, San José, Toledo, Guadalajara...). A confidently wrong pin is
worse than no pin, particularly for the "I don't know this area" use case
this view is meant to serve.
Instead: these events are excluded from the map and surfaced honestly —
a plain on-page notice near the map (e.g. "N events aren't shown on the map
because they don't have coordinates") plus a
console.warnfor theintegrating developer, both pointing at how to fix the source data (the
geocoding picker already in
apps/editor, where a human confirms the pinbefore it's published in
location.geo). No pin is invented.Online-only events (no
location.venueat all) — these aren't "missingcoordinates", they have no physical location to plot. Don't lump them into
the "missing coordinates" notice above. Give them their own treatment: a
separate section/badge/count (e.g. "N online events") rather than silently
dropping them or conflating them with unmappable in-person events.
Hybrid events (
location.geoandonlineUrl) — get a pin, with thepopup/card also noting the online option.
Non-goals
geoandgets an exact pin, or it doesn't and isn't plotted.
as one view among the existing layouts, not a standalone map component.
Implementation notes
src/calendar-layout.tspattern: a separate module, neverstatically imported by
src/main.ts, loaded viaimport()only when aconsumer requests
layout="map", so the baseote-events.jsbundle staysminimal (per
apps/embed/CLAUDE.md).(
apps/editor/src/ui/map.ts) — reuse that approach/attribution rather thanintroducing a new mapping dependency.
apps/embed/package.json, aCHANGELOG.mdentry, and the rest of therelease checklist in
apps/embed/CLAUDE.md.apps/embed/CLAUDE.mdbefore starting.Possible follow-up (separate issue)
A non-blocking
packages/validatewarning forlocation.venuepresentwithout
location.geowould surface this at publish/CI time instead of onlyin the embed at render time, following the existing "absent field = absent +
warning" convention. Not part of this issue's scope.