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>
This commit is contained in:
@@ -10,6 +10,8 @@
|
||||
- **scripts/**: Server install and maintenance scripts
|
||||
- **user/**: Site content, config, pages, and theme (standalone git repo — do not modify from here)
|
||||
- **docs/**: All plans, specs, and project documentation (moved here from `user/docs/` on 2026-06-19)
|
||||
- **docs/solutions/**: documented solutions to past problems (bugs, patterns, workflow gotchas), organized by category with YAML frontmatter (`module`, `tags`, `problem_type`). Relevant when implementing or debugging in a documented area
|
||||
- **CONCEPTS.md** (repo root): shared domain vocabulary (Trip, Entry, Story, Active Trip). Relevant when orienting to the codebase or discussing domain concepts
|
||||
|
||||
### Current stack
|
||||
|
||||
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
# Concepts
|
||||
|
||||
Shared domain vocabulary for this project — entities, named processes, and status concepts with project-specific meaning. Seeded with core domain vocabulary, then accretes as ce-compound and ce-compound-refresh process learnings; direct edits are fine. Glossary only, not a spec or catch-all.
|
||||
|
||||
## Relationships
|
||||
|
||||
A **Trip** owns its **Entries** and **Stories**. Exactly one Trip is the **Active Trip** at a time; it is the one surfaced on the home page and the target for new posts. Entries and Stories are always scoped to a Trip — they do not exist independently.
|
||||
|
||||
## Trip
|
||||
|
||||
### Trip
|
||||
A single journey the blog is organised around — the top-level content entity. A Trip aggregates its Entries and Stories and carries its own metadata (title, start/end dates, cover image, route GPX files). Each Trip renders as one consolidated **Trip page** showing an inline map, a filtered feed, and inline stats; the journal, map, stats, and story views are not separate pages.
|
||||
|
||||
### Active Trip
|
||||
The one Trip currently featured — set in site config and read by the home page and the post form. Switching the Active Trip is a deliberate, multi-file change; if the post form's target and the featured Trip fall out of sync, new posts land under the wrong Trip.
|
||||
|
||||
### Entry
|
||||
A single dated journal post within a Trip — the atomic unit of the day-to-day travel log.
|
||||
*Avoid:* daily, journal post
|
||||
|
||||
The Trip's journal section is labelled "Journal" and lives in the Trip's `dailies` container, so an Entry is colloquially "a daily"; in templates and page metadata the same thing is called an `entry`. Entries carry a date, optional location and coordinates, weather, and photos, and are ordered by date within a Trip.
|
||||
|
||||
### Story
|
||||
A long-form, designed narrative piece within a Trip — hero image plus scrollytelling/gallery sections — distinct from the short, dated Entry. Stories are curated set pieces; Entries are the running log.
|
||||
|
||||
## Flagged ambiguities
|
||||
|
||||
- "daily" / "entry" / "journal post" all refer to the same concept (a dated journal post). Canonical term: **Entry**. The section/folder is named "dailies" and the nav label is "Journal" — these name the *collection*, not a different entity.
|
||||
@@ -0,0 +1,104 @@
|
||||
---
|
||||
title: Retiring a standalone Grav sub-page that was consolidated onto another page
|
||||
date: 2026-07-04
|
||||
category: architecture-patterns
|
||||
module: Grav theme — trip pages / templates
|
||||
problem_type: architecture_pattern
|
||||
component: rails_view
|
||||
severity: medium
|
||||
applies_when:
|
||||
- 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
|
||||
tags: [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:
|
||||
|
||||
```yaml
|
||||
# 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:
|
||||
|
||||
```twig
|
||||
{# 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 `.md`s), and
|
||||
- update the `demo-load` Makefile target so it no longer `mkdir`s `02.map`/`03.stats` or copies the deleted `.md`s.
|
||||
|
||||
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/
|
||||
```
|
||||
|
||||
## Related
|
||||
|
||||
- `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
|
||||
Reference in New Issue
Block a user