16 KiB
Trip Entity Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use
superpowers:subagent-driven-developmentto 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 withgit -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) andmake test-ui(25/25) must pass after every task- No new JS framework dependencies; leaflet-gpx is 3KB vanilla JS
user/config/media.yamlmust whitelist.gpxso Grav serves it as a file- The
02.post/post-form.mdpageconfig.parentmust 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 fromuser/pages/01.tracker/tracker.md, no content change) -
Move: all
*.entry/folders fromuser/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 fromuser/pages/03.map/map.md) -
Create:
user/pages/01.trips/japan-korea-2026/03.stats/stats.md(copy fromuser/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— addactive_trip: japan-korea-2026 -
Modify (create if absent):
user/config/media.yaml— whitelist GPX -
Step 1: Verify current structure before touching anything
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
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:
---
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:
---
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
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
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:
---
title: Stories
template: stories
published: true
---
- Step 8: Delete old top-level pages
rm -rf user/pages/01.tracker user/pages/03.map user/pages/04.stats
- Step 9: Add
active_tripto site.yaml
Add to user/config/site.yaml:
active_trip: japan-korea-2026
- Step 10: Whitelist GPX in media.yaml
user/config/media.yaml (create if absent):
gpx:
type: file
extensions: ['gpx']
mime: application/gpx+xml
- Step 11: Verify pages load at new URLs
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
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/trackerpath - Modify:
user/themes/intotheeast/templates/stats.html.twig— same - Modify:
user/themes/intotheeast/templates/partials/base.html.twig— nav usesactive_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_tripfrom 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:
{% set tracker_page = grav.pages.find('/tracker') %}
{% set all_entries = tracker_page ? tracker_page.children.published() : [] %}
With:
{% 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.twignav
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:
{% 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:
{% 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:
{% 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.twigstub
user/themes/intotheeast/templates/stories.html.twig:
{% extends 'partials/base.html.twig' %}
{% block content %}
<h1>{{ page.title }}</h1>
<p>Stories coming soon.</p>
{% endblock %}
- Step 7: Verify templates render
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
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:
*.gpxfiles 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:
<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:
{% 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:
// 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 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
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-loadanddemo-resetpaths -
Modify:
scripts/test-post.sh—TRACKERvariable -
Modify:
scripts/test-form-config.sh— expected parent value -
Modify:
tests/ui/tracker.spec.js— any hardcoded/trackerURL 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:
pageconfig:
parent: '/tracker'
To:
pageconfig:
parent: '/trips/japan-korea-2026/tracker'
- Step 2: Update demo content structure
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:
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.shTRACKER path
Find the line setting TRACKER= in scripts/test-post.sh and change it to:
TRACKER="user/pages/01.trips/japan-korea-2026/01.tracker"
- Step 5: Update
test-form-config.shexpected 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:
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
make test-config && make test-post && make test-ui
Expected: all pass (14/14, 6/6, 25/25).
- Step 8: Commit
# 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.yamlblueprint
user/themes/intotheeast/blueprints/trip.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
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:
make test-config && make test-post && make test-ui— all must pass- Navigate to
http://localhost:8081/trips/japan-korea-2026/tracker— entries display in date order - Navigate to
http://localhost:8081/trips/japan-korea-2026/map— entry pins render, GPX polyline renders if a.gpxfile is present on the trip page - Navigate to
http://localhost:8081/trips/japan-korea-2026/stats— stats compute correctly - Navigate to
http://localhost:8081/trips— trip listing shows Japan & Korea 2026 - Submit a post via
/post— new entry appears under/trips/japan-korea-2026/tracker - Grav Admin: edit the trip page → "Trip" tab visible with date fields