Files

16 KiB

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

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_trip to 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 /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:

{% 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.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:

{% 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.twig stub

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: *.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:

<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.mdpageconfig.parent

  • Modify: Makefiledemo-load and demo-reset paths

  • Modify: scripts/test-post.shTRACKER 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:

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.sh TRACKER 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.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:

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.yaml blueprint

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:

  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