552 lines
21 KiB
Markdown
552 lines
21 KiB
Markdown
# Design Spec: Story Mode + MapLibre Migration
|
||
|
||
*Date: 2026-06-19*
|
||
*Inspired by: [Sabdia](https://github.com/m-cluitmans/Sabdia) — a friend's sabbatical blog built on Astro + Keystatic + MapLibre*
|
||
|
||
---
|
||
|
||
## Scope
|
||
|
||
Two parallel features:
|
||
|
||
1. **Story Mode** — a rich long-form post type alongside journal dailies, with cinematic
|
||
storytelling blocks (hero, chapter breaks, scrollytelling, pull quotes, snap gallery)
|
||
2. **MapLibre GL migration** — replace Leaflet across all three maps (full map, mini-map,
|
||
home map) with MapLibre GL JS; add animated journey line; improve CSS integration
|
||
|
||
---
|
||
|
||
## Decisions Log
|
||
|
||
### Why MapLibre GL instead of Leaflet
|
||
|
||
Leaflet renders raster PNG tiles. MapLibre GL renders vector tiles in WebGL. Key gains:
|
||
|
||
- **Animated journey line** — MapLibre's GeoJSON source model makes RAF-loop animation
|
||
trivial (`source.setData()` per frame). On Leaflet you'd call `polyline.setLatLngs()`
|
||
which also works, but MapLibre gives us everything below for free too.
|
||
- **Smooth zoom** — continuous sub-pixel zoom vs Leaflet's tile-snap zoom levels
|
||
- **Retina crisp** — vector geometry scales perfectly on HiDPI screens
|
||
- **Future-proof** — 3D terrain, tilt/pitch, per-feature click events, style control,
|
||
outdoor/topo/satellite styles for GPX track maps all become straightforward
|
||
- **GPX styling** — switching from `leaflet-gpx` to `@mapbox/togeojson` + GeoJSON layer
|
||
gives per-point colour control (speed, elevation gradients) later
|
||
|
||
Cost: ~280KB (vs ~40KB Leaflet). Acceptable — cached after first visit.
|
||
|
||
Tile source stays the same: CARTO dark vector style — free, no API key.
|
||
|
||
### Why shortcodes for story blocks (not modular pages or blueprint lists)
|
||
|
||
Evaluated three approaches for in-prose storytelling blocks:
|
||
|
||
| Approach | What it is | Verdict |
|
||
|---|---|---|
|
||
| **Shortcodes** | `[chapter-break ...]` inline in Markdown | ✅ Chosen |
|
||
| Modular pages | Each block = a child page in Admin | ✗ Ruled out |
|
||
| Blueprint list + elements | `sections:` YAML list with type selector | ✗ Ruled out |
|
||
|
||
**Modular pages** are how most Grav storytelling themes work (Quark, Oxygen, all
|
||
HTML5UP ports). Each block gets proper Admin form fields. But a 1,500-word story with
|
||
two chapter breaks requires five child pages — navigating between them on mobile while
|
||
traveling is painful. Prose ends up fragmented across "text" module pages.
|
||
|
||
**Blueprint list with elements field** (Grav's conditional field groups) could render
|
||
blocks as a structured "Add section" list in Admin. But prose still has to go in a
|
||
"text" type section, so a story becomes a long list of `text/chapter-break/text/scrolly/
|
||
text/gallery` entries rather than a flowing document.
|
||
|
||
**Shortcodes** keep everything in one Markdown editor — prose flows naturally, blocks are
|
||
inserted inline. The `shortcode-gallery-plusplus` plugin already in our stack brings
|
||
`shortcode-core` as a dependency, so no new plugin is needed.
|
||
|
||
Grav Admin2 has no rich block-editor like Keystatic/Markdoc. Shortcodes are the
|
||
closest practical equivalent for mixed prose+blocks authoring on mobile.
|
||
|
||
*Future option:* If Admin2 ever gains inline block components (or we add a Flex Object
|
||
definition), the shortcode content can be migrated — the block semantics are identical.
|
||
|
||
### Why gallery stays as lightbox on journal entries
|
||
|
||
Journal entries are short daily posts — a grid of 3–8 photos suits them.
|
||
The snap gallery is a deliberate slow storytelling device (one photo fills the screen,
|
||
reader swipes through). That pacing fits stories, not a daily feed card.
|
||
|
||
### Weather not added to story frontmatter
|
||
|
||
Weather is a journal-entry concept (captured at the moment of a daily post via
|
||
Open-Meteo). Stories are retrospective long-form narratives — weather would be referenced
|
||
in prose if relevant, not as a metadata badge.
|
||
|
||
---
|
||
|
||
## Part 1 — Story Mode
|
||
|
||
### 1.1 Page structure
|
||
|
||
Stories live as child pages under `04.stories/`:
|
||
|
||
```
|
||
user/pages/01.trips/<trip-slug>/04.stories/
|
||
stories.md ← listing page, template: stories
|
||
01.<story-slug>/
|
||
story.md ← individual story, template: story
|
||
hero.jpg
|
||
photo-a.jpg
|
||
photo-b.jpg
|
||
```
|
||
|
||
`stories.md` frontmatter:
|
||
```yaml
|
||
title: Stories
|
||
template: stories
|
||
published: true
|
||
```
|
||
|
||
### 1.2 Story frontmatter schema
|
||
|
||
```yaml
|
||
title: Into the Hills of Kyoto
|
||
date: 2026-03-28 # start date — shown in hero header
|
||
end_date: 2026-03-29 # optional; shown as "28–29 Mar 2026"
|
||
location_name: Kyoto # city/region; shown in hero header
|
||
location_country: Japan # used for stats de-duplication
|
||
lat: 34.967 # main GPS coordinate — shows pin on /map
|
||
lng: 135.773
|
||
hero_image: hero.jpg # filename in page media; required for hero section
|
||
hero_alt: The vermillion gate at Fushimi Inari at dawn
|
||
published: true
|
||
```
|
||
|
||
Fields deliberately excluded: `weather_*` (not meaningful for stories).
|
||
|
||
### 1.3 Shortcode blocks
|
||
|
||
Four blocks implemented as ShortcodeCore shortcodes.
|
||
All image paths are **filenames only** (e.g. `shrine.jpg`) — resolved against the story's
|
||
own page media folder, same convention as `hero_image`.
|
||
|
||
#### ChapterBreak
|
||
|
||
Full-bleed atmospheric photo with a frosted-glass title panel. Reveals on scroll via
|
||
IntersectionObserver (blur + translateY → clear).
|
||
|
||
```
|
||
[chapter-break image="shrine-gate.jpg" title="The Long Walk Up" number="II" /]
|
||
```
|
||
|
||
| Attribute | Required | Description |
|
||
|---|---|---|
|
||
| `image` | yes | Page media filename |
|
||
| `title` | yes | Chapter title, displayed in frosted panel |
|
||
| `number` | no | Roman numeral or label shown above title |
|
||
| `alt` | no | Alt text (defaults to `title`) |
|
||
|
||
Renders as `60vh` full-bleed block with dark gradient tint over the image and a
|
||
`backdrop-filter: blur(18px)` panel containing the chapter number + title + teal rule.
|
||
|
||
#### ScrollySection
|
||
|
||
NYT-style sticky image (55% left column) with text panels that scroll past on the right.
|
||
Steps are separated by `---` inside the shortcode body. Powered by **Scrollama** (CDN).
|
||
|
||
```
|
||
[scrolly-section image="torii-path.jpg" alt="Thousands of torii gates"]
|
||
The path stretched further than I could see.
|
||
|
||
---
|
||
|
||
Each gate was donated by a business or family, a prayer made physical.
|
||
|
||
---
|
||
|
||
By the tenth minute of walking, the city had disappeared entirely.
|
||
[/scrolly-section]
|
||
```
|
||
|
||
| Attribute | Required | Description |
|
||
|---|---|---|
|
||
| `image` | yes | Page media filename — sticky background |
|
||
| `alt` | no | Image alt text |
|
||
| `caption` | no | Small caption shown bottom-left of image |
|
||
|
||
On mobile: full-screen sticky image with text panels scrolling over it (same layout,
|
||
single column — image behind, text on top with semi-transparent card).
|
||
|
||
Image starts blurred (`blur(8px) scale(1.04)`), unblurs when section enters viewport.
|
||
Between steps: subtle pan (object-position cycles through 5 offsets) + slight overlay
|
||
darkening for depth.
|
||
|
||
#### PullQuote
|
||
|
||
Frosted-glass quote block with optional atmospheric background image. Reveals on scroll.
|
||
|
||
```
|
||
[pull-quote image="lanterns.jpg"]
|
||
The torii gates never seemed to end — and I didn't want them to.
|
||
[/pull-quote]
|
||
```
|
||
|
||
| Attribute | Required | Description |
|
||
|---|---|---|
|
||
| `image` | no | Page media filename — background photo |
|
||
| `alt` | no | Alt text for background image |
|
||
|
||
Without `image`: renders on `--color-canvas` (warm dark surface, solid).
|
||
With `image`: full-bleed image behind frosted glass panel.
|
||
|
||
Large decorative `"` marks above and below the quote text (DM Serif Display, 5rem).
|
||
|
||
#### SnapGallery
|
||
|
||
Full-screen snap-scroll photo sequence. One photo per swipe. Snap physics are pure CSS
|
||
(`scroll-snap-type: y mandatory` + `scroll-snap-stop: always` on the scroll container).
|
||
Dot indicator active state updated via a small IntersectionObserver on each slide.
|
||
|
||
```
|
||
[snap-gallery images="photo-a.jpg,photo-b.jpg,photo-c.jpg" captions="The approach,Summit view,Descent" alts="Hikers on trail,Mountain panorama,Forest path" /]
|
||
```
|
||
|
||
| Attribute | Required | Description |
|
||
|---|---|---|
|
||
| `images` | yes | Comma-separated page media filenames |
|
||
| `captions` | no | Comma-separated captions (positional) |
|
||
| `alts` | no | Comma-separated alt texts (positional) |
|
||
|
||
Each slide: blurred cover-crop background + contain-fit foreground image + caption fades
|
||
in at bottom. Dot indicator on the right edge. Page-level `scroll-snap-align: start`
|
||
with `proximity` (not mandatory) so normal page scroll is unaffected.
|
||
|
||
### 1.4 Template: `story.html.twig`
|
||
|
||
Extends `partials/base.html.twig` but overrides the nav block to show only a floating
|
||
escape link. Full layout:
|
||
|
||
```
|
||
┌────────────────────────────────────────┐
|
||
│ ← Back (position: fixed, top-left) │
|
||
│ │
|
||
│ HERO — 100vh │
|
||
│ sticky image, Ken Burns zoom-out │
|
||
│ title blurs up from bottom │
|
||
│ date · location beneath title │
|
||
│ ↓ bounce scroll indicator │
|
||
│ 40vh spacer (scroll trigger zone) │
|
||
│ │
|
||
├────────────────────────────────────────┤
|
||
│ STORY BODY │
|
||
│ max-width: 680px, centred │
|
||
│ font: DM Serif Display (headings) │
|
||
│ DM Sans (prose) │
|
||
│ {{ page.content|raw }} │
|
||
│ (Markdown + shortcode blocks) │
|
||
│ │
|
||
│ ← Back to stories (footer) │
|
||
└────────────────────────────────────────┘
|
||
```
|
||
|
||
**Hero scroll behaviour (vanilla JS, no library):**
|
||
|
||
- `window.scroll` listener (passive, rAF-throttled)
|
||
- `progress = scrollY / innerHeight` (0→1 as hero scrolls away)
|
||
- At progress > 0: dark overlay fades in (`rgba(0,0,0, progress * 0.6)`)
|
||
- Scroll indicator hides after `scrollY > 80px`
|
||
- At progress ≥ 1: overlay removed from DOM
|
||
|
||
**Ken Burns animation:** CSS `@keyframes` — `scale(1.06) → scale(1)` over 12s,
|
||
`ease-out`, `forwards`. Respects `prefers-reduced-motion: reduce`.
|
||
|
||
**Text reveal:** Title and date animate in with `filter: blur(10px) + translateY(22px)
|
||
→ clear` at 0.2s / 0.55s delay. Respects `prefers-reduced-motion`.
|
||
|
||
### 1.5 Template: `stories.html.twig`
|
||
|
||
Listing of published stories for the active trip. Grid of story cards:
|
||
|
||
```
|
||
┌──────────────┐ ┌──────────────┐
|
||
│ hero thumb │ │ hero thumb │
|
||
│ │ │ │
|
||
│ Kyoto Hills │ │ Seoul Rain │
|
||
│ 28–29 Mar │ │ 1 Apr │
|
||
│ Kyoto │ │ Seoul │
|
||
└──────────────┘ └──────────────┘
|
||
```
|
||
|
||
2-column grid on desktop, single column on mobile. Each card links to the story.
|
||
Empty state: "No stories yet — check back soon."
|
||
|
||
Stories are also listed as cards in `dailies.html.twig`'s combined feed (already
|
||
implemented — the template merges journal entries and stories by date).
|
||
|
||
### 1.6 JS dependencies
|
||
|
||
| Library | How loaded | Size | Purpose |
|
||
|---|---|---|---|
|
||
| **Scrollama** | CDN (`jsdelivr`) | ~4KB | ScrollySection step detection |
|
||
| IntersectionObserver | Native browser API | — | ChapterBreak + PullQuote reveal, SnapGallery dots |
|
||
|
||
Scrollama is only loaded on story pages (inline `<script src>` in `story.html.twig`).
|
||
|
||
### 1.7 CSS additions (story-specific)
|
||
|
||
New CSS block added to `style.css` under a `/* ── Story pages ──` section:
|
||
|
||
**Story layout:**
|
||
- `.story-hero` — `position: relative; height: 100vh; overflow: hidden`
|
||
- `.story-hero__img` — `position: sticky; top: 0; width: 100%; height: 100vh; object-fit: cover`
|
||
- `.story-hero__overlay` — `position: fixed; inset: 0; pointer-events: none` (JS-driven opacity)
|
||
- `.story-hero__content` — `position: absolute; bottom: 18%; text-align: center; color: #fff`
|
||
- `.story-escape` — `position: fixed; top: 1rem; left: 1rem; z-index: 100; color: var(--color-ink); background: var(--color-canvas); ...`
|
||
- `.story-body` — `max-width: 680px; margin: 0 auto; padding: var(--space-16) var(--space-6)`
|
||
- `.story-body p` — `font-family: var(--font-ui); font-size: 1.0625rem; line-height: 1.85; color: var(--color-ink-2)`
|
||
|
||
**ChapterBreak:**
|
||
- `.chapter-break` — full-bleed breakout, `60vh`, overflow hidden
|
||
- `.chapter-break__panel` — `backdrop-filter: blur(18px); background: rgba(26,24,20,0.25); border: 1px solid rgba(255,255,255,0.12); border-radius: var(--radius-sm)`
|
||
- Initial state: `opacity: 0; filter: blur(12px); transform: translateY(28px)` → `.is-revealed` clears all
|
||
- `.chapter-break__rule` — `40px × 2px` teal (`var(--color-accent)`) rule below title
|
||
|
||
**ScrollySection:**
|
||
- `.scrolly` — `display: grid; grid-template-columns: 55% 45%; width: 100vw` (full-bleed breakout)
|
||
- `.scrolly__media` — `position: sticky; top: var(--site-header-height); height: calc(100vh - var(--site-header-height))`
|
||
- `.scrolly-step__inner` — `background: rgba(26,24,20,0.92); backdrop-filter: blur(4px); border-radius: var(--radius-sm); border: 1px solid var(--color-border)`
|
||
- Mobile (`max-width: 768px`): single column, steps overlay the sticky image with `margin-top: calc(-(100vh - var(--site-header-height)))`
|
||
|
||
**PullQuote:**
|
||
- `.pull-quote` — bleeds `1.5rem` each side beyond prose column
|
||
- `.pull-quote__inner` — `backdrop-filter: blur(14px); background: rgba(26,24,20,0.12)` (with image) or `var(--color-canvas)` (without)
|
||
- Large `"` marks: `font-family: var(--font-display); font-size: 5rem; color: var(--color-accent); opacity: 0.4`
|
||
|
||
**SnapGallery:**
|
||
- `.pgallery__frame` — `height: 100vh; scroll-snap-type: y mandatory; overflow-y: scroll`
|
||
- `.pgallery__bg` — `object-fit: cover; filter: blur(20px) brightness(0.4)` (blurred backdrop)
|
||
- `.pgallery__fg` — `object-fit: contain` (full foreground image)
|
||
- `.pgallery__dot.is-active` — `background: var(--color-accent)`
|
||
|
||
All animations respect `prefers-reduced-motion: reduce` — transitions set to `none`,
|
||
initial states set to final states immediately.
|
||
|
||
### 1.8 Demo story content
|
||
|
||
One sample story added to `user/docs/demo/trips/japan-korea-2026/` following existing
|
||
demo conventions. Story covers 28–29 March (Kyoto days already in journal demo):
|
||
|
||
```
|
||
user/docs/demo/trips/japan-korea-2026/04.stories/01.the-thousand-gates/
|
||
story.md
|
||
```
|
||
|
||
Frontmatter mirrors the schema. Body uses all four shortcode types so they can be QA'd
|
||
in one pass. No binary image assets — `make demo-load` copies the folder; tester drops
|
||
a few JPEGs in to exercise hero + photo blocks.
|
||
|
||
---
|
||
|
||
## Part 2 — MapLibre GL Migration
|
||
|
||
### 2.1 Scope
|
||
|
||
Three files change. No new page routes. GPX file storage and delivery unchanged.
|
||
|
||
| File | Change |
|
||
|---|---|
|
||
| `map.html.twig` | Full rewrite of JS + CDN refs; CSS class renames |
|
||
| `dailies.html.twig` | Mini-map JS + CDN refs rewritten |
|
||
| `home.html.twig` | Home map JS + CDN refs rewritten |
|
||
| `style.css` | Leaflet overrides removed; MapLibre overrides added |
|
||
|
||
CDN changes (all three map templates):
|
||
|
||
```html
|
||
<!-- Remove -->
|
||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/leaflet@1.9.4/dist/leaflet.min.css">
|
||
<script src="https://cdn.jsdelivr.net/npm/leaflet@1.9.4/dist/leaflet.min.js"></script>
|
||
<script src="https://cdn.jsdelivr.net/npm/leaflet-gpx@2.1.2/gpx.min.js"></script>
|
||
|
||
<!-- Add -->
|
||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/maplibre-gl@4/dist/maplibre-gl.css">
|
||
<script src="https://cdn.jsdelivr.net/npm/maplibre-gl@4/dist/maplibre-gl.js"></script>
|
||
<!-- GPX maps only: -->
|
||
<script src="https://cdn.jsdelivr.net/npm/@mapbox/togeojson@0.16.2/togeojson.min.js"></script>
|
||
```
|
||
|
||
Tile style URL (same CARTO dark, now as vector style):
|
||
```
|
||
https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json
|
||
```
|
||
|
||
### 2.2 Animated journey line
|
||
|
||
Port of Sabdia's `animateJourneyLine` to vanilla JS against MapLibre's GeoJSON source API:
|
||
|
||
```js
|
||
map.on('load', () => {
|
||
// Add an empty source
|
||
map.addSource('journey', {
|
||
type: 'geojson',
|
||
data: { type: 'Feature', properties: {}, geometry: { type: 'LineString', coordinates: [] } }
|
||
});
|
||
|
||
// Glow layer (wide, low opacity)
|
||
map.addLayer({ id: 'journey-glow', type: 'line', source: 'journey',
|
||
paint: { 'line-color': '#2A8C73', 'line-width': 6, 'line-opacity': 0.18 }
|
||
});
|
||
|
||
// Main line
|
||
map.addLayer({ id: 'journey-line', type: 'line', source: 'journey',
|
||
paint: { 'line-color': '#2A8C73', 'line-width': 2.5, 'line-opacity': 0.85 }
|
||
});
|
||
|
||
animateJourneyLine(map, coords); // RAF loop, ease-out cubic, 5000ms
|
||
});
|
||
```
|
||
|
||
RAF loop builds coordinate array incrementally using cumulative Euclidean distance +
|
||
ease-out cubic easing. On `prefers-reduced-motion: reduce`: skip animation, set full
|
||
coordinates immediately.
|
||
|
||
Teal values use `var(--color-accent)` equivalent (`#2A8C73`) — matches our design tokens.
|
||
|
||
### 2.3 GPX rendering
|
||
|
||
Replace `leaflet-gpx` with `@mapbox/togeojson` + MapLibre GeoJSON source:
|
||
|
||
```js
|
||
fetch(gpxUrl)
|
||
.then(r => r.text())
|
||
.then(text => {
|
||
const gpx = new DOMParser().parseFromString(text, 'text/xml');
|
||
const geojson = toGeoJSON.gpx(gpx);
|
||
map.addSource('gpx-track', { type: 'geojson', data: geojson });
|
||
map.addLayer({
|
||
id: 'gpx-track-line', type: 'line', source: 'gpx-track',
|
||
paint: { 'line-color': '#2A8C73', 'line-width': 2, 'line-opacity': 0.7 }
|
||
});
|
||
});
|
||
```
|
||
|
||
Multiple GPX files (trip has several tracks): each gets its own numbered source/layer pair.
|
||
|
||
### 2.4 Markers and popups
|
||
|
||
MapLibre uses `maplibregl.Marker` (custom DOM element) + `maplibregl.Popup`.
|
||
Existing popup HTML content (hero thumbnail, date, title, link) is unchanged.
|
||
|
||
Marker style (same visual as current):
|
||
- Regular entries: `12px` teal dot with white border
|
||
- Latest/current entry: `18px` teal dot with outer ring (`box-shadow: 0 0 0 4px rgba(42,140,115,0.25)`)
|
||
|
||
Popup styled via CSS (see §2.5).
|
||
|
||
### 2.5 CSS improvements over Leaflet
|
||
|
||
**Remove (Leaflet-specific):**
|
||
```css
|
||
/* DELETE — no longer needed */
|
||
.leaflet-container { background: #282828 !important; }
|
||
```
|
||
|
||
MapLibre sets its canvas background from the style JSON (`background-color` in the style's
|
||
`background` layer). CARTO dark-matter style uses `#1a1a1a` — no flash on load.
|
||
|
||
**Add (MapLibre):**
|
||
```css
|
||
/* ── MapLibre GL overrides ───────────────────────────────────────────────────── */
|
||
|
||
/* Navigation controls (zoom +/−, compass) */
|
||
.maplibregl-ctrl-group {
|
||
background: var(--color-canvas);
|
||
border: 1px solid var(--color-border);
|
||
border-radius: var(--radius-sm);
|
||
box-shadow: var(--shadow-sm);
|
||
}
|
||
.maplibregl-ctrl-group button {
|
||
color: var(--color-ink-2);
|
||
}
|
||
.maplibregl-ctrl-group button:hover {
|
||
background: var(--color-surface-raised);
|
||
color: var(--color-ink);
|
||
}
|
||
.maplibregl-ctrl-group button + button {
|
||
border-top: 1px solid var(--color-border);
|
||
}
|
||
|
||
/* Attribution bar */
|
||
.maplibregl-ctrl-attrib {
|
||
background: rgba(26,24,20,0.75) !important;
|
||
color: var(--color-ink-muted) !important;
|
||
font-family: var(--font-ui);
|
||
font-size: 0.7rem;
|
||
backdrop-filter: blur(4px);
|
||
}
|
||
.maplibregl-ctrl-attrib a {
|
||
color: var(--color-accent) !important;
|
||
}
|
||
|
||
/* Popup */
|
||
.maplibregl-popup-content {
|
||
background: var(--color-canvas);
|
||
color: var(--color-ink);
|
||
font-family: var(--font-ui);
|
||
border: 1px solid var(--color-border);
|
||
border-radius: var(--radius-md);
|
||
box-shadow: var(--shadow-md);
|
||
padding: var(--space-4);
|
||
}
|
||
.maplibregl-popup-tip {
|
||
border-top-color: var(--color-canvas) !important;
|
||
}
|
||
.maplibregl-popup-close-button {
|
||
color: var(--color-ink-muted);
|
||
font-size: 1.1rem;
|
||
padding: var(--space-1) var(--space-2);
|
||
}
|
||
.maplibregl-popup-close-button:hover {
|
||
color: var(--color-ink);
|
||
background: transparent;
|
||
}
|
||
|
||
/* Cursor — pointer hand over clickable markers */
|
||
.maplibregl-canvas-container.maplibregl-interactive {
|
||
cursor: grab;
|
||
}
|
||
.maplibregl-canvas-container.maplibregl-interactive:active {
|
||
cursor: grabbing;
|
||
}
|
||
```
|
||
|
||
**Mobile scroll-trap prevention:** For embedded maps (mini-map on dailies, home map),
|
||
initialize with `cooperativeGestures: true` — requires two fingers to pan on touch.
|
||
The full-page `/map` uses normal gestures (`cooperativeGestures: false`, the default).
|
||
*Note: verify `cooperativeGestures` is available in the chosen MapLibre GL 4.x version
|
||
during implementation; if absent, use `dragPan: false` on touch-only + a two-finger
|
||
hint overlay as fallback.*
|
||
|
||
### 2.6 What is NOT migrated now
|
||
|
||
Features from Sabdia's map that were explicitly deferred:
|
||
|
||
| Feature | Decision |
|
||
|---|---|
|
||
| Ghost pins for upcoming/planned stops | Documented; deferred — requires `show_preview` frontmatter field + Twig logic |
|
||
| Pulsing amber dot for current location | Documented; deferred — requires "current entry" detection logic |
|
||
| `flyTo()` on marker click | Deferred — nice UX upgrade, implement after migration stabilises |
|
||
| 3D terrain | Deferred — requires DEM tile source (MapTiler key) |
|
||
| Per-story inline MapBlock shortcode | Deferred — implement as part of story mode v2 |
|
||
| MapTiler outdoor/satellite/topo styles for GPX | Deferred — requires MapTiler API key |
|
||
|
||
These are preserved here so they can be picked up in a later milestone without needing
|
||
to re-research the Sabdia implementation.
|
||
|
||
---
|
||
|
||
## Out of scope
|
||
|
||
- Story-specific inline MapBlock shortcode (deferred, see §2.6)
|
||
- Animated hero video (requires server-side FFmpeg, not available in Grav)
|
||
- Push notifications for new stories
|
||
- Story-level statistics (word count, reading time)
|
||
- Co-authoring / Travel Buddy equivalent
|
||
- 3D flyover video
|