Files
intotheeast-com/docs/reference/architecture.md
T
m038andClaude Opus 5 ed6e43ae51 docs: trim CLAUDE.md 305->179 lines; extract code descriptions to docs/
CLAUDE.md is loaded into context on every request, so every line has a
recurring cost. Applies one rule to decide what earns its place: keep what
changes behaviour (rules and gotchas Claude cannot discover before it acts);
extract what merely describes code (Claude reads the code anyway, and prose
about code silently drifts).

The four stale facts fixed in the previous commit were all in the
"describes code" class -- active_trip, the Admin2 version, demo-load's
scope, the gitignore list. None were rules. That is the argument for moving
this material next to what it documents.

Extracted (kept as pointers):
- entry-map + trip-feed-col parameter contracts (56 lines) -> reference/
  architecture.md "Shared partial contracts". CLAUDE.md keeps only the
  invariants: single map path, must assign window.tripMap/homeMap, keep
  trip-feed-col single-purpose, initTripStats depends on MapUtils.
- Prod override runbook (49 -> 9 lines) -> guides/deploy-cycle.md "The env
  override tree", incl. the Twig dev/prod table and WEB_HOST. CLAUDE.md
  keeps the two behavioural rules: never commit prod values, and Admin on
  the server writes to the env tree (so check both config paths, env wins).
- GPX API routes, session auth and the Blob/FormData upload gotcha ->
  guides/gpx-manager.md "How the manager is wired".
- Trip-switch procedure -> guides/trip-switching.md. CLAUDE.md keeps the
  one rule that matters: never re-add pageconfig.parent to post-form.md.
- Also trimmed the dev-command table and custom-plugin table added in the
  previous commit; both largely restated the Makefile and blueprints.

Fixed the guides being pointed into, so the pointers lead to truth:
- trip-switching.md instructed editing a pageconfig.parent that no longer
  exists -- its whole "two files must be updated together" premise was
  obsolete and would have reintroduced the desync it warned about.
- architecture.md: Grav 2.0.4->2.0.7, Admin2 2.0.10->2.0.12, corrected the
  posting pipeline to show cache-on-save injecting parent before the write,
  added entry-actions to the custom-plugin list.
- japan-korea-2026 -> denmark-2026 across guides/reference (docs/solutions
  keeps its historical references intact -- those are incident records).

Verified: every markdown link resolves, every referenced section heading
exists, and each extracted item was confirmed present in its new home.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-24 20:55:28 +02:00

11 KiB

Architecture Overview

How the intotheeast site hangs together.


Stack

Layer Technology Notes
CMS Grav 2.0.7 stable Flat-file PHP CMS; no database. Server upgrades in place via bin/gpm self-upgrade
Admin Admin2 v2.0.12 Plugin slug: admin2 (not admin)
GPM channel stable Authoritative in user/config/system.yamlgpm.releases; GRAV_CHANNEL=production in compose is cosmetic
Container Docker (getgrav/grav base + custom Dockerfile) Grav 2.0 baked in at build time
PHP session session.save_path = /tmp Set in php/php-local.ini
Dev URL http://localhost:8081 Mapped from container port 80
Maps MapLibre GL JS Replaced Leaflet; one shared map path (MapUtils.initEntryMap) on trip + home
GPX rendering toGeoJSON (bundled in js/map.js) Parses GPX → GeoJSON route layers client-side; no CDN

Plugin roles

The posting pipeline is a chain of three plugins:

Browser POST /post
│
├─ Grav Form plugin (built-in)
│  └─ validates required fields; handles file uploads
│
├─ cache-on-save (custom) — onFormValidationProcessed, runs BEFORE the write
│  ├─ setData('parent', …) ← derived from site.active_trip
│  └─ sets pageconfig.overwrite_mode: edit when the hidden edit_path is filled,
│     false when empty (create a fresh dated folder)
│
├─ add-page-by-form (third-party, patched — see deploy/patches/)
│  └─ reads post-form.md config:
│     ├─ pageconfig.slug_field → slug from date + title
│     └─ pagefrontmatter → template: entry
│  └─ writes entry.md to user/pages/01.trips/<trip>/01.dailies/<slug>.entry/
│  └─ moves uploaded photos into the page folder
│
└─ cache-on-save (again, post-write)
   └─ calls $grav['cache']->deleteAll() on every new-entry form submission
   └─ ensures entries appear in feed immediately in both dev and prod mode

Other notable plugins:

Plugin Role
login Auth for /post and /gpx-manager
api (Grav API v1) Used by /gpx-manager to list/upload/delete GPX files
admin2 Admin panel at /admin
story-blocks (custom) Storytelling shortcode blocks for long-form stories (needs shortcode-core)
entry-actions (custom) Owner-only, active-trip-scoped journal entry actions (delete) via the Grav API

Plugin management model

Three categories, by how each plugin is installed and maintained:

  1. GPM-managed (plugins.txtmake install-plugins): the marketplace plugins, including login, form, admin2, api, flex-objects, shortcodes, etc. As of the 2.0.4 upgrade, admin2/api/flex-objects moved into this category — they were previously hand-extracted from the core bundle. Update with bin/gpm update (make remote-update-plugins-<env> on servers).
  2. Custom, in-repo (user/plugins/ allowlisted in user/.gitignore): cache-on-save, story-blocks, entry-actions. Versioned in the user repo.
  3. Remote-only: git-sync — installed and configured only on servers, never in plugins.txt, and disabled during upgrades.

Template hierarchy

All page templates extend base.html.twig:

templates/
├─ base.html.twig          ← site shell: nav, fonts, CSS tokens
├─ default.html.twig       ← extends base; generic page
├─ home.html.twig          ← extends base; context-aware two-column layout
├─ trip.html.twig          ← extends base; trip page with filter bar (All/Journal/Stories)
├─ entry.html.twig         ← extends base; single journal entry (gallery, badges, map)
├─ story.html.twig         ← extends base; single story (Ken Burns hero, shortcodes)
└─ gpx-manager.html.twig   ← extends base; admin UI for GPX file management

The standalone dailies.html.twig, map.html.twig, stats.html.twig and stories.html.twig view templates were removed in the 2026-07-04 standalone-page cleanup — the trip page (trip.html.twig) consolidated the feed, inline map, and inline stats.

Partials live in templates/partials/ (plus macros in templates/macros/). Key partials: base.html.twig (site shell extended by all page templates), entry-map.html.twig (shared map column + initEntryMap call, used by trip + home), trip-feed-col.html.twig (feed column chrome, shared by trip + home), home-predeparture.html.twig, entry-journal.html.twig / entry-story.html.twig (feed cards), trip-publish-toggle.html.twig, and weather-icons.html.twig.

Shared partial contracts

Two partials are included by both trip.html.twig and the active branch of home.html.twig, via {% include … with {…} only %}. The only keyword means every value must be passed explicitly — the tables below are the contracts. The rules that govern them (single map path, required map globals, never hand-edit bundles) live in CLAUDE.md; these are the parameter details.

entry-map.html.twig

Renders the .home-map-col column (map div #{{ map_id }} + fullscreen button) and, when entries is non-empty, a thin <script> assigning window.{{ map_global }} from initEntryMap. Callers resolve header values (use_gpx / autoconnect) and pass them in.

Parameter Type Trip passes Home passes
map_id string 'trip-map' 'home-map'
map_global string 'tripMap' 'homeMap'
entries array [{lat, lng, slug, title, url, type?, force_connect, ...}] same
card_prefix string 'entry-' 'entry-'
story_markers bool true (diamond markers) false
gpx_urls array gpx_urls home_gpx_urls
use_gpx bool page.header.use_gpx ?? true derived from trip.header
autoconnect string page.header.autoconnect ?? 'on' derived from trip.header
gpx_source_prefix string 'gpx' 'home-gpx'
journey_id string 'trip-journey' 'home-journey'

trip-feed-col.html.twig

The column beside the map: date-range header, filter bar, stats/cycling panels, feed loop.

Parameter Type Trip passes Home-active passes
trip_page Page page trip
all_items array sorted by date, flag 4 (oldest→newest) sorted by date, flag 3 (newest→oldest)
journal_entries array dailies children dailies children
journal_count / story_count int counts counts
has_gpx bool has_gpx home_gpx_urls|length > 0
gpx_urls array gpx_urls home_gpx_urls
gps_points array gps_points gps_points
show_sort bool true false (home keeps its own feed order)
trip_header_extras bool true not passed (defaults false)

trip_header_extras gates the trip-page-only header block (one-liner .home-trip-tagline, expandable .trip-header-desc, .trip-header-banner cover strip) rendered between the counts and the filter bar. home.html.twig omits it so those extras never leak onto the home route.

Sibling: home-predeparture.html.twig is the home-only "Coming soon" landing state, taking only trip_page. home.html.twig picks it with {% if all_items|length == 0 %}home-predeparture {% else %}trip-feed-col. Keep trip-feed-col single-purpose — do not fold the pre-departure branch back into it.

Stats/cycling JS glue: the partial emits an inline DOMContentLoaded script calling window.initTripStats({ gpxUrls, gpsPoints, hasGpx }) — one shared function in js/src/main.js. It no-ops when #stat-distance is absent, populates exact distance + cycling stats from GPX, and falls back to a ~-prefixed haversine estimate (or for <2 points) when there is no GPX. It depends on window.MapUtils from map.js (loaded in the bottom asset group on both pages).

History: the map setup replaced an older three-variant arrangement (a feed-map.html.twig partial with its own inline init, plus a full-page map.html.twig), deleted in the 2026-07-04 standalone-page cleanup.


Trip entity structure

The site is organized around Trip entities. The active trip is set in user/config/site.yamlactive_trip, as a route (e.g. /trips/denmark-2026), not a bare slug.

user/pages/01.trips/
└─ denmark-2026/
   ├─ trip.md              ← template: trip; title, date_start, cover_image, album_url
   ├─ *.gpx               ← GPX route files (served as page media; auto-detected by trip.html.twig)
   ├─ 01.dailies/         ← journal entry children (container .md is routable:false)
   └─ 04.stories/         ← story children (container .md is routable:false)

01.dailies/ and 04.stories/ are inert data containers — the trip page aggregates their children; visiting the container routes directly 404s/redirects. (The former 02.map/ and 03.stats/ folders were removed with their view templates.)


GPX data flow

GPX file uploaded to trip page media
         │
         ▼
user/pages/01.trips/<slug>/*.gpx
         │
         ▼
trip.html.twig / home.html.twig: trip_page.media.all → filter .gpx → entry-map partial
         │
         ▼
MapLibre source: each GPX file parsed by toGeoJSON (bundled in js/map.js) → GeoJSON source
         │
         ▼
Connector suppression: same-file 10km proximity check prevents spurious inter-track segments
         │  (override with force_connect: true in trip frontmatter)
         ▼
Rendered as route polyline on map

Data flow for a post submission

1. User fills /post form and taps Submit
2. Grav Form plugin validates: title and content required
3. cache-on-save (onFormValidationProcessed) injects the write target:
     parent ← derived from site.active_trip  (e.g. /trips/denmark-2026/dailies)
     overwrite_mode ← edit if edit_path filled, else false
4. add-page-by-form reads post-form.md:
     pageconfig.slug_field:  date,title
     pagefrontmatter:        template: entry
5. New page written to:
     user/pages/01.trips/denmark-2026/01.dailies/
     └─ 2026-07-20-0930-first-day-in-kyoto.entry/
        └─ entry.md
6. Photos moved into the same folder
7. cache-on-save calls $grav['cache']->deleteAll()
8. Browser: form shows success message
9. Feed at /trips/denmark-2026 immediately shows new entry

Key config files

File Purpose
user/config/site.yaml active_trip route; site title/description
user/config/system.yaml Twig cache, flex accounts/pages, language prefix
user/config/media.yaml Registers .gpx as a valid media type
user/plugins/api/api.yaml session_enabled: true for GPX manager auth
user/themes/intotheeast/css/tokens.css Design tokens (colors, fonts, spacing)
CLAUDE.md Project rules and always-loaded context for Claude