# Homepage Redesign Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Context-aware homepage with a persistent two-column map+feed layout: active-trip mode shows the live feed + GPX on the home map; between-trips mode shows a curated highlights grid from all trips with markers-only on the map. **Architecture:** Single `home.html.twig` with a `{% if config.site.travelling %}` branch. Active trip branch keeps the existing feed and adds GPX loading to the home map. Between-trips branch selects one random `featured:true` entry per trip (max 6), renders a highlight card grid, and passes coordinates to the map (no GPX, no journey line). Three blueprint files expose new data fields. A site-config blueprint exposes the mode switch and active-trip selector in Admin2. **Tech Stack:** Grav CMS 2.0 (PHP/Twig), MapLibre GL v4, toGeoJSON CDN, Playwright (Node.js) ## Global Constraints - All `user/` file changes committed via `git -C user` from the project root (or `git` from within `user/`) - Test files in `tests/ui/` and `scripts/` committed via plain `git` from the project root - No new Grav plugins - No JS build pipeline — plain CSS and vanilla JS only - `config.site.active_trip` stores a **full page route**: `/trips/italy-2026-demo` (not a bare slug) - `config.site.travelling` is `true` for active-trip mode, `false` for between-trips mode - `entry.header.featured` and `story.header.featured` (bool) gate highlight eligibility — no type-based auto-include; both stories and journal entries use the same flag - `trip.header.tagline` (string) is the trip description shown on highlight cards - Dev server at `http://localhost:8081` must be running (`make start`) for all Playwright tests - Demo data must be loaded (`make demo-load`) before running Playwright tests - Playwright test IDs continue sequentially: next map test is M8; next home tests are H2–H5 --- ### Task 1: Blueprints, config, and demo seed data **Files:** - Create: `user/blueprints/config/site.yaml` - Modify: `user/themes/intotheeast/blueprints/trip.yaml` — add `tagline` field in Trip tab - Modify: `user/themes/intotheeast/blueprints/entry.yaml` — add `featured` toggle in Entry tab - Modify: `user/themes/intotheeast/blueprints/story.yaml` — add `featured` toggle in Publishing tab - Modify: `user/config/site.yaml` — change `active_trip` to full route, add `travelling: true` - Modify: `user/pages/01.trips/italy-2026-demo/04.stories/val-dorcia-at-dawn/story.md` — add `featured: true` - Modify: first journal entry under `user/pages/01.trips/italy-2026-demo/01.dailies/` — add `featured: true` - Modify: matching demo source files in `user/docs/demo/trips/italy-2026-demo/` — mirror the `featured: true` additions **Interfaces:** - Produces: `config.site.travelling` (bool) — read in Twig as `config.site.travelling` - Produces: `config.site.active_trip` (string, full route) — used in Task 2 path lookups - Produces: `entry.header.featured` / `story.header.featured` (bool) — used in Task 3 selection logic - Produces: `trip.header.tagline` (string) — used in Task 3 card rendering - [ ] **Step 1: Record baseline test result** ```bash make test ``` Expected: 13 passed, 1 failed (`parent set to /trips/japan-korea-2026/dailies` — pre-existing). Record this so you can verify nothing changed after your edits. - [ ] **Step 2: Create `user/blueprints/config/site.yaml`** ```yaml form: validation: loose fields: active_trip: type: pages label: Active Trip start_route: '/trips' show_root: false show_slug: true travelling: type: toggle label: Currently Travelling highlight: 1 default: false options: 1: 'Yes' 0: 'No' validate: type: bool ``` Note: `type: pages` is confirmed present in Admin2's JS bundle but untested in a site config blueprint. If it fails to render in Admin2, fall back to `type: select` with explicit `options:` entries — one per trip slug — and no other code changes are needed. - [ ] **Step 3: Add `tagline` to `user/themes/intotheeast/blueprints/trip.yaml`** In the `trip` tab's `fields` block, after `header.album_url`, add: ```yaml header.tagline: type: text label: Tagline placeholder: '6 weeks from Venice to Sicily by train' help: 'Short description shown on homepage highlight cards' ``` - [ ] **Step 4: Add `featured` toggle to `user/themes/intotheeast/blueprints/entry.yaml`** In the `entry` tab's `fields` block, after `header.force_connect`, add: ```yaml header.featured: type: toggle label: Featured highlight help: 'Show as a homepage highlight when not travelling' highlight: 1 default: 0 options: 1: 'Yes' 0: 'No' validate: type: bool ``` - [ ] **Step 5: Add `featured` toggle to `user/themes/intotheeast/blueprints/story.yaml`** In the `publishing` tab's `fields` block, after `header.published`, add: ```yaml header.featured: type: toggle label: Featured highlight help: 'Show as a homepage highlight when not travelling' highlight: 1 default: 0 options: 1: 'Yes' 0: 'No' validate: type: bool ``` - [ ] **Step 6: Update `user/config/site.yaml`** Replace the file contents with: ```yaml title: 'Into the East' description: 'A travel blog by Mischa' author: name: Mischa email: mischa@gorinskat.nl taxonomies: [category, tag] metadata: description: 'Into the East — travel journal' active_trip: /trips/italy-2026-demo travelling: true ``` - [ ] **Step 7: Mark the demo story as featured** Open `user/pages/01.trips/italy-2026-demo/04.stories/val-dorcia-at-dawn/story.md` and add `featured: true` to its YAML frontmatter block. For example, if the existing frontmatter ends with `published: true`, add the line after it: ```yaml featured: true ``` - [ ] **Step 8: Mark one demo journal entry as featured** Find the first entry folder: ```bash ls user/pages/01.trips/italy-2026-demo/01.dailies/ | head -1 ``` Open `user/pages/01.trips/italy-2026-demo/01.dailies//entry.md` and add `featured: true` to its YAML frontmatter. Ensure the entry has `lat` and `lng` set — if the first entry doesn't, pick the first one that does (check with `grep -l "^lat:" user/pages/01.trips/italy-2026-demo/01.dailies/*/entry.md | head -1`). - [ ] **Step 9: Mirror featured flags to demo source** The demo source lives in `user/docs/demo/trips/italy-2026-demo/`. Apply the same `featured: true` additions to: - `user/docs/demo/trips/italy-2026-demo/stories/val-dorcia-at-dawn/story.md` - The matching journal entry in `user/docs/demo/trips/italy-2026-demo/dailies//entry.md` This ensures featured flags survive `make demo-reset`. - [ ] **Step 10: Verify tests unchanged** ```bash make test ``` Expected: identical to Step 1 (13 passed, 1 pre-existing failure). These are YAML-only changes — no template or script changed. - [ ] **Step 11: Commit** ```bash git -C user add blueprints/config/site.yaml \ themes/intotheeast/blueprints/trip.yaml \ themes/intotheeast/blueprints/entry.yaml \ themes/intotheeast/blueprints/story.yaml \ config/site.yaml \ pages/01.trips/italy-2026-demo/04.stories/val-dorcia-at-dawn/story.md \ docs/demo/trips/italy-2026-demo/stories/val-dorcia-at-dawn/story.md git -C user commit -m "feat: add blueprints for active_trip/travelling config, tagline, featured fields" ``` Then commit the journal entry (substitute the actual slug discovered in Step 8): ```bash git -C user add pages/01.trips/italy-2026-demo/01.dailies//entry.md \ docs/demo/trips/italy-2026-demo/dailies//entry.md git -C user commit -m "chore: mark demo entries as featured for homepage highlight testing" ``` --- ### Task 2: Active trip mode — route-based lookup + GPX on home map **Files:** - Modify: `user/themes/intotheeast/templates/home.html.twig` — full replacement - Modify: `tests/ui/maps.spec.js` — add M8 **Interfaces:** - Consumes: `config.site.travelling` (bool) — Task 1 - Consumes: `config.site.active_trip` (full route string) — Task 1 - Produces: `window.homeMap` global (already existed — now with GPX sources `home-gpx-0` … and `home-journey`) - Produces: `{% else %}` placeholder in template for Task 3 to fill - [ ] **Step 1: Write failing test M8** Add to `tests/ui/maps.spec.js`: ```js // ── M8: Home map has GPX journey source on active trip ──────────────────────── test('M8: home map has a journey source after GPX settles (active trip)', async ({ page }) => { // Requires travelling: true in user/config/site.yaml (set in Task 1). // Requires GPX files attached to the active trip (italy-2026-demo has 7). const errors = []; page.on('pageerror', e => errors.push(e.message)); await page.goto('/'); await expect(page.locator('#home-map canvas.maplibregl-canvas')).toBeVisible({ timeout: 10000 }); await expect(page.locator('#home-map .maplibregl-marker').first()).toBeVisible({ timeout: 15000 }); await page.waitForFunction(function () { return window.homeMap && (window.homeMap.getSource('home-journey') !== undefined || window.homeMap.getSource('home-gpx-0') !== undefined); }, { timeout: 20000 }); const hasSource = await page.evaluate(function () { return !!(window.homeMap.getSource('home-journey') || window.homeMap.getSource('home-gpx-0')); }); expect(hasSource, 'Home map has a journey or GPX source').toBe(true); expect(errors, 'No JS errors on home page').toHaveLength(0); }); ``` Run to confirm it fails: ```bash npx playwright test tests/ui/maps.spec.js --grep "M8" ``` Expected: FAIL (GPX sources not yet added to home map). - [ ] **Step 2: Replace `home.html.twig` with the active-trip-mode version** Replace the entire file with: ```twig {% extends 'partials/base.html.twig' %} {% block content %} {% set trip_route = config.site.active_trip %} {% set trip = grav.pages.find(trip_route) %} {% if config.site.travelling %} {# ══════════════════════════════════════════════════════════ ACTIVE TRIP MODE #} {% set dailies_page = grav.pages.find(trip_route ~ '/dailies') %} {% set stories_page = grav.pages.find(trip_route ~ '/stories') %} {% set journal_entries = dailies_page ? dailies_page.children.published() : [] %} {% set story_entries = stories_page ? stories_page.children.published() : [] %} {% set all_items = [] %} {% for e in journal_entries %} {% set all_items = all_items|merge([{'type': 'journal', 'page': e, 'date': e.header.date}]) %} {% endfor %} {% for s in story_entries %} {% set all_items = all_items|merge([{'type': 'story', 'page': s, 'date': s.header.date}]) %} {% endfor %} {% set all_items = all_items|sort_by_key('date', 3) %} {% set journal_count = journal_entries|length %} {% set story_count = story_entries|length %} {% set map_entries = [] %} {% for item in all_items %} {% if item.type == 'journal' and item.page.header.lat is not empty and item.page.header.lng is not empty %} {% set map_entries = map_entries|merge([{ 'lat': item.page.header.lat|number_format(6, '.', ''), 'lng': item.page.header.lng|number_format(6, '.', ''), 'slug': item.page.slug, 'title': item.page.title, 'url': item.page.url, 'force_connect': item.page.header.force_connect ? true : false }]) %} {% endif %} {% endfor %} {% set home_gpx_urls = [] %} {% if trip %} {% for name, media in trip.media.all %} {% if name|split('.')|last == 'gpx' %} {% set home_gpx_urls = home_gpx_urls|merge([trip.url ~ '/' ~ name]) %} {% endif %} {% endfor %} {% endif %}

{{ trip ? trip.title : trip_route }}

{{ journal_count }} journal {{ journal_count == 1 ? 'entry' : 'entries' }} {% if story_count > 0 %} · {{ story_count }} {{ story_count == 1 ? 'story' : 'stories' }}{% endif %}
{% if all_items|length > 0 %} {% for item in all_items %} {% set entry = item.page %} {% if item.type == 'journal' %} {% set weather_icons = { 'Sunny': '☀️', 'Partly cloudy': '⛅', 'Cloudy': '☁️', 'Foggy': '🌫️', 'Drizzle': '🌦️', 'Rain': '🌧️', 'Snow': '❄️', 'Thunderstorm': '⛈️' } %}

{{ entry.title }}

{% set images = entry.media.images %} {% if images|length > 0 %}
{% for img in images %}
{{ entry.title }}
{% endfor %}
{% if images|length > 1 %} {% endif %} {% endif %}
{{ entry.content|raw }}
{% else %} {% set hero = null %} {% if entry.header.hero_image and entry.media[entry.header.hero_image] is defined %} {% set hero = entry.media[entry.header.hero_image] %} {% elseif entry.media.images|length > 0 %} {% set hero = entry.media.images|first %} {% endif %} {% if hero %}
{{ entry.title }}
{% endif %}
✦ Story

{{ entry.title }}

{% endif %} {% endfor %} {% else %}

No entries yet. The journey is about to begin.

{% endif %}
{% if map_entries|length > 0 %} {% if home_gpx_urls|length > 0 %} {% endif %} {% endif %} {% else %} {# ════════════════════════════════════════════════ BETWEEN-TRIPS MODE (Task 3) #}

Off season — highlights coming in Task 3.

{% endif %} {% endblock %} ``` - [ ] **Step 3: Run M8 test** ```bash npx playwright test tests/ui/maps.spec.js --grep "M8" ``` Expected: PASS. The home map now loads GPX URLs, adds `home-gpx-N` layer sources, and replaces the simple `home-journey` source with connector-suppressed segments. - [ ] **Step 4: Run existing home + map tests** ```bash npx playwright test tests/ui/maps.spec.js tests/ui/home.spec.js ``` Expected: M4 and H1 still pass; M8 passes. - [ ] **Step 5: Commit** ```bash git -C user add themes/intotheeast/templates/home.html.twig git -C user commit -m "feat: add travelling branch and GPX to home map (active trip mode)" git add tests/ui/maps.spec.js git commit -m "test(maps): add M8 — home map GPX source on active trip" ``` --- ### Task 3: Between-trips highlights mode + CSS + Playwright tests **Files:** - Modify: `user/themes/intotheeast/templates/home.html.twig` — replace `{% else %}` placeholder with full highlights branch - Modify: `user/themes/intotheeast/css/style.css` — append highlight card and grid styles - Create: `tests/ui/home-highlights.spec.js` **Interfaces:** - Consumes: `config.site.travelling` (bool) — Task 1 - Consumes: `entry.header.featured` / `story.header.featured` (bool) — Task 1 - Consumes: `trip.header.tagline` (string) — Task 1 - Produces: `.home-highlights-grid` — the grid container, used in Playwright selectors - Produces: `.home-highlight-card[id="highlight-"]` — per-card IDs for map marker scroll-to - Produces: `.home-highlights-cta` — CTA link to `/trips` - Produces: `window.homeMap` global in between-trips mode (same name, separate branch) - [ ] **Step 1: Write failing tests** Create `tests/ui/home-highlights.spec.js`: ```js // @ts-check // Tests: H2–H5 — Between-trips highlights mode // These tests temporarily set travelling: false in user/config/site.yaml, // run the assertions, then restore the original value. // Requires demo data with featured entries: run `make demo-load` first. const { test, expect } = require('@playwright/test'); const fs = require('fs'); const path = require('path'); const SITE_YAML_PATH = path.join(__dirname, '../../user/config/site.yaml'); test.describe('Between-trips highlights mode', () => { let originalSiteYaml; test.beforeAll(async () => { originalSiteYaml = fs.readFileSync(SITE_YAML_PATH, 'utf8'); const patched = originalSiteYaml.replace(/^travelling:\s*true/m, 'travelling: false'); fs.writeFileSync(SITE_YAML_PATH, patched); // Brief pause for Grav to re-read config on next request await new Promise(r => setTimeout(r, 400)); }); test.afterAll(async () => { fs.writeFileSync(SITE_YAML_PATH, originalSiteYaml); }); // ── H2: Highlights grid is visible ────────────────────────────────────────── test('H2: homepage shows highlights grid when not travelling', async ({ page }) => { await page.goto('/'); await expect(page.locator('.home-highlights-grid')).toBeVisible({ timeout: 10000 }); }); // ── H3: Highlight cards contain trip link ──────────────────────────────────── test('H3: highlight cards have a View-trip link', async ({ page }) => { await page.goto('/'); await expect(page.locator('.home-highlight-card').first()).toBeVisible({ timeout: 10000 }); await expect(page.locator('.home-highlight-trip-link').first()).toBeVisible(); }); // ── H4: Between-trips home map renders without JS errors ──────────────────── test('H4: home map renders in between-trips mode without JS errors', async ({ page }) => { const errors = []; page.on('pageerror', e => errors.push(e.message)); await page.goto('/'); await expect(page.locator('#home-map canvas.maplibregl-canvas')).toBeVisible({ timeout: 10000 }); expect(errors, 'No JS errors').toHaveLength(0); }); // ── H5: CTA links to /trips ────────────────────────────────────────────────── test('H5: "Explore all past trips" CTA links to /trips', async ({ page }) => { await page.goto('/'); const cta = page.locator('.home-highlights-cta'); await expect(cta).toBeVisible({ timeout: 10000 }); await expect(cta).toHaveAttribute('href', /\/trips/); }); }); ``` Run to confirm they fail: ```bash npx playwright test tests/ui/home-highlights.spec.js ``` Expected: H2–H5 all FAIL (`.home-highlights-grid` not present). - [ ] **Step 2: Append highlight CSS to `user/themes/intotheeast/css/style.css`** Append to the end of `style.css`: ```css /* ── Between-trips highlights grid ──────────────────────────────────────────── */ .home-highlights-header { margin-bottom: var(--space-8); padding-bottom: var(--space-6); border-bottom: 1px solid var(--color-border); } .home-highlights-title { font-family: var(--font-display); font-size: var(--text-2xl); font-weight: 400; color: var(--color-ink); margin-bottom: var(--space-2); } .home-highlights-subtitle { font-size: var(--text-sm); color: var(--color-ink-muted); } .home-highlights-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--space-6); margin-bottom: var(--space-10); } @media (max-width: 900px) { .home-highlights-grid { grid-template-columns: repeat(2, 1fr); } } @media (max-width: 600px) { .home-highlights-grid { grid-template-columns: 1fr; } } .home-highlight-card { border: 1px solid var(--color-border); border-radius: var(--radius-md); background: var(--color-canvas); overflow: hidden; display: flex; flex-direction: column; } .home-highlight-image { aspect-ratio: 16 / 9; overflow: hidden; } .home-highlight-image img { width: 100%; height: 100%; object-fit: cover; display: block; } .home-highlight-body { padding: var(--space-4); display: flex; flex-direction: column; gap: var(--space-2); flex: 1; } .home-highlight-badge { font-size: var(--text-xs); font-weight: 600; font-variant: small-caps; letter-spacing: 0.08em; color: var(--color-accent); } .home-highlight-badge--journal { color: var(--color-ink-muted); } .home-highlight-title { font-family: var(--font-display); font-size: var(--text-lg); font-weight: 400; color: var(--color-ink); text-decoration: none; line-height: 1.3; } .home-highlight-title:hover { color: var(--color-accent); } .home-highlight-trip { margin-top: auto; padding-top: var(--space-3); border-top: 1px solid var(--color-border); font-size: var(--text-xs); color: var(--color-ink-muted); display: flex; flex-direction: column; gap: var(--space-1); } .home-highlight-trip-name { font-weight: 600; color: var(--color-ink-2); } .home-highlight-tagline { font-style: italic; } .home-highlight-trip-link { color: var(--color-accent); text-decoration: none; font-weight: 500; } .home-highlight-trip-link:hover { text-decoration: underline; } .home-highlights-cta-wrap { text-align: center; padding-top: var(--space-4); border-top: 1px solid var(--color-border); } .home-highlights-cta { display: inline-block; color: var(--color-accent); font-size: var(--text-sm); font-weight: 500; text-decoration: none; padding: var(--space-3) var(--space-6); border: 1px solid var(--color-accent); border-radius: var(--radius-sm); } .home-highlights-cta:hover { background: var(--color-accent); color: var(--color-canvas); } ``` - [ ] **Step 3: Replace the placeholder `{% else %}` branch in `home.html.twig`** Find this exact block (added in Task 2): ```twig {% else %} {# ════════════════════════════════════════════════ BETWEEN-TRIPS MODE (Task 3) #}

Off season — highlights coming in Task 3.

{% endif %} ``` Replace it with: ```twig {% else %} {# ══════════════════════════════════════════════════════ BETWEEN-TRIPS MODE #} {# ── Highlight selection ─────────────────────────────────────────────────── #} {% set trips_page = grav.pages.find('/trips') %} {% set pool = [] %} {% if trips_page %} {% for trip_item in trips_page.children.published() %} {% set t_dailies = grav.pages.find(trip_item.route ~ '/dailies') %} {% set t_stories = grav.pages.find(trip_item.route ~ '/stories') %} {% set candidates = [] %} {% if t_dailies %} {% for e in t_dailies.children.published() %} {% if e.header.featured %} {% set candidates = candidates|merge([{'type': 'journal', 'page': e, 'trip': trip_item}]) %} {% endif %} {% endfor %} {% endif %} {% if t_stories %} {% for s in t_stories.children.published() %} {% if s.header.featured %} {% set candidates = candidates|merge([{'type': 'story', 'page': s, 'trip': trip_item}]) %} {% endif %} {% endfor %} {% endif %} {% if candidates|length > 0 %} {% set pool = pool|merge([random(candidates)]) %} {% endif %} {% endfor %} {% endif %} {% set pool = pool|shuffle %} {% set highlights = pool|slice(0, 6) %} {# ── Map entries (entries with coordinates) ──────────────────────────────── #} {% set highlights_map_entries = [] %} {% for item in highlights %} {% if item.page.header.lat is not empty and item.page.header.lng is not empty %} {% set highlights_map_entries = highlights_map_entries|merge([{ 'lat': item.page.header.lat|number_format(6, '.', ''), 'lng': item.page.header.lng|number_format(6, '.', ''), 'slug': item.page.slug, 'title': item.page.title, 'url': item.page.url }]) %} {% endif %} {% endfor %}

Into the East

A few moments from past journeys

{% if highlights|length > 0 %}
{% for item in highlights %} {% set entry = item.page %} {% set hero = null %} {% if entry.header.hero_image and entry.media[entry.header.hero_image] is defined %} {% set hero = entry.media[entry.header.hero_image] %} {% elseif entry.media.images|length > 0 %} {% set hero = entry.media.images|first %} {% endif %}
{% if hero %}
{{ entry.title }}
{% endif %}
{% if item.type == 'story' %} ✦ Story {% else %} ▸ Journal {% endif %} {{ entry.title }}
{{ item.trip.title }} {% if item.trip.header.tagline %} {{ item.trip.header.tagline }} {% endif %} → View trip
{% endfor %}
{% else %}

No highlights yet — mark entries as featured to show them here.

{% endif %}
{% if highlights_map_entries|length > 0 %} {% endif %} {% endif %} {% endblock %} ``` - [ ] **Step 4: Run H2–H5 tests** ```bash npx playwright test tests/ui/home-highlights.spec.js ``` Expected: H2, H3, H4, H5 all PASS. - [ ] **Step 5: Verify no regressions** ```bash make test npx playwright test tests/ui/ ``` Expected: `make test` same as baseline (13 passed, 1 pre-existing failure). All Playwright tests pass — H1 and M4 use `travelling: true`; H2–H5 temporarily flip to `false` and restore it. - [ ] **Step 6: Commit** ```bash git -C user add themes/intotheeast/templates/home.html.twig \ themes/intotheeast/css/style.css git -C user commit -m "feat: add between-trips highlights mode with grid and map markers" git add tests/ui/home-highlights.spec.js git commit -m "test(home): add H2–H5 between-trips highlights Playwright tests" ```