Files
intotheeast-com/docs/working/plans/2026-06-20-inline-journal-feed.md

36 KiB
Raw Permalink Blame History

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 <article class="journal-post"> 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 (<a class="entry-card entry-card--story">) 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 </body>
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 24

  • Step 1: Add .journal-post CSS block to style.css

In user/themes/intotheeast/css/style.css, find the line:

/* ── Single entry ────────────────────────────────────────────────────────────── */

Insert the following block before that comment:

/* ── 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;
}

  • 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:

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

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

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

.entry-card.is-highlighted {
    animation: card-highlight 0.7s ease-out forwards;
}

Replace with:

.journal-post.is-highlighted {
    animation: card-highlight 0.7s ease-out forwards;
}
  • Step 3: Add dot-sync JS to base.html.twig

In user/themes/intotheeast/templates/partials/base.html.twig, find:

    {{ assets.js('bottom')|raw }}
</body>

Replace with:

    {{ assets.js('bottom')|raw }}
<script>
(function () {
    document.querySelectorAll('.journal-photo-strip').forEach(function (strip) {
        var dots = strip.nextElementSibling;
        if (!dots || !dots.classList.contains('journal-photo-dots')) return;
        var dotEls = Array.from(dots.querySelectorAll('.journal-photo-dot'));
        strip.addEventListener('scroll', function () {
            var idx = Math.round(strip.scrollLeft / strip.offsetWidth);
            dotEls.forEach(function (d, i) { d.classList.toggle('is-active', i === idx); });
        }, { passive: true });
    });
})();
</script>
</body>
  • Step 4: Run existing tests to confirm nothing broke
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).

  • Step 5: Commit user subrepo
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

  • 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:

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

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

    // 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);
    });
  • Step 2: Run T1 and T2 to verify they fail
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).

  • 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:

            {% if item.type == 'journal' %}
            <a class="entry-card" id="entry-{{ entry.slug }}" data-type="journal" data-lat="{{ entry.header.lat }}" data-lng="{{ entry.header.lng }}" href="{{ entry.url }}">

This {% if item.type == 'journal' %} block ends at </a> before {% else %}. Replace the entire journal card block (from {% if item.type == 'journal' %} through the closing </a> of the journal branch, leaving the {% else %} story branch intact) with:

            {% if item.type == 'journal' %}
            {% set weather_icons = {
                'Sunny': '☀️', 'Partly cloudy': '⛅', 'Cloudy': '☁️',
                'Foggy': '🌫️', 'Drizzle': '🌦️', 'Rain': '🌧️',
                'Snow': '❄️', 'Thunderstorm': '⛈️'
            } %}
            <article class="journal-post" id="entry-{{ entry.slug }}" data-type="journal" data-lat="{{ entry.header.lat }}" data-lng="{{ entry.header.lng }}">
                <header class="journal-post-header">
                    <h2 class="journal-post-title">{{ entry.title }}</h2>
                    <p class="journal-post-meta">
                        <a class="journal-post-permalink" href="{{ entry.url }}">
                            <time datetime="{{ entry.date|date('Y-m-d') }}">{{ entry.date|date('d M Y')|upper }}</time>
                        </a>
                        {% if entry.header.location_city or entry.header.location_country %}
                        <span class="journal-post-location">
                            · 📍
                            {%- 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(', ') }}
                        </span>
                        {% endif %}
                        {% if entry.header.weather_desc %}
                        <span class="journal-post-weather">· {{ weather_icons[entry.header.weather_desc] ?? '' }} {{ entry.header.weather_desc }}</span>
                        {% endif %}
                    </p>
                </header>

                {% set images = entry.media.images %}
                {% if images|length > 0 %}
                <div class="journal-photo-strip" data-slides="{{ images|length }}">
                    {% for img in images %}
                    <div class="journal-photo-slide">
                        <img src="{{ img.cropResize(900, 600).url }}" alt="{{ entry.title }}" loading="lazy">
                    </div>
                    {% endfor %}
                </div>
                {% if images|length > 1 %}
                <div class="journal-photo-dots" aria-hidden="true">
                    {% for img in images %}
                    <span class="journal-photo-dot{% if loop.first %} is-active{% endif %}"></span>
                    {% endfor %}
                </div>
                {% endif %}
                {% endif %}

                <div class="journal-post-body">{{ entry.content|raw }}</div>
            </article>

The exact text to find and replace is the old journal branch. The old branch starts with:

            {% if item.type == 'journal' %}
            <a class="entry-card" id="entry-{{ entry.slug }}" data-type="journal" data-lat="{{ entry.header.lat }}" data-lng="{{ entry.header.lng }}" href="{{ entry.url }}">
                    {% if hero %}
                    <div class="entry-card-photo">
                        <img src="{{ hero.cropResize(720, 405).url }}" alt="{{ entry.title }}" loading="lazy">
                        <div class="entry-card-photo-overlay">
                            <time class="entry-date-overlay" datetime="{{ entry.date|date('Y-m-d') }}">
                                {{ entry.date|date('d M Y')|upper }}
                            </time>
                            {% if entry.header.location_city or entry.header.location_country %}
                            <span class="entry-location-overlay">
                                📍
                                {% 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 %}
                            </span>
                            {% endif %}
                        </div>
                    </div>
                    {% else %}
                    <div class="entry-card-textmeta">
                        <time class="entry-date-plain" datetime="{{ entry.date|date('Y-m-d') }}">
                            {{ entry.date|date('d M Y')|upper }}
                        </time>
                        {% if entry.header.location_city or entry.header.location_country %}
                        <span class="entry-location-plain">
                            {%- 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(', ') }}
                        </span>
                        {% endif %}
                    </div>
                    {% endif %}
                    <div class="entry-card-body">
                        <h2 class="entry-title">{{ entry.title }}</h2>
                        <p class="entry-excerpt">{{ entry.summary|striptags|slice(0, 250)|trim }}</p>
                        <span class="entry-read-more">Read entry →</span>
                    </div>
            </a>
  • Step 4: Run T1 and T2 to verify they pass
cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast
npx playwright test --project=chromium tests/ui/dailies.spec.js -g "T1:|T2:"

Expected: PASS.

  • Step 5: Run the full suite to check no regressions
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.

  • Step 6: Commit
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

  • Step 1: Update M7 to the new selector

In tests/ui/maps.spec.js, find:

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

    // Within 500ms of click + delay, one journal-post should have is-highlighted
    await expect(page.locator('.journal-post.is-highlighted')).toBeVisible({ timeout: 1500 });
  • Step 2: Run M7 to verify it fails
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 <a class="entry-card">).

  • 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:

                    {% if item.type == 'journal' %}
                    <a class="entry-card" id="entry-{{ entry.slug }}" data-type="journal" data-lat="{{ entry.header.lat }}" data-lng="{{ entry.header.lng }}" href="{{ entry.url }}">
                            {% if hero %}
                            <div class="entry-card-photo">
                                <img src="{{ hero.cropResize(720, 405).url }}" alt="{{ entry.title }}" loading="lazy">
                                <div class="entry-card-photo-overlay">
                                    <time class="entry-date-overlay" datetime="{{ entry.date|date('Y-m-d') }}">
                                        {{ entry.date|date('d M Y')|upper }}
                                    </time>
                                    {% if entry.header.location_city or entry.header.location_country %}
                                    <span class="entry-location-overlay">
                                        📍
                                        {% 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 %}
                                    </span>
                                    {% endif %}
                                </div>
                            </div>
                            {% else %}
                            <div class="entry-card-textmeta">
                                <time class="entry-date-plain" datetime="{{ entry.date|date('Y-m-d') }}">
                                    {{ entry.date|date('d M Y')|upper }}
                                </time>
                                {% if entry.header.location_city or entry.header.location_country %}
                                <span class="entry-location-plain">
                                    {%- 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(', ') }}
                                </span>
                                {% endif %}
                            </div>
                            {% endif %}
                            <div class="entry-card-body">
                                <h2 class="entry-title">{{ entry.title }}</h2>
                                <p class="entry-excerpt">{{ entry.summary|striptags|slice(0, 250)|trim }}</p>
                                <span class="entry-read-more">Read entry →</span>
                            </div>
                    </a>

Replace with:

                    {% if item.type == 'journal' %}
                    {% set weather_icons = {
                        'Sunny': '☀️', 'Partly cloudy': '⛅', 'Cloudy': '☁️',
                        'Foggy': '🌫️', 'Drizzle': '🌦️', 'Rain': '🌧️',
                        'Snow': '❄️', 'Thunderstorm': '⛈️'
                    } %}
                    <article class="journal-post" id="entry-{{ entry.slug }}" data-type="journal" data-lat="{{ entry.header.lat }}" data-lng="{{ entry.header.lng }}">
                        <header class="journal-post-header">
                            <h2 class="journal-post-title">{{ entry.title }}</h2>
                            <p class="journal-post-meta">
                                <a class="journal-post-permalink" href="{{ entry.url }}">
                                    <time datetime="{{ entry.date|date('Y-m-d') }}">{{ entry.date|date('d M Y')|upper }}</time>
                                </a>
                                {% if entry.header.location_city or entry.header.location_country %}
                                <span class="journal-post-location">
                                    · 📍
                                    {%- 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(', ') }}
                                </span>
                                {% endif %}
                                {% if entry.header.weather_desc %}
                                <span class="journal-post-weather">· {{ weather_icons[entry.header.weather_desc] ?? '' }} {{ entry.header.weather_desc }}</span>
                                {% endif %}
                            </p>
                        </header>

                        {% set images = entry.media.images %}
                        {% if images|length > 0 %}
                        <div class="journal-photo-strip" data-slides="{{ images|length }}">
                            {% for img in images %}
                            <div class="journal-photo-slide">
                                <img src="{{ img.cropResize(900, 600).url }}" alt="{{ entry.title }}" loading="lazy">
                            </div>
                            {% endfor %}
                        </div>
                        {% if images|length > 1 %}
                        <div class="journal-photo-dots" aria-hidden="true">
                            {% for img in images %}
                            <span class="journal-photo-dot{% if loop.first %} is-active{% endif %}"></span>
                            {% endfor %}
                        </div>
                        {% endif %}
                        {% endif %}

                        <div class="journal-post-body">{{ entry.content|raw }}</div>
                    </article>
  • Step 4: Run M7 to verify it passes
cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast
npx playwright test --project=chromium tests/ui/maps.spec.js -g "M7:"

Expected: PASS.

  • Step 5: Run full suite
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.

  • Step 6: Commit
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

  • Step 1: Write the failing H1 test

Create tests/ui/home.spec.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();
});
  • Step 2: Run H1 to verify it fails
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 <a class="entry-card">).

  • Step 3: Replace journal card in home.html.twig

In user/themes/intotheeast/templates/home.html.twig, find the journal branch:

                    {% if item.type == 'journal' %}
                    <a class="entry-card" id="entry-{{ entry.slug }}" data-lat="{{ entry.header.lat }}" data-lng="{{ entry.header.lng }}" href="{{ entry.url }}">
                            {% if hero %}
                            <div class="entry-card-photo">
                                <img src="{{ hero.cropResize(720, 405).url }}" alt="{{ entry.title }}" loading="lazy">
                                <div class="entry-card-photo-overlay">
                                    <time class="entry-date-overlay" datetime="{{ entry.date|date('Y-m-d') }}">
                                        {{ entry.date|date('d M Y')|upper }}
                                    </time>
                                    {% if entry.header.location_city or entry.header.location_country %}
                                    <span class="entry-location-overlay">
                                        📍
                                        {% 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 %}
                                    </span>
                                    {% endif %}
                                </div>
                            </div>
                            {% else %}
                            <div class="entry-card-textmeta">
                                <time class="entry-date-plain" datetime="{{ entry.date|date('Y-m-d') }}">
                                    {{ entry.date|date('d M Y')|upper }}
                                </time>
                                {% if entry.header.location_city or entry.header.location_country %}
                                <span class="entry-location-plain">
                                    {%- 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(', ') }}
                                </span>
                                {% endif %}
                            </div>
                            {% endif %}
                            <div class="entry-card-body">
                                <h2 class="entry-title">{{ entry.title }}</h2>
                                <p class="entry-excerpt">{{ entry.summary|striptags|slice(0, 250)|trim }}</p>
                                <span class="entry-read-more">Read entry →</span>
                            </div>
                    </a>

Replace with:

                    {% if item.type == 'journal' %}
                    {% set weather_icons = {
                        'Sunny': '☀️', 'Partly cloudy': '⛅', 'Cloudy': '☁️',
                        'Foggy': '🌫️', 'Drizzle': '🌦️', 'Rain': '🌧️',
                        'Snow': '❄️', 'Thunderstorm': '⛈️'
                    } %}
                    <article class="journal-post" id="entry-{{ entry.slug }}" data-lat="{{ entry.header.lat }}" data-lng="{{ entry.header.lng }}">
                        <header class="journal-post-header">
                            <h2 class="journal-post-title">{{ entry.title }}</h2>
                            <p class="journal-post-meta">
                                <a class="journal-post-permalink" href="{{ entry.url }}">
                                    <time datetime="{{ entry.date|date('Y-m-d') }}">{{ entry.date|date('d M Y')|upper }}</time>
                                </a>
                                {% if entry.header.location_city or entry.header.location_country %}
                                <span class="journal-post-location">
                                    · 📍
                                    {%- 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(', ') }}
                                </span>
                                {% endif %}
                                {% if entry.header.weather_desc %}
                                <span class="journal-post-weather">· {{ weather_icons[entry.header.weather_desc] ?? '' }} {{ entry.header.weather_desc }}</span>
                                {% endif %}
                            </p>
                        </header>

                        {% set images = entry.media.images %}
                        {% if images|length > 0 %}
                        <div class="journal-photo-strip" data-slides="{{ images|length }}">
                            {% for img in images %}
                            <div class="journal-photo-slide">
                                <img src="{{ img.cropResize(900, 600).url }}" alt="{{ entry.title }}" loading="lazy">
                            </div>
                            {% endfor %}
                        </div>
                        {% if images|length > 1 %}
                        <div class="journal-photo-dots" aria-hidden="true">
                            {% for img in images %}
                            <span class="journal-photo-dot{% if loop.first %} is-active{% endif %}"></span>
                            {% endfor %}
                        </div>
                        {% endif %}
                        {% endif %}

                        <div class="journal-post-body">{{ entry.content|raw }}</div>
                    </article>

Note: home.html.twig journal posts do not include data-type (the home page has no filter bar) — this matches the existing <a class="entry-card"> on home which also had no data-type.

  • Step 4: Run H1 to verify it passes
cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast
npx playwright test --project=chromium tests/ui/home.spec.js

Expected: PASS.

  • Step 5: Run the full suite
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.

  • Step 6: Commit
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"