# Home / Trip View Convergence Design **Date:** 2026-06-27 **Status:** Approved for implementation ## Problem The home page in active-trip mode (`home.html.twig`, `config.site.travelling` branch) and the trip page (`trip.html.twig`) are meant to present the same experience — the same content, behaving near-identically. Today their feeds already match (both render journal + story entries via the shared `entry-journal`/`entry-story` partials), but the **feed-col chrome diverges**: | Feature | Trip page | Home-active | Converge? | |---|---|---|---| | Feed lists journal + stories | ✅ | ✅ | already matches | | Date-range header | ✅ | ❌ | **yes** | | Filter bar (All / Journal / Stories) | ✅ | ❌ | **yes** | | Stats panel | ✅ | ❌ | **yes** | | Cycling panel | ✅ (if GPX) | ❌ | **yes** | | Sort toggle button | ✅ | ❌ | **no — intended difference** | | Default feed order | oldest→newest (sort flag 4) | its own (sort flag 3) | **no — intended difference** | The chrome markup is the divergence. The supporting **behavior is already global**: `js/main.js` (loaded for every page via `base.html.twig:10`) runs `initFilterBar()`, `initPanelToggles()`, and `initSortButton('trip-sort-toggle', …)`, each a silent no-op when its markup is absent. The entry partials already emit `data-type`, which the filter relies on. So rendering the same markup on home is enough for the filter bar, panel toggles, and (where present) the sort button to work with **zero new JS**. The single exception is the **stats/cycling computation glue** (writing distance/elevation values into `#stat-distance`, `#cyc-*`). That code is currently *inline* in `trip.html.twig` and not global, so the stats/cycling panels cannot function on home until it is shared. ## Goals - Home-active gains the date-range header, filter bar, and stats/cycling panels — matching the trip page. - The shared feed-col chrome lives in **one** place (a partial), so future header/chrome changes apply to both pages. - Home-active keeps its own default feed order and has **no** sort button (the two intended differences). - Stats/cycling computation works on both pages from a single shared JS function. - No change to the trip page's rendered output (structural refactor only on that side). ## Non-goals - **Map convergence is out of scope.** The home-active map omitting story markers, lacking a fullscreen button, and its hash-only click behavior are all deferred to a later map-init spec (see `project-map-init-refactor` memory). Both inline map scripts and both map-col markup blocks stay exactly as they are. - Extracting the `all_items` / `map_entries` build loops to a macro — Twig macros output HTML, not arrays (established constraint; see `2026-06-23-template-refactor-design.md`). Each page keeps its own data-build loops. - Any visual restyling of the chrome — home reuses the trip's existing CSS classes unchanged. - Adding a sort button to home, or changing home's default order. ## Architecture ### 1. New shared partial: `templates/partials/trip-feed-col.html.twig` Holds the entire `.home-feed-col` content currently inline in `trip.html.twig:70-120`: - header (`.home-trip-header`): title, date range (when `trip_page.header.date_start` set), counts - filter bar (`.trip-filter-bar`): All / Journal / Stories buttons - the sort button (`#trip-sort-toggle`) — **rendered only when `show_sort` is true** - panel toggles (`.trip-panel-toggles`): Stats, and Cycling (when `has_gpx`) - `stats_panel(...)` and (when `has_gpx`) `cycling_panel(...)` macro calls - the feed loop over `all_items` with the `#feed-filter-empty` sentinel **Interface** (called via `{% include 'partials/trip-feed-col.html.twig' with {…} only %}`): | Param | Type | Trip passes | Home-active passes | |---|---|---|---| | `trip_page` | Page | `page` | `trip` | | `all_items` | array | sorted by date, flag 4 | sorted by date, flag 3 | | `journal_entries` | array | dailies children | dailies children | | `journal_count` | int | count | count | | `story_count` | int | count | count | | `has_gpx` | bool | `gpx_urls\|length > 0` | `home_gpx_urls\|length > 0` | | `show_sort` | bool | `true` | `false` | Because the partial is called with `only`, it must `{% import 'macros/stats.html.twig' %}` and `{% import 'macros/cycling.html.twig' %}` itself. Both pages already build `all_items`, the counts, and `has_gpx` for their existing map data, so these are passed in rather than rebuilt — no new duplication is introduced. ### 2. Shared stats glue: `initTripStats(config)` in `js/src/main.js` Extract the inline stats/cycling computation from `trip.html.twig:213-249` into a config-driven function. Current inline logic: if GPX present, `MapUtils.parseGpxFiles(urls, …)` fills `#stat-distance` and all `#cyc-*` fields; otherwise sum `haversineKm` over `gps_points` and write the `~`-prefixed estimate to `#stat-distance`. ```js function initTripStats(config) { // config: { gpxUrls: [], gpsPoints: [[lat,lng],...], hasGpx: bool } // No-op if #stat-distance is absent (page has no stats panel). } ``` Called from the boot block alongside the other inits. Each page provides the config via a small inline `