Three Twig macros (stats panel, cycling panel, date range), five template modifications, and two latent bug fixes in inactive templates. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
6.3 KiB
Template Refactor Design
Date: 2026-06-23 Status: Approved for implementation
Problem
trip.html.twig (386 lines) mixes three concerns: 50 lines of stats computation, 80 lines of panel HTML, and 130 lines of map/JS logic. Changes to stats logic or panel structure require reading through all three to understand what's where.
Four templates also duplicate the map_entries collection loop, and date range formatting is independently reimplemented in story.html.twig and stories.html.twig. Two inactive templates (map.html.twig, feed-map.html.twig) have bugs that would surface as soon as they're activated.
Goals
- Reduce
trip.html.twigfrom 386 to ~260 lines by extracting stats computation and panel HTML to macros - Extract date range formatting to one macro used by both story templates
- Fix two latent bugs in inactive templates (wrong DOMContentLoaded timing, CSS not making it into
<head>) - Zero visual change — this is structural only
Non-goals
- Moving stats computation to PHP (performance difference at 60-80 entries is ~5-20ms, only on uncached loads; not worth a plugin)
- Extracting
map_entriesloops to a macro (Twig macros output HTML, not data; they can't return arrays) - Changing any JS logic — DOMContentLoaded wrappers are structural fixes only (no logic changes)
- Fixing
dailies.html.twig's CDN PhotoSwipe — Milestone 1 leftover, inactive page, separate concern - Any visual layout changes
Key constraint: Twig macros vs data
Twig macros produce rendered HTML output, not return values. This means:
- Stats + cycling panels → good macro candidates (compute internally, render HTML)
- Date range string → good macro candidate (outputs text)
map_entriesarray → cannot be a macro; each template's loop stays inline
Architecture
New macros
templates/macros/stats.html.twig
Signature: stats_panel(journal_entries, page, journal_count, has_gpx)
Computes internally: days_on_road, country_display[], city_display[], temp_min, temp_max. Outputs the full <div id="trip-stats-block"> HTML with Twig-computed values baked in and JS placeholder IDs (id="stat-distance") for the distance stat filled by JS after page load.
templates/macros/cycling.html.twig
Signature: cycling_panel()
No computation needed — all values are JS placeholders (id="cyc-distance" etc.). Outputs <div id="trip-cycling-block"> HTML.
templates/macros/date-range.html.twig
Signature: format_date_range(start_date, end_date)
Condensed smart formatting extracted from story.html.twig: same month → 12–15 Jun 2026; same year → 12 Jun – 3 Jul 2026; different years → 28 Dec 2025 – 3 Jan 2026. If end_date is empty or equals start_date, outputs a single date. Used by both story.html.twig and stories.html.twig (unifies the two currently divergent implementations).
Data flow in trip.html.twig
{% import 'macros/stats.html.twig' as m %}
{% import 'macros/cycling.html.twig' as mc %}
{# Data building stays inline (feeds both HTML and JS) #}
{% set journal_entries = ... %}
{% set gpx_urls = [...] %}
{% set gps_points = [...] %} {# for STATS_GPS in <script> #}
{% set map_entries = [...] %} {# for TRIP_ENTRIES in <script> #}
{# HTML: macro calls replace 130 lines of computation + panel HTML #}
{{ m.stats_panel(journal_entries, page, journal_count, has_gpx) }}
{% if has_gpx %}{{ mc.cycling_panel() }}{% endif %}
{# JS block: unchanged, uses Twig vars already in scope #}
<script>
var TRIP_ENTRIES = {{ map_entries|json_encode|raw }};
var STATS_GPS = {{ gps_points|json_encode|raw }};
var GPX_URLS = {{ gpx_urls|json_encode|raw }};
...
</script>
Latent bug fixes
Bug 1 — CSS not reaching <head> in feed-map consumers
feed-map.html.twig calls {% do assets.addCss('map.css') %} inside {% block content %}, but base.html.twig renders {{ assets.css() }} in <head> before content. The CSS is added too late and never appears in <head>.
Fix: remove assets.addCss/addJs from feed-map.html.twig. Add {% block map_assets %} to dailies.html.twig and stories.html.twig — this block is declared at line 11 of base.html.twig, before {{ assets.css() }}, so it registers correctly.
Bug 2 — maplibregl is not defined in map.html.twig and feed-map.html.twig
These templates call new maplibregl.Map() in inline <script> blocks inside {% block content %}. But map.js is in the bottom group, output by {{ assets.js('bottom') }} at the end of <body> — after the inline scripts have already run. MapLibre is not defined yet when the inline script executes.
Fix: wrap map init in document.addEventListener('DOMContentLoaded', function() { ... }). This is the same pattern trip.html.twig already uses correctly; DOMContentLoaded fires after all synchronous scripts (including bottom-group scripts) have run.
File map
| Action | File | Change |
|---|---|---|
| Create | templates/macros/stats.html.twig |
New macro: stats computation + panel HTML |
| Create | templates/macros/cycling.html.twig |
New macro: cycling panel HTML |
| Create | templates/macros/date-range.html.twig |
New macro: smart date range string |
| Modify | templates/trip.html.twig |
Import + call macros; remove stats loops + panel HTML; ~130 lines shorter |
| Modify | templates/story.html.twig |
Replace 15-line date logic with macro call |
| Modify | templates/stories.html.twig |
Add {% block map_assets %}; replace date logic with macro call |
| Modify | templates/map.html.twig |
Add {% block map_assets %} at top level; add DOMContentLoaded wrapper |
| Modify | templates/partials/feed-map.html.twig |
Remove asset registration; wrap map init in DOMContentLoaded |
| Modify | templates/dailies.html.twig |
Add {% block map_assets %} override |
Testing
trip.html.twig: stats panel renders correctly (days, countries, cities, temp range); cycling panel appears when GPX present; map initialises; distance stat fills via JSstory.html.twig: date range displays correctly for single-day, same-month, same-year, cross-year entriesstories.html.twig: same date range validation; map loads without console errorsmap.html.twig: no console errors on page load (was:maplibregl is not defined)- All inactive templates: no regressions on the active trip page