diff --git a/docs/working/plans/2026-07-05-trip-description-and-hero.md b/docs/working/plans/2026-07-05-trip-description-and-hero.md new file mode 100644 index 0000000..cdbc958 --- /dev/null +++ b/docs/working/plans/2026-07-05-trip-description-and-hero.md @@ -0,0 +1,261 @@ +--- +title: Trip Description, One-liner & Hero Image - Plan +type: feat +date: 2026-07-05 +topic: trip-description-and-hero +artifact_contract: ce-unified-plan/v1 +artifact_readiness: implementation-ready +product_contract_source: ce-brainstorm +execution: 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 header** — `home-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 (~180–220px) 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 (~180–220px) 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 (~180–220px) 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 ~2–3 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 2–3-line preview and the full body in markup, and toggle an expanded class with a small inline `