Files
intotheeast-com/docs/solutions/architecture-patterns/retiring-a-consolidated-grav-sub-page.md
T
m038andClaude Opus 4.8 2695bce835 docs: compound learning — retiring a consolidated Grav sub-page
Capture the safe procedure + three non-obvious traps from the standalone-
page cleanup: keep the folder as a routable:false data container, repoint
Back-link fallbacks to the grandparent (silent direct-landing regression),
and sync the gitignored demo source + Makefile or the next demo-load undoes it.

Seed CONCEPTS.md (Trip, Entry, Story, Active Trip) and surface docs/solutions/
+ CONCEPTS.md in CLAUDE.md so future sessions discover them.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 14:23:25 +02:00

7.1 KiB

title, date, category, module, problem_type, component, severity, applies_when, tags
title date category module problem_type component severity applies_when tags
Retiring a standalone Grav sub-page that was consolidated onto another page 2026-07-04 architecture-patterns Grav theme — trip pages / templates architecture_pattern rails_view medium
Deleting a standalone Grav view whose content now renders inside another page
A page folder holds child pages that other templates fetch via grav.pages.find(route).children
Detail pages have a Back link that falls back to the deleted page
The affected trip is demo content regenerated by make demo-load
grav
twig
page-tree
routable
back-link
demo-content
page-retirement

Retiring a standalone Grav sub-page that was consolidated onto another page

Context

The site consolidated four standalone trip views — /map, /stats, /dailies, /stories — onto a single trip page (inline map + filter bar + inline stats). Removing the now-redundant view pages looks like a simple git rm, but a Grav "page" is a folder + a .md that names a template, and several things quietly depend on both halves. Missing any of them ships a broken site or a broken make demo-load. This captures the safe procedure and the three traps that are not obvious from the file listing.

Guidance

Treat a page as two separable roles: a routable view (the .md's template renders a URL) and a data container (the folder holds child pages other code reads). Retiring the view must preserve the container.

1. Keep the folder; neutralise the view — do not delete the container. 01.dailies/ and 04.stories/ hold the journal/story children, and trip.html.twig / home.html.twig reach them via grav.pages.find(route ~ '/dailies').children. Deleting the folder (or its .md) breaks that lookup and the entries vanish from the feed. Instead, keep the folder and repoint its .md to an inert container:

# 01.dailies/dailies.md
---
title: Journal
template: default      # was: dailies  (dailies.html.twig is deleted)
routable: false        # the container's own URL 404s
visible: false         # not enumerated in nav
---

routable: false makes the container URL inert without unrouting its children — individual entries stay reachable at /trips/<slug>/dailies/<entry>, and find(...).children still resolves because the page remains in the tree. (Folders that were pure views with no children — 02.map/, 03.stats/ — can be deleted outright.)

2. Repoint Back-link fallbacks to the surviving surface (grandparent), not the retired parent. Detail templates used href="{{ page.parent().url }}" with an onclick that runs history.back() when history exists. history.back() covers in-app navigation, but the href is the fallback for direct-landing visitors (shared link, new browser tab, search result) — and it pointed at the now-inert container:

{# entry.html.twig / story.html.twig — BEFORE (breaks on direct landing) #}
<a class="back-pill" href="{{ page.parent().url }}"
   onclick="if(history.length > 1){ history.back(); return false; }">← Back</a>

{# AFTER — fall back to the trip page (grandparent), the surface that survived #}
<a class="back-pill" href="{{ page.parent().parent().url }}"
   onclick="if(history.length > 1){ history.back(); return false; }">← Back</a>

This is a silent regression: the detail page renders fine (200, no Twig error), so curl/CI-of-the-page all pass. It only breaks when a cold visitor clicks Back — the exact path a shared link takes.

3. Sync the gitignored demo source AND the Makefile, or the next reload undoes the cleanup. The italy-2026-demo trip pages are gitignored; they are regenerated by make demo-load from user/docs/demo/trips/italy-2026-demo/ (which is tracked in the user repo). Deleting pages from the live tree is not enough — you must also:

  • delete/repoint the demo source files (map.md, stats.md, and the dailies//stories container .mds), and
  • update the demo-load Makefile target so it no longer mkdirs 02.map/03.stats or copies the deleted .mds.

Otherwise the next make demo-load recreates the deleted pages pointing at deleted templates. This matters doubly because Playwright's global-setup runs make demo-load before the suite — stale demo source breaks tests, not just a manual reload.

4. Sweep the test suite for the deleted routes. Delete tests that target the gone pages; re-point tests whose behaviour moved to the consolidation surface (e.g. the feed sort toggle is now #trip-sort-toggle on the trip page). Note the consolidation surface may sort differently (the trip page is oldest-first; the old /dailies view was newest-first) — re-pointed ordering assertions may need to invert.

Why This Matters

The two halves of a Grav page (routable view vs. data container) are invisible in a file listing but load-bearing. The failure modes are asymmetric and sneaky: deleting a container loudly empties a feed (easy to catch), but the back-link fallback fails silently for only a subset of visitors, and the demo-source drift fails later — on the next reload or CI run, not during the change. A curl/HTTP smoke test passes all three while two are broken. Getting the procedure right the first time avoids a shipped regression and a red suite that looks unrelated to the change.

When to Apply

  • Deleting any Grav view page whose feed/map/list now renders inside another page.
  • Any time a page folder is a parent of child pages that templates fetch via find(route).children — keep it as a routable:false container.
  • Whenever a detail page's Back link (or any page.parent() reference) could resolve to the page being retired.
  • Whenever the affected trip is italy-2026-demo (or any gitignored, demo-load-regenerated content).

Examples

Verification that proves the container split worked (children of a routable:false parent stay reachable):

# keepers
/                                         → 200
/trips/italy-2026-demo                     → 200   (feed still populated)
/trips/italy-2026-demo/dailies/<entry>     → 200   (child of routable:false container)
/trips/italy-2026-demo/stories/<story>     → 200
# retired views
/trips/italy-2026-demo/map                 → 404
/trips/italy-2026-demo/stats               → 404
/trips/italy-2026-demo/dailies             → 404   (container inert; children still route)

Grep gate before declaring done — no include/import/link references to the deleted templates remain (a lone macros/stats.html.twig hit is the shared macro, a keeper — not the deleted stats.html.twig page):

grep -rnE "include .*(feed-map|dailies|stories|map|stats)\.html|~ '/map'|~ '/stats'" templates/
  • docs/working/plans/2026-07-04-standalone-page-cleanup.md — the plan this learning came from
  • docs/reference/architecture.md — page-tree structure and the single MapUtils.initEntryMap map path
  • docs/guides/trip-switching.md — new trips now scaffold only 01.dailies/ + 04.stories/ (inert containers)
  • CLAUDE.md → "Trip entity architecture" and "One map path" — the current-state contract