# Asset Pipeline & Frontend Reliability 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. **Goal:** Eliminate all CDN dependencies, self-host fonts, and deduplicate shared JS logic into versioned bundles built via Docker. **Architecture:** esbuild (via throwaway Docker Node container) produces two IIFE bundles — `js/main.js` (universal UI + fonts) and `js/map.js` (MapLibre + GPX utils) — plus extracted CSS in `css-compiled/`. Templates register assets via Grav's Asset Manager instead of hardcoded CDN tags. All duplicated inline JS moves to the main bundle; map init code stays in templates. **Tech Stack:** esbuild 0.21+, Node 20 Alpine (Docker only), MapLibre GL 4, PhotoSwipe 5, Scrollama 3, @mapbox/togeojson 0.16, @fontsource-variable/dm-sans, @fontsource/dm-serif-display, Grav 2.0 Asset Manager. ## Global Constraints - All commands run inside Docker — no local Node.js required - Output files (`js/main.js`, `js/map.js`, `css-compiled/*.css`, `fonts/*.woff2`) are committed to the user/ repo - `node_modules/` is gitignored - Working directory for all file edits: `user/themes/intotheeast/` - All JS bundles use `--format=iife` so templates can reference `maplibregl`, `MapUtils`, `toGeoJSON` as window globals - Grav Asset Manager is used for all asset registration — no hardcoded ` ``` - [ ] **Step 2: Add map_assets block immediately after `{% block content %}`** After the opening `{% block content %}` line, add: ```twig {% block map_assets %} {% do assets.addCss('theme://css-compiled/map.css') %} {% do assets.addJs('theme://js/map.js', {group: 'bottom'}) %} {% endblock %} ``` - [ ] **Step 3: Remove the duplicated JS blocks** Remove the following ` ``` Add the map assets block at the very top of the `{% if map_entries|length > 0 %}` block (before the `
`): ```twig {% block map_assets %} {% do assets.addCss('theme://css-compiled/map.css') %} {% do assets.addJs('theme://js/map.js', {group: 'bottom'}) %} {% endblock %} ``` - [ ] **Step 2: map.html.twig — remove CDN tags, add map_assets block** In `user/themes/intotheeast/templates/map.html.twig`: Remove lines 39–42: ```html ``` Add after `{% block content %}`: ```twig {% block map_assets %} {% do assets.addCss('theme://css-compiled/map.css') %} {% do assets.addJs('theme://js/map.js', {group: 'bottom'}) %} {% endblock %} ``` - [ ] **Step 3: story.html.twig — remove Scrollama CDN tag** In `user/themes/intotheeast/templates/story.html.twig`: Remove line 72: ```html ``` Scrollama is now bundled in `main.js` and exposed as `window.scrollama`. The existing inline script that calls `scrollama()` will work unchanged. - [ ] **Step 4: Commit** ```bash git -C user add themes/intotheeast/templates/partials/feed-map.html.twig themes/intotheeast/templates/map.html.twig themes/intotheeast/templates/story.html.twig git -C user commit -m "refactor: remove CDN tags from feed-map, map, story templates" ``` --- ### Task 8: End-to-end verification — no external requests, all features intact **Files:** None modified. Verification only. - [ ] **Step 1: Verify zero external requests on the trip page** Open `http://localhost:8081/trips/japan-korea-2026`. Open DevTools → Network tab → reload. Check these domains appear zero times: - `cdn.jsdelivr.net` - `fonts.googleapis.com` - `fonts.gstatic.com` - [ ] **Step 2: Verify trip page features** - Map renders with markers and GPX track - Marker click scrolls to entry card and flashes it - Fullscreen map toggle expands/collapses - Filter bar: All / Journal / Stories each filter correctly - Sort toggle (↑/↓) reverses feed order - Stats panel opens and closes (click "Stats ▾" button) - Cycling panel opens and closes if GPX present ("Cycling ▾" button) - GPX distance figure populates in stats grid - Cycling stats grid populates (distance, gain, loss, highest, lowest, moving time, avg speed) - Back-to-top button appears after scrolling down; click scrolls to top - Journal photo strip: swipe/scroll dots sync; ‹ › buttons navigate; expand button opens PhotoSwipe; arrow keys advance; click outside closes - [ ] **Step 3: Verify story page** Open a story URL (e.g. `http://localhost:8081/trips/italy-2026-demo/stories/sorano-rock-and-time`): - Hero image loads - Scroll overlay darkens/lightens on scroll - Story title fades into nav bar as hero scrolls out - Back-to-top button appears and works - If page has `.scrolly` sections: they animate on scroll - No console errors - [ ] **Step 4: Check built file sizes** ```bash ls -lh user/themes/intotheeast/js/main.js user/themes/intotheeast/js/map.js user/themes/intotheeast/css-compiled/main.css user/themes/intotheeast/css-compiled/map.css ``` Rough expected sizes (minified): - `main.js`: ~80–150 KB (PhotoSwipe + Scrollama + UI code) - `map.js`: ~600–900 KB (MapLibre GL dominates) - `main.css`: ~30–60 KB (PhotoSwipe + @font-face rules) - `map.css`: ~80–120 KB (MapLibre GL styles) If `map.js` is unexpectedly small (<100 KB), MapLibre GL may not have bundled — check that `import maplibregl from 'maplibre-gl'` is in `js/src/map.js`. - [ ] **Step 5: Final commit** ```bash git -C user add -A git -C user status # confirm only expected files git -C user commit -m "chore: verify asset pipeline — all CDN deps eliminated" git add -A git commit -m "chore: complete asset pipeline — self-hosted deps, deduplicated JS" ```