Files
intotheeast-com/docs/superpowers/plans/2026-06-19-trip-entity.md
T

539 lines
16 KiB
Markdown

# 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/<slug>/`. 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/<slug>``/trips/japan-korea-2026/tracker/<slug>` — 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() : [] %}
<div class="trip-hero">
<h1>{{ page.title }}</h1>
{% if page.header.date_start %}
<p class="trip-dates">
{{ page.header.date_start|date('d M Y') }}
{% if page.header.date_end %}{{ page.header.date_end|date('d M Y') }}{% endif %}
</p>
{% endif %}
</div>
<nav class="trip-nav">
<a href="{{ page.route }}/tracker">Journal</a>
<a href="{{ page.route }}/map">Map</a>
<a href="{{ page.route }}/stats">Stats</a>
<a href="{{ page.route }}/stories">Stories</a>
</nav>
{% if entries|length > 0 %}
<section class="trip-recent">
<h2>Recent entries</h2>
{% for entry in entries|slice(0, 3) %}
<a href="{{ entry.url }}">
<span>{{ entry.date|date('d M Y') }}</span>
{{ entry.title }}
{% if entry.header.location_city %} · {{ entry.header.location_city }}{% endif %}
</a>
{% endfor %}
</section>
{% endif %}
{% endblock %}
```
- [ ] **Step 5: Create `trips.html.twig`**
`user/themes/intotheeast/templates/trips.html.twig`:
```twig
{% extends 'partials/base.html.twig' %}
{% block content %}
<h1>{{ page.title }}</h1>
{% set trips = page.children.published() %}
{% if trips|length == 0 %}
<p>No trips yet.</p>
{% else %}
<ul class="trips-list">
{% for trip in trips %}
<li>
<a href="{{ trip.url }}">
<strong>{{ trip.title }}</strong>
{% if trip.header.date_start %}
<span>{{ trip.header.date_start|date('d M Y') }}</span>
{% endif %}
</a>
</li>
{% endfor %}
</ul>
{% endif %}
{% endblock %}
```
- [ ] **Step 6: Create `stories.html.twig` stub**
`user/themes/intotheeast/templates/stories.html.twig`:
```twig
{% extends 'partials/base.html.twig' %}
{% block content %}
<h1>{{ page.title }}</h1>
<p>Stories coming soon.</p>
{% 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
<script src="https://cdn.jsdelivr.net/npm/leaflet-gpx@2.1.2/gpx.min.js"></script>
```
- [ ] **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 `<script>` block, after the map is initialized and before the entry markers loop, add:
```javascript
// GPX route tracks
const gpxUrls = {{ gpx_urls|json_encode|raw }};
gpxUrls.forEach(url => {
new L.GPX(url, {
async: true,
polyline_options: { color: '#1F6B5A', weight: 2, opacity: 0.7 },
marker_options: { startIconUrl: null, endIconUrl: null, shadowUrl: null }
}).addTo(map);
});
```
Disabling start/end markers keeps the map clean — the entry pins already mark key stops.
- [ ] **Step 4: Test with a sample GPX**
Create a minimal 3-point GPX file to test without a real Komoot export:
```xml
<?xml version="1.0"?>
<gpx version="1.1" creator="test">
<trk><trkseg>
<trkpt lat="35.6762" lon="139.6503"><time>2026-03-25T10:00:00Z</time></trkpt>
<trkpt lat="35.0116" lon="135.7681"><time>2026-03-27T10:00:00Z</time></trkpt>
<trkpt lat="37.5665" lon="126.9780"><time>2026-04-01T10:00:00Z</time></trkpt>
</trkseg></trk>
</gpx>
```
Upload via Grav Admin to the trip page media, then verify the map at `/trips/japan-korea-2026/map` renders the polyline. Remove the test file after verification.
- [ ] **Step 5: Verify map still works without GPX**
Confirm map renders normally when no `.gpx` files are present (gpxUrls = []).
- [ ] **Step 6: Commit**
```bash
git -C user add themes/intotheeast/templates/map.html.twig
git -C user commit -m "feat: add GPX route rendering to trip map via leaflet-gpx"
```
---
### Task 4: Update post form, Makefile, demo content, and tests
**Files:**
- Modify: `user/pages/02.post/post-form.md``pageconfig.parent`
- Modify: `Makefile``demo-load` and `demo-reset` paths
- Modify: `scripts/test-post.sh``TRACKER` variable
- Modify: `scripts/test-form-config.sh` — expected parent value
- Modify: `tests/ui/tracker.spec.js` — any hardcoded `/tracker` URL references
- Modify: `user/docs/demo/` — move demo entries to new path structure
- [ ] **Step 1: Update post form parent**
In `user/pages/02.post/post-form.md`, change:
```yaml
pageconfig:
parent: '/tracker'
```
To:
```yaml
pageconfig:
parent: '/trips/japan-korea-2026/tracker'
```
- [ ] **Step 2: Update demo content structure**
```bash
mkdir -p user/docs/demo/trips/japan-korea-2026/tracker
mv user/docs/demo/tracker/* user/docs/demo/trips/japan-korea-2026/tracker/
rmdir user/docs/demo/tracker
```
- [ ] **Step 3: Update Makefile demo targets**
In `Makefile`, update `demo-load` and `demo-reset`:
```makefile
demo-load:
cp -r user/docs/demo/trips/japan-korea-2026/tracker/. user/pages/01.trips/japan-korea-2026/01.tracker/
docker exec intotheeast_grav bash -c "cd /var/www/html && php bin/grav clearcache"
demo-reset:
@for dir in user/docs/demo/trips/japan-korea-2026/tracker/*/; do \
folder=$$(basename "$$dir"); \
rm -rf "user/pages/01.trips/japan-korea-2026/01.tracker/$$folder"; \
done
docker exec intotheeast_grav bash -c "cd /var/www/html && php bin/grav clearcache"
```
- [ ] **Step 4: Update `test-post.sh` TRACKER path**
Find the line setting `TRACKER=` in `scripts/test-post.sh` and change it to:
```bash
TRACKER="user/pages/01.trips/japan-korea-2026/01.tracker"
```
- [ ] **Step 5: Update `test-form-config.sh` expected parent**
Find the assertion that checks `parent: '/tracker'` and update to check for `parent: '/trips/japan-korea-2026/tracker'`.
- [ ] **Step 6: Check Playwright tests for hardcoded paths**
Search `tests/ui/` for any hardcoded `/tracker` URL references:
```bash
grep -rn "tracker\|/map\|/stats" tests/ui/
```
Update any that reference the old paths to use the new trip-scoped paths.
- [ ] **Step 7: Run full test suite**
```bash
make test-config && make test-post && make test-ui
```
Expected: all pass (14/14, 6/6, 25/25).
- [ ] **Step 8: Commit**
```bash
# Main repo changes (Makefile + test scripts)
git add Makefile scripts/test-post.sh scripts/test-form-config.sh tests/
git commit -m "fix: update paths for trips/japan-korea-2026 restructure"
# User repo changes
git -C user add pages/02.post/post-form.md docs/demo/
git -C user commit -m "fix: update post form parent and demo content paths for trip structure"
```
---
### Task 5: Admin blueprint for trip page type
**Files:**
- Create: `user/themes/intotheeast/blueprints/trip.yaml`
**Interfaces:**
- Produces: "Trip" tab in Grav Admin when editing the trip page, with date range and cover image fields
- [ ] **Step 1: Create `trip.yaml` blueprint**
`user/themes/intotheeast/blueprints/trip.yaml`:
```yaml
title: 'Trip'
'@extends':
type: default
context: blueprints://pages
form:
fields:
tabs:
type: tabs
active: 1
fields:
trip:
type: tab
title: Trip
fields:
header.date_start:
type: date
label: 'Start Date'
placeholder: '2026-06-17'
help: 'First day of the trip'
header.date_end:
type: date
label: 'End Date'
placeholder: ''
help: 'Leave blank if trip is ongoing'
header.cover_image:
type: text
label: 'Cover Image Filename'
placeholder: 'cover.jpg'
help: 'Used in the trips listing page'
```
- [ ] **Step 2: Verify blueprint appears in Admin**
Open Grav Admin → Pages → Trips → Japan & Korea 2026 → Edit. Confirm the "Trip" tab appears with start date, end date, cover image fields.
- [ ] **Step 3: Commit**
```bash
git -C user add themes/intotheeast/blueprints/trip.yaml
git -C user commit -m "feat: add Admin blueprint for trip page type"
```
---
## Verification
After all tasks, run end-to-end check:
1. `make test-config && make test-post && make test-ui` — all must pass
2. Navigate to `http://localhost:8081/trips/japan-korea-2026/tracker` — entries display in date order
3. Navigate to `http://localhost:8081/trips/japan-korea-2026/map` — entry pins render, GPX polyline renders if a `.gpx` file is present on the trip page
4. Navigate to `http://localhost:8081/trips/japan-korea-2026/stats` — stats compute correctly
5. Navigate to `http://localhost:8081/trips` — trip listing shows Japan & Korea 2026
6. Submit a post via `/post` — new entry appears under `/trips/japan-korea-2026/tracker`
7. Grav Admin: edit the trip page → "Trip" tab visible with date fields