Three related improvements to the pre-rendered hero map system:
* New medium viewport variant (561–899 CSS px) for the per-hike detail
hero and the /hikes overview. Tablet/split-pane viewports were
getting the wide pose (chosen for ~1920 CSS px), which landed too
zoomed in. Each variant is rendered at a pose matching its
container, so the static→Leaflet handover aligns at every band.
Manifest fields are optional — pages fall back to the wide variant
on tablets until build-hikes regenerates the images.
* Overview frames on Switzerland (fixed center [46.82, 8.23]) with
explicit per-variant zooms (wide=8, medium=8, narrow=7) rather than
auto-fitting the union of hike bboxes. The previous behavior zoomed
in on whichever corner the catalogue clustered in; this reads as
"hikes across CH". Bumps OVERVIEW_RENDER_VERSION so cached overview
images get invalidated on the next build.
* Removed the post-tile-load flyToBounds in both HikeMap.svelte and
HikesOverviewMap.svelte. The map already opens at the static pose
via setView; the second auto-fit was adding a visible wobble on
routes whose bbox sits at an integer-zoom boundary (e.g. the
Einsiedeln–Unteriberg detail), where the build-time fit and
Leaflet's runtime fit disagree by one zoom step at the user's
actual container size.
Cards + filter bar fly up from below when arriving at /hikes and drop
back down when leaving (in both directions of /hikes ↔ detail). Clicked
card morphs into the detail hero with a cross-fade so the thumbnail
dissolves into the map instead of snapping. Photo strip slides in from
the right. Root content cross-fades so metrics + content under the hero
phase in rather than appear at the end of the morph.
Track JSON moves from a client-side $effect into +page.ts so the strip
is in the DOM at view-transition snapshot time — also kills the brief
layout shift when it used to pop in post-load.
Add tile-proxy/: a small Rust (axum) service behind nginx that serves one
canonical XYZ scheme (/{karte,luftbild,dufour}/{z}/{x}/{y}) and, per tile,
picks the provider by geometry — swisstopo when the tile overlaps a
swisstopo-covered region (Switzerland or Liechtenstein, each simplified +
2 km buffer; tile-bbox ∩ polygon at every zoom), otherwise OpenTopoMap
(schematic) / Esri World Imagery (satellite), with an auto-fallback for
border 404s. Includes the region generator (gen-regions.mjs), a Makefile,
nginx caching-proxy + systemd examples, and a README. Listen address is
env-driven (TILE_PROXY_ADDR).
App side:
- New mapTiles.ts is the single source for the proxy URLs + combined
attribution; HikeMap / HikesOverviewMap / EditMap fetch through
maps.bocken.org instead of swisstopo directly, on-map attribution
controls removed, preconnect + footer credits updated (swisstopo /
OpenStreetMap+OpenTopoMap / Esri).
- Region-aware schematic max zoom (isSwissRegion helper): detail map caps
at z17 abroad and hides the CH/LI-only Dufour layer; overview caps at
z18 when a shown hike is abroad.
- Route-builder: add the satellite layer via the same bottom-right layer
popover as the other maps.
Add two filters to the /hikes filter panel:
- "Nur Touren in der aktuellen Saison" toggle — keeps only hikes whose
recommended season window covers the current month (year-wrap aware;
hikes without a window count as year-round).
- "Kanton / Land" — a typeahead that abstracts the hike's area over the
border: Swiss hikes group by canton (coat-of-arms), hikes abroad by
country (flag). Generalised the tag typeahead into ChipTypeahead
(optional icon + label mapping) and reused it for both tags and areas.
Supporting bits: countries.ts (ISO/name → flag), hikeArea.ts (the
canton-or-country resolver, namespaced so codes can't collide), prepared
flag SVGs for CH/DE/IT/AT/FR, and an optional `country` field on the hike
manifest type (populated by the build script; the app falls back to canton
for Swiss hikes until a rebuild).
Replace the flat always-open filter slab with a collapsed command bar:
a result/totals summary, removable active-filter chips, and a Filter
toggle (with active-count badge). The control panel expands in-flow
below the bar (slide), pushing the card grid down instead of overlaying
it, with breathing room added between the hero map and the bar.
- Range filters are now dual-thumb sliders (lower + upper bound) for
distance, duration, ascent and descent, via a new RangeSlider
component (pointer + keyboard, crossover hand-off). Shared bounds math
lives in filterBounds.ts so slider extents and the page's default
filter state can't drift.
- Difficulty selection renders the actual SAC trail-sign markers
(T1 yellow Wegweiser, T2/T3 white-red-white, T4-T6 white-blue-white),
matching the hike cards; selected signs light up in full colour while
unselected ones are dimmed.
- Tag selection uses a typeahead (input + dropdown + removable chips),
mirroring the recipe filter but themed with the semantic variables.
Two bugs:
* Toggling a tag in the filter bar didn't update `?tag=` in the URL,
so the page wasn't shareable / back-button-restorable past the
initial deep-link state. Add a writer $effect that mirrors
`filter.tags` into the URL via `replaceState` (no history churn).
* The overview map's polylines + camera were built once on mount and
never refreshed when the `hikes` prop changed, so filtering left
the map showing the full set and zoomed for it. Extract polyline
rendering into a function called both on mount and from a
prop-watching $effect; on change, smoothly fly to the new union.
Detail-page tag chips become anchor links to `/hikes?tag=<name>`.
HikesFilterBar grows a tags fieldset (sorted by frequency, with the
hash prefix the chips use) so the user can keep narrowing from there.
Multi-tag filtering is OR — a hike matching any selected tag stays
visible. AND would shrink the listing fast given how few tags most
hikes carry; OR matches what "show me more like this" feels like.
The overview page reads `tag` query params on mount and pre-fills the
filter — supports repeated params (`?tag=winter&tag=easy`).
The wide static hero picks its zoom for a desktop-sized container
(fitWidth 1920), so on phones the bbox lands too zoomed-in: most of
the route falls outside the visible 400 CSS px band.
Build now emits a second pose per hero — rendered with fitWidth 400 /
fitHeight 480 onto a 1200² canvas — so the auto-fit zoom matches what
Leaflet picks at the same container size. Per-hike hero gains four
variants total (theme × viewport); overview hero gains two.
The page picks which `<img>` to show via a `max-width: 560px` media
query (no JS needed for the swap), and `matchMedia` decides which
pose to hand to Leaflet's first `setView` so the static→live cross-
fade aligns regardless of viewport.
Drive-by: replace the long-stale `hike.heroMapUrl` reference in the
detail page's track-loading fallback with `hike.heroMapUrlLight`.
Mirrors the per-hike detail-page hero on the /hikes index. Build emits one
WebP at the union bbox of every visible hike with each preview polyline
drawn in its SAC-tier colour; page renders it under the live Leaflet map
and fades it out once the first tile batch loads.
Tile fetcher now distinguishes HTTP 4xx ("intentionally blank — outside
Switzerland") from real network errors, so the larger overview canvas
that extends into DE/IT/FR doesn't trip the network-failure abort.
Build pipeline (scripts/build-hikes.ts) parses per-hike GPX, encodes
images via sharp, reverse-geocodes the centroid against Swisstopo and
emits a typed manifest under src/lib/data/hikes.generated.ts (gitignored).
Track JSON + image binaries live outside /static; served in dev by a
small hike-images plugin in vite.config.ts, in prod by nginx (private/
images proxied through Node + X-Accel-Redirect for auth-gating).
/hikes overview: full-bleed Swisstopo hero map (HikesOverviewMap) sits
under the sticky nav, drawing one polyline per route coloured by SAC
tier (T1 yellow Wegweiser, T2/T3 white-red-white, T4-T6 white-blue-
white). Click navigates, hover thickens + tooltips. Layer toggle,
recenter, GPS controls mirror the detail map (minus images toggle).
Cards drop the trail SVG, gain a per-route icon + SAC marker
pictogram on the cover, altitude range, season label, and "Neu" badge
for recently-published hikes. Filter bar + totals strip recompute over
the currently-visible set.
/hikes/[slug]: hero map with elevation profile, photo strip with map
sync, scroll-position pin, GPX download, SAC marker stats + min/max
altitude + season.
Route-builder (/hikes/route-builder): client-side draft persisted to
localStorage, EXIF-driven image placement, snap-to-route via BRouter
(OSRM + linear fallback) and Swisstopo profile.json elevation
enrichment that handles degenerate same-coord segments via the height
endpoint.
Filter init switched from a script-time snapshot of data.hikes (which
sporadically returned a one-hike subset during dev hydration and
locked the page to that single hike) to a post-mount \$effect.
Content under src/content/hikes/ intentionally not included (WIP).