Files
intotheeast-com/docs/working/plans/2026-07-05-trip-description-and-hero.md
T
m038andClaude Opus 4.8 7ea90de12b test(trip): specs for trip one-liner, description, banner & home gating
- trips-list.spec.js: one-liner presence/absence, retina srcset, cover
  fallback, alt text (U3).
- trip-header.spec.js: HTD stacking order, expandable description, banner
  fallback, split intact (U4).
- home.spec.js: AE7 — the gated trip-page extras never leak onto the home
  route (asserted mode-independently so it can't race the home-highlights
  suite that toggles travelling in a parallel worker).

Also marks the plan complete.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDS6t8wcpbwKvvrxykVQ5K
2026-07-06 00:02:48 +02:00

28 KiB
Raw Blame History

title, type, date, topic, artifact_contract, artifact_readiness, product_contract_source, execution
title type date topic artifact_contract artifact_readiness product_contract_source execution
Trip Description, One-liner & Hero Image - Plan feat 2026-07-05 trip-description-and-hero ce-unified-plan/v1 implementation-ready ce-brainstorm code

Trip Description, One-liner & Hero Image - Plan

Status: Complete (2026-07-06)

Goal Capsule

  • Objective: Give each trip an optional one-liner and description, surface them on the trip list and trip page, and fix the low-resolution trip cover image — all editable from the admin panel.
  • Product authority: Mischa (site owner).
  • Open blockers: None. Ready for planning.

Product Contract

Summary

Add an optional one-liner and an optional description to trips, and render them where they help: the one-liner on both the trip-list cards and the trip page, the description on the trip page only. On the trip page, extend the existing in-column headerhome-trip-header in the shared trip-feed-col partial, which already shows title + dates/counts — with the one-liner (below the title) and the description, plus a thin banner image strip (~180220px) directly below the header text and above the filter bar. No new header is introduced above the map+journal split, and the split itself is unchanged. Because trip-feed-col is shared with the homepage active-trip view, these additions are gated to the trip-page caller so that view is unaffected. Fix the trip cover image so it renders sharp (larger derivative + retina srcset) and is chosen via an admin media picker, with the current auto-pick fallback retained.

Problem Frame

Trips currently carry no human-readable summary anywhere the reader sees. The trip-list cards (user/themes/intotheeast/templates/trips.html.twig) show only title, dates, and counts; the trip page (user/themes/intotheeast/templates/trip.html.twig) shows the title, dates, and counts only inside the feed column — via the shared trip-feed-col partial's home-trip-header block — and no one-liner, description, or banner image. A header.tagline field already exists in the trip blueprint but is used only on homepage highlight cards, and a markdown content field (labeled "Description" in admin) exists but is never rendered. Separately, the trip-list cover image auto-picks the first journal entry's first photo and crops it to 720×240, which looks soft — especially on high-DPI screens — and the author has no easy way to choose a better shot.

Key Decisions

  • Reuse header.tagline as the single one-liner. The existing tagline field becomes the one source for the short subtitle across all three surfaces (homepage highlight cards, trip list, trip page). Rejected a separate new field: two fields to keep in sync for one concept.
  • Reuse the markdown content field as the description. It is already editable in admin and labeled "Description"; it is simply not rendered on the trip page yet. Rejected adding a new short-text field.
  • Extend the existing in-column header; no new header above the split. The one-liner and description are added to the existing home-trip-header block (in the shared trip-feed-col partial, which already renders title + dates/counts), with a thin banner strip (~180220px) directly below the header text and above the filter bar — not a full-bleed hero, and not a separate header above the map+journal split. A slim banner plus text is forgiving of source-photo quality; the full-bleed story-style hero was rejected for pushing primary content below the fold and making the page hostage to photo quality, and a new above-split header was rejected because it would duplicate the title/dates/counts the feed column already shows. Because trip-feed-col is shared with the homepage active-trip view, the additions are gated (via a partial parameter) to the trip-page caller so that view is unchanged.
  • One-liner on the list, one-liner + description on the page. The list stays scannable (short subtitle only); the fuller description lives on the trip page.

Requirements

Trip data & admin

  • R1. A trip's one-liner is stored in the existing header.tagline field and remains editable in the admin trip form.
  • R2. A trip's description is stored in the existing markdown content field and remains editable in the admin trip form.
  • R3. The admin header.cover_image control is a media picker that lets the author select an uploaded image on the trip page, replacing the current type-in-a-filename text field. The picker selects from images uploaded to the trip page's own media; if a trip has none yet, the author uploads one there first, and the R7 auto-pick remains the fallback until a cover is chosen.
  • R4. The one-liner and description are both optional.

Trip list card

  • R5. When a trip's one-liner is set, the trip-list card displays it (between title and the dates/counts meta line); when unset, no one-liner line renders.
  • R6. The trip-list card cover image renders sharply on standard and high-DPI displays via a larger derivative (rendered at 1440×480) plus a retina srcset (720w and 1440w candidates). Sharpness depends on adequate source resolution — see Dependencies / Assumptions.
  • R7. The card cover image source is the author-selected cover_image when set; when unset, it falls back to the first journal entry's first image (current behavior).

Trip page header

  • R8. On the trip page, the existing in-column header (home-trip-header in the shared trip-feed-col partial) renders — each only when set — the one-liner (directly below the title) and the description (below the dates/counts), in addition to the title, dates, and counts it already shows.
  • R9. Directly below the header text and above the filter bar, the trip page renders a thin banner image strip (~180220px) using the same cover-image source and fallback as the list card (R7), rendered sharply per R6 as a fixed-height center-crop (no focal-point control); when no image is available, the header renders text-only with no banner strip.
  • R10. No new header is added above the map+journal split, and the map + journal two-column split is unchanged in structure and position.
  • R11. If a set cover_image no longer resolves (file deleted or moved), the trip-list card and the trip-page banner fall back to the R7 auto-pick rather than rendering a broken image.
  • R12. The one-liner, description, and banner strip are added for the trip-page caller of trip-feed-col only (via a partial parameter); the homepage active-trip view's header is unchanged.
  • R13. The one-liner is plain text (soft cap ~120 characters). The description is markdown; the header shows the first ~23 lines with the remainder collapsed behind an expand control, so the map+journal split stays above the fold by default while the full description remains readable on demand.
  • R14. The cover/banner image's alt text is the trip title.
  • R15. On narrow/mobile viewports the banner strip and header text reflow without pushing the map+journal split off-screen (e.g. reduced banner height); exact breakpoints are decided during planning.

Acceptance Examples

  • AE1. Covers R4, R5, R8. Given a trip with neither one-liner nor description set, when a reader views the trip list and the trip page, then no one-liner line and no description block render on either surface, and the in-column header still shows the title (and dates/counts if present).
  • AE2. Covers R8. Given a trip with a one-liner but no description, when a reader views the trip page, then the in-column header shows the title, one-liner, and dates/counts, and renders no description block.
  • AE3. Covers R7, R9. Given a trip with no cover_image set but at least one journal entry with an image, when a reader views the list card and the trip-page banner strip, then both show the first entry's first image (sharp per R6).
  • AE4. Covers R9. Given a trip with no cover_image and no journal-entry images, when a reader views the trip page, then the header renders text-only with no banner strip.
  • AE5. Covers R6. Given a trip cover image, when a reader views the trip-list card or the trip-page banner strip on a high-DPI (retina) display, then the larger derivative and retina srcset apply and the image renders sharply.
  • AE6. Covers R10. Given any trip, when a reader views the trip page, then the map + journal two-column split renders unchanged in structure and position, with no new header inserted above it.
  • AE7. Covers R12. Given the active trip, when a reader views the homepage active-trip view, then its in-column header is unchanged — no description block and no banner strip are added there.

Scope Boundaries

  • The full-bleed, story-style hero banner treatment for trips.
  • A new header rendered above the map+journal split (the one-liner, description, and banner extend the existing in-column header instead).
  • Any change to the map/journal two-column split (layout, columns, feed order, filter bar).
  • Any change to the homepage active-trip view's header (the trip-page additions are gated to the trip-page caller of the shared trip-feed-col partial).
  • A separate one-liner field distinct from header.tagline, or a separate description field distinct from the markdown content.
  • Showing the full description on the trip-list cards.
  • Author-adjustable crop / focal-point control for the banner (fixed center-crop only).

Dependencies / Assumptions

  • Confirmed (2026-07-05): header.tagline and the markdown content field are already present and editable in the admin trip form (trip.yaml), so R1/R2 need no new admin fields. header.cover_image is currently a plain text field.
  • Resolved (2026-07-05): the media-picker for cover_image (R3) uses Grav core's pagemediaselect field type. Confirmed present in the Admin2 v2.0.11 compiled field-type registry (app/_app/immutable/chunks/DzO1nmNX.js), where pagemediaselect, mediapicker, and filepicker all route to the same picker component. It binds to the page's own media and stores the selected filename — the same value shape header.cover_image holds today — so the trip.media[cover_image] template lookups need no change and no text-field fallback is required.
  • Grav's image derivative + srcset helpers are available in Twig for producing the larger and retina cover renditions.
  • Cover source photos are assumed ≥1440px wide. A smaller source cannot be sharpened by a larger derivative (Grav upscales), so the R6 sharpness goal depends on adequate source resolution, not just a bigger render box.
  • Before enabling description rendering, grep existing user/pages/01.trips/*/trip.md for non-empty content bodies and confirm each reads as a public description or is intentionally cleared. Verified empty across the four current trip.md files as of 2026-07-05; the check guards future/other trips.
  • Reusing one header.tagline across the homepage highlight card, the trip-list card, and the trip-page header assumes the existing per-trip tagline copy reads acceptably on all three; per-surface opt-out is out of scope. Audit current taglines before shipping.
  • Reusing the markdown content body as the description means a future long-form trip article distinct from the short summary would require splitting the field — accepted tradeoff.

Follow-up (post-implementation)

  • Backfill one-liners and descriptions for the active and past trips so the reader-facing summary goal is actually realized — the four current trip.md files have empty content bodies, so shipping the plumbing alone leaves existing trips showing title/dates only.

Sources / Research

  • user/themes/intotheeast/templates/trips.html.twig — current trip-list card markup and cropResize(720, 240) cover logic with first-entry fallback.
  • user/themes/intotheeast/templates/trip.html.twig — current trip page (map+feed via entry-map and trip-feed-col partials; no dedicated header above the split).
  • user/themes/intotheeast/templates/partials/trip-feed-col.html.twig — the shared feed-column header (home-trip-header: title, dates, counts, filter bar, panel toggles) that this plan extends with the one-liner, description, and banner; also included by home.html.twig's active-trip branch, hence the trip-page gating in R12.
  • user/themes/intotheeast/blueprints/trip.yaml — existing header.tagline (homepage-card copy) and markdown content ("Description") fields; header.cover_image as a text field.
  • user/themes/intotheeast/templates/story.html.twig — existing hero pattern (the rejected full-bleed reference).

Planning Contract

Product Contract preservation: changed — R13 (description is now expandable rather than a fixed clamp) and the cover_image picker assumption (resolved: pagemediaselect confirmed renderable in Admin2 v2.0.11, text-field fallback dropped), both per owner decision on 2026-07-05. All other Product Contract IDs unchanged.

Key Technical Decisions

  • KTD1. pagemediaselect for the cover field, no fallback. Change header.cover_image in trip.yaml from type: text to type: pagemediaselect. Confirmed renderable in Admin2 v2.0.11 (see the resolved dependency note above). Because it stores the selected filename — the value shape cover_image already holds — the existing trip.media[trip.header.cover_image] lookups in the templates are unchanged. Rejected the text-field fallback: unnecessary once the field type was verified to render.
  • KTD2. One shared cover macro, not duplicated resolution. The trip-list card and the trip-page banner need the same three-step cover resolution (author-selected → first journal entry's first image → none, per R7/R11) and the same retina rendering (R6/R14). Put both in a new macros/cover.html.twig so the two surfaces cannot drift. Rejected copy-pasting the current inline trips.html.twig logic into the partial: two copies of R7/R11 to keep in sync.
  • KTD3. Retina via two explicit cropResize derivatives + srcset, not Grav's native helper. Render a 1× and a 2× derivative with cropResize and emit an explicit srcset (e.g. 720w, 1440w for the card). This mirrors the existing working cropResize(720, 240) call and gives exact control, with no dependency on Grav's auto-srcset/derivatives config. The CSS crop (object-fit: cover, fixed aspect-ratio) is unchanged — only the derivative resolution and the srcset attribute change. Rejected Medium.derivatives(): adds a config dependency for no gain here.
  • KTD4. Gate the header extras with a partial parameter that defaults off. Add a trip_header_extras parameter to trip-feed-col.html.twig, defaulted to false. trip.html.twig passes it true; home.html.twig is left untouched, so its include ... only omits the parameter and the active-trip header renders exactly as today (satisfies R12/AE7 with zero edits to the home template). Rejected a positive flag on the home caller: more edits, more regression surface, on the branch the plan must not change.
  • KTD5. Expandable description as inline progressive enhancement. Render the 23-line preview and the full body in markup, and toggle an expanded class with a small inline <script> in the partial — the same pattern the partial already uses for initTripStats. Avoids touching js/src/main.js and the make build-assets step. Rejected a fixed CSS-only clamp: it would make the full description unreadable anywhere (owner decision). Rejected a <details>/<summary> element: harder to style the collapsed state as a clean N-line preview.

High-Level Technical Design

The trip-page in-column header (.home-trip-header), when trip_header_extras is true, stacks in this order. Everything from the filter bar down is unchanged; the home active-trip caller renders only the unshaded rows.

flowchart TB
  T["h1 title (existing)"]
  O["one-liner — header.tagline (R8, new)"]
  D["dates (existing)"]
  C["counts (existing)"]
  DESC["description — content, 2-3 line preview + expand (R8/R13, new)"]
  B["banner strip ~180-220px — cover macro (R9, new)"]
  F["filter bar (existing, unchanged)"]
  P["panel toggles (existing, unchanged)"]
  T --> O --> D --> C --> DESC --> B --> F --> P
  SPLIT["map + journal two-column split — unchanged, stays above the fold (R10/R15)"]
  P -.-> SPLIT

Assumptions & Constraints

  • Only css/style.css and the .html.twig templates are hand-edited; both are loaded directly (base.html.twig links css/style.css), so no build step is needed for this work. css-compiled/main.css is esbuild output and is not touched.
  • Banner dimensions (1× render box and mobile height) are tunable during implementation within the R9 ~180220px envelope; the plan fixes the approach, not the exact pixel values.
  • Source photos are assumed ≥1440px wide (Product Contract dependency); a smaller source cannot be sharpened by a larger derivative.

Sequencing

U1 and U2 are independent and can land first in either order. U3 and U4 both consume the U2 macro. U5 (CSS) supports U3 and U4 and should land with them for meaningful visual verification. Order: U1 → U2 → (U3, U4) → U5.


Implementation Units

U1. Cover field → pagemediaselect

  • Goal: Replace the type-in-a-filename cover control with an Admin2 media picker (R3).
  • Requirements: R3.
  • Dependencies: none.
  • Files: user/themes/intotheeast/blueprints/trip.yaml
  • Approach: Change header.cover_image from type: text to type: pagemediaselect. Keep the label, refresh the help text (pick from images uploaded to this trip page). The stored value stays a filename, so no template change is required here.
  • Patterns to follow: existing field definitions in trip.yaml; field type verified against the Admin2 v2.0.11 registry.
  • Test scenarios: Test expectation: none — admin-only blueprint config with no automated test surface. Verified manually in U-level verification: the picker renders in the Admin2 trip form, lists the page's uploaded images, and saves the chosen filename into header.cover_image.
  • Verification: In Admin2, the trip form shows a media dropdown (not a text box); selecting an image and saving writes its filename to the page header.

U2. Shared cover macro

  • Goal: Centralize cover resolution + retina rendering for reuse by the list card and the trip-page banner (R6, R7, R11, R14).
  • Requirements: R6, R7, R11, R14.
  • Dependencies: none (U1 not required — resolution reads the same header.cover_image filename regardless of how it was set).
  • Files: user/themes/intotheeast/templates/macros/cover.html.twig (new)
  • Approach: Two macros.
    • resolve(trip_page) → returns a Medium or null: if trip_page.header.cover_image is set and trip_page.media[...] resolves, return it; else look up grav.pages.find(trip_page.route ~ '/dailies'), take the first published entry's first image if present; else null. This encodes R7 (fallback) and R11 (a set-but-missing cover_image falls through to the auto-pick rather than returning a broken reference).
    • img(medium, alt, w, h) → emits <img src=cropResize(w,h).url srcset="…(w)w, …(2w)w" sizes=… alt=alt loading="lazy"> using cropResize(w, h) and cropResize(w*2, h*2) (R6, R14).
  • Patterns to follow: the existing inline resolution in trips.html.twig:16-29; the existing cropResize(...).url calls in the theme; other macros under user/themes/intotheeast/templates/macros/.
  • Test scenarios: the macro has no standalone harness; these are asserted through the rendered DOM in U3/U4 specs — cover_image set + resolvable returns that image; cover_image set but file missing falls back to the first-entry image (R11); no cover_image but an entry image exists returns the first-entry image (R7/AE3); no cover_image and no entry images returns null (drives AE4); rendered <img> carries both srcset candidates (R6/AE5) and alt equal to the trip title (R14).
  • Verification: both U3 and U4 render covers through this macro with identical fallback behavior; no inline cover-resolution logic remains in either caller.

U3. Trip-list card: one-liner + retina cover

  • Goal: Show the one-liner on list cards and render the cover sharply, via the shared macro (R5, R6, R7).
  • Requirements: R5, R6, R7, R11, R14.
  • Dependencies: U2.
  • Files: user/themes/intotheeast/templates/trips.html.twig, tests/ui/trip/trips-list.spec.js (new)
  • Approach: Import macros/cover.html.twig. Replace the inline cover block (trips.html.twig:16-29) with cover.resolve(trip) + cover.img(cover, trip.title, 720, 240) inside the existing .trip-card-cover wrapper (keeps the 3:1 aspect + object-fit: cover). Add a one-liner line rendering trip.header.tagline, between .trip-card-title and .trip-card-meta, only when the tagline is set (R5).
  • Patterns to follow: existing card markup and classes in trips.html.twig; .trip-card-cover CSS at css/style.css:1089.
  • Test scenarios:
    • Covers R5. A trip with a tagline renders a one-liner element between the title and the meta line.
    • Covers R5/AE1. A trip with no tagline renders no one-liner element.
    • Covers R6/AE5. The card cover <img> exposes a srcset with 720w and 1440w candidates.
    • Covers R7/AE3. With no cover_image set, the card cover uses the first journal entry's first image.
    • Covers R11. With a cover_image pointing at a missing file, the card falls back to the auto-pick and renders no broken image.
    • Covers R14. The cover alt equals the trip title.
  • Verification: the past-trips list shows one-liners where set and sharp covers on a 2× DPR emulation.

U4. Trip-page header extras (gated)

  • Goal: Extend the in-column header with the one-liner, expandable description, and banner strip — for the trip-page caller only (R8, R9, R10, R12, R13).
  • Requirements: R8, R9, R10, R12, R13.
  • Dependencies: U2.
  • Files: user/themes/intotheeast/templates/partials/trip-feed-col.html.twig, user/themes/intotheeast/templates/trip.html.twig, tests/ui/trip/trip-header.spec.js (new), tests/ui/home/home.spec.js (extend for AE7)
  • Approach: Add a trip_header_extras parameter to the partial, |default(false). In trip.html.twig's include, pass trip_header_extras: true; leave home.html.twig untouched (its include ... only omits the parameter → default false → unchanged, per KTD4/R12). Inside .home-trip-header, gated on the flag and on each value's presence, render in the HTD order: one-liner (trip_page.header.tagline) directly below the title (R8); description (trip_page.content|raw) below the counts as a 23-line preview plus an expand control (R8/R13); banner strip below the description and above the filter bar using cover.resolve(trip_page) + cover.img(...) at banner dimensions, omitted entirely when resolve returns null (R9/AE4). Add a small inline <script> (alongside the existing initTripStats script) that toggles the expanded class on the description. The map+journal split and everything from the filter bar down are not touched (R10).
  • Patterns to follow: the existing .home-trip-header block and inline <script> in trip-feed-col.html.twig; the include ... with {...} only calls in trip.html.twig and home.html.twig.
  • Test scenarios:
    • Covers R8/AE2. Trip page with a tagline and no description shows the one-liner below the title and no description block.
    • Covers R8/R13. Trip page with a description shows a clamped preview plus an expand control that reveals the full text.
    • Covers R8/R9. Trip page with tagline + description + cover shows one-liner, description, and a banner strip positioned above the filter bar.
    • Covers R9/AE3. Trip with no cover_image but an entry image shows the banner using the first-entry image.
    • Covers R9/AE4. Trip with no cover and no entry images renders a text-only header with no banner element.
    • Covers R10/AE6. The map + journal two-column split renders unchanged with no new header inserted above it.
    • Covers R12/AE7. The homepage active-trip view renders no description block and no banner strip (assertion added to home.spec.js).
  • Verification: trip page shows the extras in HTD order and expands the description; the homepage active-trip header is visually identical to before.

U5. Header + banner CSS

  • Goal: Style the one-liner, expandable description, and banner strip, and keep the split above the fold on narrow viewports (R6 display, R9, R13, R15).
  • Requirements: R9, R13, R15.
  • Dependencies: U3, U4 (styles the markup they add).
  • Files: user/themes/intotheeast/css/style.css
  • Approach: Add rules for the trip-card one-liner, the header one-liner, the description preview/expanded states, the expand control, and .trip-header-banner (full width, fixed height in the ~180220px envelope, object-fit: cover, matching radius/spacing of the header). Collapse the description preview with a fixed max-height + overflow: hidden (the expanded state lifts the cap), not -webkit-line-clamp: content|raw renders multi-paragraph markdown (multiple <p>), and line-clamp reliably clamps only a single block box, so it would not hold the 23-line preview across paragraphs. Add a mobile @media block that reduces banner height and reflows the header text so the map+journal split is not pushed off-screen (R15). The existing .trip-card-cover needs no change — object-fit: cover + aspect-ratio: 3/1 already crop the larger derivative.
  • Patterns to follow: existing .home-trip-header, .trip-dates, .home-trip-counts (css/style.css:926-951) and .trip-card-cover (css/style.css:1089); the theme's CSS custom properties (--space-*, --text-*, --color-*).
  • Test scenarios: Test expectation: none — presentational CSS; structural correctness (element presence, expand toggle) is asserted by U3/U4 specs, and appearance/reflow is verified visually including a narrow-viewport check.
  • Verification: on desktop and a mobile viewport, the banner and header text render cleanly and the map+journal split remains visible without scrolling past a wall of header content.

Verification Contract

Dev server: the worktree's Docker dev server at http://localhost:8081 (docker compose ... up). Playwright specs live in the outer repo under tests/ui/ and run against that server.

Gate Command / action Applies to
New + extended UI specs pass npx playwright test tests/ui/trip/trips-list.spec.js tests/ui/trip/trip-header.spec.js tests/ui/home/home.spec.js U3, U4
No regression in related suites npx playwright test tests/ui/trip tests/ui/home tests/ui/maps U4 (shared partial), U5
Admin picker renders + saves Manual: Admin2 → trip form → cover field is a media picker → select → save → confirm filename stored U1
Retina sharpness Manual: DevTools at 2× DPR on /trips and a trip page → cover/banner load the 1440w derivative U2, U3, U4
Acceptance examples Manual walkthrough of AE1AE7 against a trip with/without tagline, description, and cover all

No lint/build step applies — the edited css/style.css and templates are served directly.


Definition of Done

Global

  • AE1AE7 all verified against real trip content (with and without tagline, description, and cover).
  • New specs (trips-list.spec.js, trip-header.spec.js) and the home.spec.js AE7 assertion pass; existing tests/ui/trip, tests/ui/home, and tests/ui/maps suites still pass.
  • Admin2 renders the pagemediaselect cover field and persists the selected filename.
  • The homepage active-trip view is visually unchanged (no description block, no banner).
  • No abandoned/experimental markup, CSS, or scripts left in the diff.
  • Content backfill of one-liners and descriptions for existing trips remains a post-implementation follow-up (per the Product Contract) and is not required for done.

Per unit

Unit Done when
U1 Cover field is a working Admin2 media picker storing a filename.
U2 Both callers resolve and render covers through the macro; no inline cover logic remains.
U3 List cards show one-liners where set and sharp retina covers with correct fallback; U3 specs pass.
U4 Trip-page header shows one-liner, expandable description, and gated banner in HTD order; home view unchanged; U4 specs pass.
U5 Header/banner styled; description expands; split stays above the fold on mobile.