diff --git a/docs/superpowers/plans/2026-06-19-trip-entity.md b/docs/superpowers/plans/2026-06-19-trip-entity.md new file mode 100644 index 0000000..007e21b --- /dev/null +++ b/docs/superpowers/plans/2026-06-19-trip-entity.md @@ -0,0 +1,538 @@ +# Trip Entity Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use `superpowers:subagent-driven-development` to implement this plan task-by-task. + +**Goal:** Restructure the site around a Trip entity — tracker/map/stats/stories become children of `/trips/japan-korea-2026/`, GPX route files live as media on the trip page, and `site.yaml` holds an `active_trip` slug so the nav can switch trips via config. + +**Architecture:** Trip = a Grav page (`trip.html.twig`) at `/trips//`. Map/stats templates find the tracker via `page.parent().route ~ '/tracker'` instead of the hardcoded `/tracker` path. Leaflet-gpx (CDN) loads all `*.gpx` media files from the trip page. A `trips.html.twig` listing page provides the multi-trip root. Stories is stubbed with a placeholder template. + +**Tech Stack:** Grav CMS 1.7/2.0, Twig, Leaflet.js, leaflet-gpx (CDN, vanilla JS — consistent with existing inline JS pattern) + +## Global Constraints + +- All content/theme edits go in `user/` — commit with `git -C user`, not main-repo git +- Entry URLs change: `/tracker/` → `/trips/japan-korea-2026/tracker/` — acceptable pre-launch +- `make test-post` (6/6) and `make test-ui` (25/25) must pass after every task +- No new JS framework dependencies; leaflet-gpx is 3KB vanilla JS +- `user/config/media.yaml` must whitelist `.gpx` so Grav serves it as a file +- The `02.post/post-form.md` `pageconfig.parent` must stay in sync with the tracker path + +--- + +### Task 1: Restructure pages under `/trips/` + +**Files:** +- Create: `user/pages/01.trips/trips.md` +- Create: `user/pages/01.trips/japan-korea-2026/trip.md` +- Create: `user/pages/01.trips/japan-korea-2026/01.tracker/tracker.md` (copy from `user/pages/01.tracker/tracker.md`, no content change) +- Move: all `*.entry/` folders from `user/pages/01.tracker/` → `user/pages/01.trips/japan-korea-2026/01.tracker/` +- Create: `user/pages/01.trips/japan-korea-2026/02.map/map.md` (copy from `user/pages/03.map/map.md`) +- Create: `user/pages/01.trips/japan-korea-2026/03.stats/stats.md` (copy from `user/pages/04.stats/stats.md`) +- Create: `user/pages/01.trips/japan-korea-2026/04.stories/stories.md` +- Delete: `user/pages/01.tracker/`, `user/pages/03.map/`, `user/pages/04.stats/` +- Modify: `user/config/site.yaml` — add `active_trip: japan-korea-2026` +- Modify (create if absent): `user/config/media.yaml` — whitelist GPX + +- [ ] **Step 1: Verify current structure before touching anything** + +```bash +find user/pages -name "*.md" | sort +``` +Expected: entries under `01.tracker/`, map at `03.map/map.md`, stats at `04.stats/stats.md`. + +- [ ] **Step 2: Create trips hierarchy** + +```bash +mkdir -p user/pages/01.trips/japan-korea-2026/01.tracker +mkdir -p user/pages/01.trips/japan-korea-2026/02.map +mkdir -p user/pages/01.trips/japan-korea-2026/03.stats +mkdir -p user/pages/01.trips/japan-korea-2026/04.stories +``` + +- [ ] **Step 3: Write `trips.md`** + +`user/pages/01.trips/trips.md`: +```yaml +--- +title: Trips +template: trips +content: + items: '@self.children' + order: + by: date + dir: desc +--- +``` + +- [ ] **Step 4: Write `trip.md`** + +`user/pages/01.trips/japan-korea-2026/trip.md`: +```yaml +--- +title: 'Japan & Korea 2026' +template: trip +date: '2026-06-17' +date_start: '2026-06-17' +date_end: '' +cover_image: '' +content: + items: '@self.children' +--- +``` + +- [ ] **Step 5: Copy tracker.md, move entries** + +```bash +cp user/pages/01.tracker/tracker.md user/pages/01.trips/japan-korea-2026/01.tracker/tracker.md +mv user/pages/01.tracker/*.entry user/pages/01.trips/japan-korea-2026/01.tracker/ +``` + +- [ ] **Step 6: Copy map.md and stats.md** + +```bash +cp user/pages/03.map/map.md user/pages/01.trips/japan-korea-2026/02.map/map.md +cp user/pages/04.stats/stats.md user/pages/01.trips/japan-korea-2026/03.stats/stats.md +``` + +- [ ] **Step 7: Write stories stub** + +`user/pages/01.trips/japan-korea-2026/04.stories/stories.md`: +```yaml +--- +title: Stories +template: stories +published: true +--- +``` + +- [ ] **Step 8: Delete old top-level pages** + +```bash +rm -rf user/pages/01.tracker user/pages/03.map user/pages/04.stats +``` + +- [ ] **Step 9: Add `active_trip` to site.yaml** + +Add to `user/config/site.yaml`: +```yaml +active_trip: japan-korea-2026 +``` + +- [ ] **Step 10: Whitelist GPX in media.yaml** + +`user/config/media.yaml` (create if absent): +```yaml +gpx: + type: file + extensions: ['gpx'] + mime: application/gpx+xml +``` + +- [ ] **Step 11: Verify pages load at new URLs** + +```bash +curl -s -o /dev/null -w "%{http_code}" http://localhost:8081/trips/japan-korea-2026/tracker +# Expected: 200 +curl -s -o /dev/null -w "%{http_code}" http://localhost:8081/trips/japan-korea-2026/map +# Expected: 200 +curl -s -o /dev/null -w "%{http_code}" http://localhost:8081/trips/japan-korea-2026/stats +# Expected: 200 +``` + +- [ ] **Step 12: Commit** + +```bash +git -C user add pages/01.trips config/site.yaml config/media.yaml +git -C user rm -r --cached pages/01.tracker pages/03.map pages/04.stats +git -C user commit -m "feat: restructure pages under trips/japan-korea-2026 entity" +``` + +--- + +### Task 2: Update templates for trip-relative paths + new trip/trips/stories templates + +**Files:** +- Modify: `user/themes/intotheeast/templates/map.html.twig` — change hardcoded `/tracker` path +- Modify: `user/themes/intotheeast/templates/stats.html.twig` — same +- Modify: `user/themes/intotheeast/templates/partials/base.html.twig` — nav uses `active_trip` +- Create: `user/themes/intotheeast/templates/trip.html.twig` +- Create: `user/themes/intotheeast/templates/trips.html.twig` +- Create: `user/themes/intotheeast/templates/stories.html.twig` + +**Interfaces:** +- Consumes: `config.site.active_trip` from site.yaml (set in Task 1) +- Produces: map/stats find entries via `page.parent().route ~ '/tracker'` + +- [ ] **Step 1: Fix `map.html.twig` — tracker path** + +Replace: +```twig +{% set tracker_page = grav.pages.find('/tracker') %} +{% set all_entries = tracker_page ? tracker_page.children.published() : [] %} +``` +With: +```twig +{% set tracker_page = grav.pages.find(page.parent().route ~ '/tracker') %} +{% set all_entries = tracker_page ? tracker_page.children.published() : [] %} +``` + +- [ ] **Step 2: Fix `stats.html.twig` — tracker path** + +Same replacement as Step 1 (identical pattern in stats.html.twig). + +- [ ] **Step 3: Update `base.html.twig` nav** + +Replace hardcoded nav href values with `active_trip`-driven paths. The pattern in base.html.twig currently sets hrefs to `/tracker`, `/map`, `/stats`. Replace with: + +```twig +{% set active_trip = config.site.active_trip %} +{% set trip_base = '/trips/' ~ active_trip %} +``` + +Nav links become: +- Journal: `{{ trip_base }}/tracker` +- Map: `{{ trip_base }}/map` +- Stats: `{{ trip_base }}/stats` + +Active state detection: replace `page.url starts with '/tracker'` checks with `page.url starts with trip_base ~ '/tracker'` (and similarly for map/stats). + +- [ ] **Step 4: Create `trip.html.twig`** + +`user/themes/intotheeast/templates/trip.html.twig`: +```twig +{% extends 'partials/base.html.twig' %} + +{% block content %} +{% set tracker_page = grav.pages.find(page.route ~ '/tracker') %} +{% set entries = tracker_page ? tracker_page.children.published() : [] %} + +
+

{{ page.title }}

+ {% if page.header.date_start %} +

+ {{ page.header.date_start|date('d M Y') }} + {% if page.header.date_end %} — {{ page.header.date_end|date('d M Y') }}{% endif %} +

+ {% endif %} +
+ + + +{% if entries|length > 0 %} +
+

Recent entries

+ {% for entry in entries|slice(0, 3) %} + + {{ entry.date|date('d M Y') }} + {{ entry.title }} + {% if entry.header.location_city %} · {{ entry.header.location_city }}{% endif %} + + {% endfor %} +
+{% endif %} +{% endblock %} +``` + +- [ ] **Step 5: Create `trips.html.twig`** + +`user/themes/intotheeast/templates/trips.html.twig`: +```twig +{% extends 'partials/base.html.twig' %} + +{% block content %} +

{{ page.title }}

+{% set trips = page.children.published() %} +{% if trips|length == 0 %} +

No trips yet.

+{% else %} + +{% endif %} +{% endblock %} +``` + +- [ ] **Step 6: Create `stories.html.twig` stub** + +`user/themes/intotheeast/templates/stories.html.twig`: +```twig +{% extends 'partials/base.html.twig' %} + +{% block content %} +

{{ page.title }}

+

Stories coming soon.

+{% endblock %} +``` + +- [ ] **Step 7: Verify templates render** + +```bash +curl -s -o /dev/null -w "%{http_code}" http://localhost:8081/trips/japan-korea-2026 +# Expected: 200 +curl -s -o /dev/null -w "%{http_code}" http://localhost:8081/trips +# Expected: 200 +curl -s -o /dev/null -w "%{http_code}" http://localhost:8081/trips/japan-korea-2026/stories +# Expected: 200 +``` + +Check nav links resolve correctly on tracker/map/stats pages. + +- [ ] **Step 8: Commit** + +```bash +git -C user add themes/intotheeast/templates/ +git -C user commit -m "feat: add trip/trips/stories templates, update nav and map/stats to use trip-relative paths" +``` + +--- + +### Task 3: Add GPX route support to map template + +**Files:** +- Modify: `user/themes/intotheeast/templates/map.html.twig` + +**Interfaces:** +- Consumes: `*.gpx` files uploaded as media to the trip page (`page.parent()`) +- Produces: GPX tracks rendered as colored polylines on the Leaflet map, underneath entry pins + +- [ ] **Step 1: Add leaflet-gpx script tag** + +In `map.html.twig`, after the existing Leaflet script tag, add: +```html + +``` + +- [ ] **Step 2: Collect GPX URLs from trip media** + +After the `{% set trip_page = page.parent() %}` line (add this at the top of the template, alongside the tracker_page lookup), add: + +```twig +{% set gpx_urls = [] %} +{% for name, media in trip_page.media.all %} + {% if name|split('.')|last == 'gpx' %} + {% set gpx_urls = gpx_urls|merge([media.url]) %} + {% endif %} +{% endfor %} +``` + +- [ ] **Step 3: Pass GPX URLs to JavaScript** + +In the `