# Inline Journal Feed 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. **Status:** ✅ Complete (2026-06-20) **Goal:** Replace click-through journal entry cards with fully inline posts (photo strip + full text) across the trip page, dailies page, and home page. **Architecture:** Each journal entry becomes an `
` block that renders all its images in a CSS scroll-snap strip with dot indicators, followed by the full body text. The `id`, `data-type`, `data-lat`, `data-lng` attributes stay on the root so map targeting, filter JS, and flash animation continue to work. Story cards in all three feeds are unchanged. **Tech Stack:** Grav 2.0 Twig templates, CSS scroll-snap (no library), vanilla JS IntersectionObserver-free dot sync via scroll event, Playwright tests ## Global Constraints - All CSS values must use design tokens (`var(--...)`) — no hard-coded colours, sizes, or radii - `id="entry-{{ entry.slug }}"` must remain on the journal post root (map scroll targeting) - `data-type="journal"` must remain on the journal post root (filter bar JS) - `data-lat` and `data-lng` must remain on the journal post root (map marker rendering) - Story cards (``) are not touched by any task - Two git repos: user content at `/home/mischa/Projects/travel-blog-intotheeast/user/` (separate git repo); outer repo at `/home/mischa/Nextcloud/Projects/travel-blog-intotheeast/`. Templates and CSS commit to the user subrepo; tests commit to the outer repo. Always update the outer repo's `user` submodule pointer in the same commit as the test changes. - Dev server: http://localhost:8081 --- ## File Map | File | Change | |---|---| | `user/themes/intotheeast/css/style.css` | Add `.journal-post` component; remove journal-card-only rules; update `.is-highlighted` selector | | `user/themes/intotheeast/templates/partials/base.html.twig` | Add photo-strip dot-sync JS before `` | | `user/themes/intotheeast/templates/dailies.html.twig` | Replace journal card block with `.journal-post`; add `weather_icons` | | `user/themes/intotheeast/templates/trip.html.twig` | Replace journal card block with `.journal-post`; add `weather_icons` | | `user/themes/intotheeast/templates/home.html.twig` | Replace journal card block with `.journal-post`; add `weather_icons` | | `tests/ui/dailies.spec.js` | Update T1 selector; update T2 selectors | | `tests/ui/maps.spec.js` | Update M7 selector | | `tests/ui/home.spec.js` | New file — H1 test | --- ### Task 1: CSS foundation + dot-sync JS Add all new `.journal-post` CSS and the photo-strip dot-sync JS. Remove CSS classes that are only used by the old journal entry card (not by story cards). This task has no template changes — existing tests must still pass at the end. **Files:** - Modify: `user/themes/intotheeast/css/style.css` - Modify: `user/themes/intotheeast/templates/partials/base.html.twig` **Interfaces:** - Produces: `.journal-post`, `.journal-post-header`, `.journal-post-title`, `.journal-post-meta`, `.journal-post-permalink`, `.journal-post-location`, `.journal-post-weather`, `.journal-photo-strip`, `.journal-photo-slide`, `.journal-photo-dots`, `.journal-photo-dot.is-active`, `.journal-post-body`, `.journal-post.is-highlighted` — all usable by Tasks 2–4 - [x] **Step 1: Add `.journal-post` CSS block to `style.css`** In `user/themes/intotheeast/css/style.css`, find the line: ```css /* ── Single entry ────────────────────────────────────────────────────────────── */ ``` Insert the following block **before** that comment: ```css /* ── Journal post (inline feed) ─────────────────────────────────────────────── */ .journal-post { border-bottom: 1px solid var(--color-border); padding-bottom: var(--space-12); margin-bottom: var(--space-12); } .journal-post-header { margin-bottom: var(--space-4); } .journal-post-title { font-family: var(--font-display); font-size: var(--text-xl); font-weight: 400; line-height: var(--leading-snug); color: var(--color-ink); margin-bottom: var(--space-2); } .journal-post-meta { font-size: var(--text-xs); color: var(--color-ink-muted); display: flex; align-items: center; flex-wrap: wrap; gap: var(--space-2); } .journal-post-permalink { color: var(--color-ink-muted); text-decoration: none; font-weight: 700; letter-spacing: 0.07em; } .journal-post-permalink:hover { color: var(--color-accent); } .journal-post-location, .journal-post-weather { color: var(--color-ink-muted); } .journal-photo-strip { display: flex; overflow-x: scroll; scroll-snap-type: x mandatory; scrollbar-width: none; border-radius: var(--radius-md); margin-bottom: var(--space-3); } .journal-photo-strip::-webkit-scrollbar { display: none; } .journal-photo-slide { flex: 0 0 100%; scroll-snap-align: start; aspect-ratio: 3 / 2; overflow: hidden; } .journal-photo-slide img { width: 100%; height: 100%; object-fit: cover; display: block; } .journal-photo-dots { display: flex; justify-content: center; gap: var(--space-2); margin-bottom: var(--space-4); } .journal-photo-dot { width: 6px; height: 6px; border-radius: 9999px; background: var(--color-border); transition: background 0.2s; } .journal-photo-dot.is-active { background: var(--color-ink-muted); } .journal-post-body { font-size: var(--text-base); line-height: var(--leading-normal); color: var(--color-ink-2); } .journal-post-body p { margin-bottom: var(--space-4); } .journal-post-body p:last-child { margin-bottom: 0; } .journal-post.is-highlighted { animation: card-highlight 0.7s ease-out forwards; } ``` - [x] **Step 2: Remove journal-card-only CSS rules from `style.css`** These rules are only used by the old journal entry card. Story cards do not use them. Remove each block exactly as shown. **Remove `.entry-card-photo-overlay` and its children:** ```css .entry-card-photo-overlay { position: absolute; inset: auto 0 0 0; padding: var(--space-5) var(--space-4) var(--space-3); background: linear-gradient(to top, rgba(0,0,0,0.58) 0%, transparent 100%); display: flex; align-items: flex-end; gap: var(--space-3); flex-wrap: wrap; } .entry-date-overlay { font-size: var(--text-xs); font-weight: 700; letter-spacing: 0.08em; color: rgba(255,255,255,0.92); } .entry-location-overlay { font-size: var(--text-xs); color: rgba(255,255,255,0.85); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 180px; } ``` Replace with nothing (delete the block entirely). **Remove the text-only meta block and its comment:** ```css /* Card: text-only variant */ .entry-card-textmeta { display: flex; align-items: center; gap: var(--space-3); margin-bottom: var(--space-3); flex-wrap: wrap; } .entry-date-plain { font-size: var(--text-xs); font-weight: 700; letter-spacing: 0.07em; color: var(--color-ink-muted); } .entry-location-plain { font-size: var(--text-xs); color: var(--color-ink-muted); } ``` Replace with nothing. **Remove `.entry-excerpt` and `.entry-read-more`:** ```css .entry-excerpt { font-size: var(--text-base); line-height: var(--leading-normal); color: var(--color-ink-2); margin-bottom: var(--space-3); } .entry-read-more { font-size: var(--text-sm); font-weight: 500; color: var(--color-accent); } ``` Replace with nothing. **Replace `.entry-card.is-highlighted` with `.journal-post.is-highlighted`:** Find: ```css .entry-card.is-highlighted { animation: card-highlight 0.7s ease-out forwards; } ``` Replace with: ```css .journal-post.is-highlighted { animation: card-highlight 0.7s ease-out forwards; } ``` - [x] **Step 3: Add dot-sync JS to `base.html.twig`** In `user/themes/intotheeast/templates/partials/base.html.twig`, find: ```twig {{ assets.js('bottom')|raw }} ``` Replace with: ```twig {{ assets.js('bottom')|raw }} ``` - [x] **Step 4: Run existing tests to confirm nothing broke** ```bash cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast npx playwright test --project=chromium tests/ui/dailies.spec.js tests/ui/maps.spec.js tests/ui/trip-filter.spec.js tests/ui/stories.spec.js ``` Expected: all existing tests pass (M7 still passes because `trip.html.twig` has not changed yet — the JS still adds `is-highlighted` to `.entry-card` elements, and the old M7 selector `.entry-card.is-highlighted` finds the element). - [x] **Step 5: Commit user subrepo** ```bash cd /home/mischa/Projects/travel-blog-intotheeast/user git add themes/intotheeast/css/style.css themes/intotheeast/templates/partials/base.html.twig git commit -m "feat: add journal-post CSS component and dot-sync JS; remove stale journal-card-only rules" ``` --- ### Task 2: dailies.html.twig + T1/T2 test updates Replace the journal entry card in `dailies.html.twig` with the new `.journal-post` inline block. Update T1 and T2 tests to match the new structure. **Files:** - Modify: `user/themes/intotheeast/templates/dailies.html.twig` - Modify: `tests/ui/dailies.spec.js` **Interfaces:** - Consumes: `.journal-post` CSS from Task 1 - Produces: `/trips/japan-korea-2026/dailies` renders `.journal-post` blocks; T1 and T2 pass with new selectors - [x] **Step 1: Update T1 and T2 tests to their new selectors** In `tests/ui/dailies.spec.js`, make the following changes: **T1** — change `.entry-card` to `.journal-post`: ```js // OLD await expect(page.locator('.entry-card').first()).toBeVisible(); // NEW await expect(page.locator('.journal-post').first()).toBeVisible(); ``` **T2** — replace the entire card locator + index block with id-based selectors: Find: ```js // Both fixture entries must be visible on the page const newerCard = page.locator(`.entry-card[href*="${NEWER_SLUG}"]`); const olderCard = page.locator(`.entry-card[href*="${OLDER_SLUG}"]`); await expect(newerCard).toBeVisible(); await expect(olderCard).toBeVisible(); // The newer entry should appear higher in the DOM (lower index) const newerIdx = await newerCard.evaluate(el => { return [...document.querySelectorAll('.entry-card')].findIndex(c => c === el); }); const olderIdx = await olderCard.evaluate(el => { return [...document.querySelectorAll('.entry-card')].findIndex(c => c === el); }); ``` Replace with: ```js // Both fixture entries must be visible on the page const newerCard = page.locator(`#entry-${NEWER_SLUG}`); const olderCard = page.locator(`#entry-${OLDER_SLUG}`); await expect(newerCard).toBeVisible(); await expect(olderCard).toBeVisible(); // The newer entry should appear higher in the DOM (lower index) const newerIdx = await newerCard.evaluate(el => { return [...document.querySelectorAll('.journal-post')].findIndex(c => c.id === el.id); }); const olderIdx = await olderCard.evaluate(el => { return [...document.querySelectorAll('.journal-post')].findIndex(c => c.id === el.id); }); ``` - [x] **Step 2: Run T1 and T2 to verify they fail** ```bash cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast npx playwright test --project=chromium tests/ui/dailies.spec.js -g "T1:|T2:" ``` Expected: FAIL — `.journal-post` selector finds no elements (the page still renders `.entry-card`). - [x] **Step 3: Add `weather_icons` map and replace journal card in `dailies.html.twig`** In `user/themes/intotheeast/templates/dailies.html.twig`, find the line: ```twig {% if item.type == 'journal' %} ``` This `{% if item.type == 'journal' %}` block ends at `` before `{% else %}`. Replace the entire journal card block (from `{% if item.type == 'journal' %}` through the closing `` of the journal branch, leaving the `{% else %}` story branch intact) with: ```twig {% 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 }}
``` The exact text to find and replace is the old journal branch. The old branch starts with: ```twig {% if item.type == 'journal' %} {% if hero %}
{{ entry.title }}
{% if entry.header.location_city or entry.header.location_country %} 📍 {% if entry.header.location_city %}{{ entry.header.location_city|slice(0,20) }}{% endif %} {% if entry.header.location_city and entry.header.location_country %}, {% endif %} {% if entry.header.location_country %}{{ entry.header.location_country }}{% endif %} {% endif %}
{% else %}
{% if entry.header.location_city or entry.header.location_country %} {%- set _loc = [] -%} {%- if entry.header.location_city -%}{%- set _loc = _loc|merge([entry.header.location_city]) -%}{%- endif -%} {%- if entry.header.location_country -%}{%- set _loc = _loc|merge([entry.header.location_country]) -%}{%- endif -%} 📍 {{ _loc|join(', ') }} {% endif %}
{% endif %}

{{ entry.title }}

{{ entry.summary|striptags|slice(0, 250)|trim }}

Read entry →
``` - [x] **Step 4: Run T1 and T2 to verify they pass** ```bash cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast npx playwright test --project=chromium tests/ui/dailies.spec.js -g "T1:|T2:" ``` Expected: PASS. - [x] **Step 5: Run the full suite to check no regressions** ```bash npx playwright test --project=chromium tests/ui/dailies.spec.js tests/ui/maps.spec.js tests/ui/trip-filter.spec.js tests/ui/stories.spec.js ``` Expected: all pass. - [x] **Step 6: Commit** ```bash cd /home/mischa/Projects/travel-blog-intotheeast/user git add themes/intotheeast/templates/dailies.html.twig git commit -m "feat: replace journal entry card with inline journal-post in dailies feed" cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast git add tests/ui/dailies.spec.js user git commit -m "test: update T1/T2 selectors for inline journal-post structure" ``` --- ### Task 3: trip.html.twig + M7 test update Replace the journal entry card in `trip.html.twig` with the `.journal-post` block. Update M7 which currently tests `.entry-card.is-highlighted` on the trip page. **Files:** - Modify: `user/themes/intotheeast/templates/trip.html.twig` - Modify: `tests/ui/maps.spec.js` **Interfaces:** - Consumes: `.journal-post` CSS and `.journal-post.is-highlighted` from Task 1; journal-post HTML pattern from Task 2 - Produces: `/trips/japan-korea-2026` renders `.journal-post` blocks; M7 passes with `.journal-post.is-highlighted` - [x] **Step 1: Update M7 to the new selector** In `tests/ui/maps.spec.js`, find: ```js // Within 500ms of click + delay, one entry-card should have is-highlighted await expect(page.locator('.entry-card.is-highlighted')).toBeVisible({ timeout: 1500 }); ``` Replace with: ```js // Within 500ms of click + delay, one journal-post should have is-highlighted await expect(page.locator('.journal-post.is-highlighted')).toBeVisible({ timeout: 1500 }); ``` - [x] **Step 2: Run M7 to verify it fails** ```bash cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast npx playwright test --project=chromium tests/ui/maps.spec.js -g "M7:" ``` Expected: FAIL — `.journal-post.is-highlighted` not found (trip.html.twig still renders ``). - [x] **Step 3: Replace journal card in `trip.html.twig`** In `user/themes/intotheeast/templates/trip.html.twig`, find and replace the journal branch of the `{% if item.type == 'journal' %}` block. The old branch to replace is: ```twig {% if item.type == 'journal' %} {% if hero %}
{{ entry.title }}
{% if entry.header.location_city or entry.header.location_country %} 📍 {% if entry.header.location_city %}{{ entry.header.location_city|slice(0,20) }}{% endif %} {% if entry.header.location_city and entry.header.location_country %}, {% endif %} {% if entry.header.location_country %}{{ entry.header.location_country }}{% endif %} {% endif %}
{% else %}
{% if entry.header.location_city or entry.header.location_country %} {%- set _loc = [] -%} {%- if entry.header.location_city -%}{%- set _loc = _loc|merge([entry.header.location_city]) -%}{%- endif -%} {%- if entry.header.location_country -%}{%- set _loc = _loc|merge([entry.header.location_country]) -%}{%- endif -%} 📍 {{ _loc|join(', ') }} {% endif %}
{% endif %}

{{ entry.title }}

{{ entry.summary|striptags|slice(0, 250)|trim }}

Read entry →
``` Replace with: ```twig {% 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 }}
``` - [x] **Step 4: Run M7 to verify it passes** ```bash cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast npx playwright test --project=chromium tests/ui/maps.spec.js -g "M7:" ``` Expected: PASS. - [x] **Step 5: Run full suite** ```bash npx playwright test --project=chromium tests/ui/dailies.spec.js tests/ui/maps.spec.js tests/ui/trip-filter.spec.js tests/ui/stories.spec.js ``` Expected: all pass. - [x] **Step 6: Commit** ```bash cd /home/mischa/Projects/travel-blog-intotheeast/user git add themes/intotheeast/templates/trip.html.twig git commit -m "feat: replace journal entry card with inline journal-post in trip feed" cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast git add tests/ui/maps.spec.js user git commit -m "test: update M7 selector for journal-post.is-highlighted" ``` --- ### Task 4: home.html.twig + H1 test Replace the journal entry card in `home.html.twig` and add a minimal home page test. **Files:** - Modify: `user/themes/intotheeast/templates/home.html.twig` - Create: `tests/ui/home.spec.js` **Interfaces:** - Consumes: `.journal-post` CSS from Task 1; journal-post HTML pattern from Task 2 - [x] **Step 1: Write the failing H1 test** Create `tests/ui/home.spec.js`: ```js // @ts-check // Tests: H1 — home page journal feed const { test, expect } = require('@playwright/test'); // ── H1: Home page renders inline journal posts ───────────────────────────────── test('H1: home page shows at least one inline journal-post block', async ({ page }) => { await page.goto('/'); await expect(page.locator('.journal-post').first()).toBeVisible(); await expect(page.locator('.site-header')).toBeVisible(); }); ``` - [x] **Step 2: Run H1 to verify it fails** ```bash cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast npx playwright test --project=chromium tests/ui/home.spec.js ``` Expected: FAIL — `.journal-post` not found (home page still renders ``). - [x] **Step 3: Replace journal card in `home.html.twig`** In `user/themes/intotheeast/templates/home.html.twig`, find the journal branch: ```twig {% if item.type == 'journal' %} {% if hero %}
{{ entry.title }}
{% if entry.header.location_city or entry.header.location_country %} 📍 {% if entry.header.location_city %}{{ entry.header.location_city|slice(0,20) }}{% endif %} {% if entry.header.location_city and entry.header.location_country %}, {% endif %} {% if entry.header.location_country %}{{ entry.header.location_country }}{% endif %} {% endif %}
{% else %}
{% if entry.header.location_city or entry.header.location_country %} {%- set _loc = [] -%} {%- if entry.header.location_city -%}{%- set _loc = _loc|merge([entry.header.location_city]) -%}{%- endif -%} {%- if entry.header.location_country -%}{%- set _loc = _loc|merge([entry.header.location_country]) -%}{%- endif -%} 📍 {{ _loc|join(', ') }} {% endif %}
{% endif %}

{{ entry.title }}

{{ entry.summary|striptags|slice(0, 250)|trim }}

Read entry →
``` Replace with: ```twig {% 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 }}
``` Note: `home.html.twig` journal posts do **not** include `data-type` (the home page has no filter bar) — this matches the existing `` on home which also had no `data-type`. - [x] **Step 4: Run H1 to verify it passes** ```bash cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast npx playwright test --project=chromium tests/ui/home.spec.js ``` Expected: PASS. - [x] **Step 5: Run the full suite** ```bash npx playwright test --project=chromium tests/ui/dailies.spec.js tests/ui/maps.spec.js tests/ui/trip-filter.spec.js tests/ui/stories.spec.js tests/ui/home.spec.js ``` Expected: all pass. - [x] **Step 6: Commit** ```bash cd /home/mischa/Projects/travel-blog-intotheeast/user git add themes/intotheeast/templates/home.html.twig git commit -m "feat: replace journal entry card with inline journal-post on home page" cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast git add tests/ui/home.spec.js user git commit -m "test: add H1 home page journal-post test" ```