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>
This commit is contained in:
@@ -28,26 +28,13 @@ The Docker dev server runs at **http://localhost:8081** (mapped from container p
|
||||
|
||||
### Local dev commands
|
||||
|
||||
| Command | What it does |
|
||||
|---|---|
|
||||
| `make setup` | One-shot first run: `build` → `start` → `install-plugins` → `fix-perms` |
|
||||
| `make start` / `make stop` | Bring the compose stack up / down (`start-grav` = Grav service only) |
|
||||
| `make build` | Rebuild the custom Docker image (after `Dockerfile` changes) |
|
||||
| `make build-assets` | Rebuild theme JS/CSS bundles in a `node:20-alpine` container |
|
||||
| `make install-plugins` | GPM-install everything in `plugins.txt`, then re-apply local patches |
|
||||
| `make fix-perms` | Fix ownership in the container after root-owned writes |
|
||||
`make setup` for a first run (build → start → install-plugins → fix-perms); `make start` / `make stop` thereafter. Other targets are self-describing in the `Makefile`.
|
||||
|
||||
**`make build-assets` is mandatory after editing anything in `user/themes/intotheeast/js/src/`.** Sources live in `js/src/`; esbuild writes the committed bundles — `js/main.js`, `js/map.js`, `js/feed-actions.js`, `js/trip-publish.js`, `js/post/`, and the CSS extracted into `css-compiled/`. **Never hand-edit those.** By contrast `css/style.css` and `css/tokens.css` are hand-authored sources, not build outputs. `build-assets` runs as your host UID (`--user`) so the outputs in the bind-mounted `user/` tree are not root-owned.
|
||||
**`make build-assets` is mandatory after editing anything in `user/themes/intotheeast/js/src/`** — esbuild writes the *committed* bundles `js/main.js`, `js/map.js`, `js/feed-actions.js`, `js/trip-publish.js`, `js/post/`, and `css-compiled/`. **Never hand-edit those.** By contrast `css/style.css` and `css/tokens.css` are hand-authored sources.
|
||||
|
||||
### Custom plugins
|
||||
|
||||
Three plugins are site-owned and tracked in the `user/` repo (everything else under `user/plugins/` is GPM-managed and git-ignored):
|
||||
|
||||
| Plugin | Role |
|
||||
|---|---|
|
||||
| `cache-on-save` | Clears the page-tree cache on `new-entry` submits, and derives the write target from `site.active_trip` (`onFormValidationProcessed` → `setData('parent', …)`) |
|
||||
| `story-blocks` | Storytelling shortcode blocks for long-form stories (depends on `shortcode-core`) |
|
||||
| `entry-actions` | Owner-only, active-trip-scoped journal entry actions (delete) via the Grav API |
|
||||
Three plugins are site-owned and tracked in the `user/` repo (everything else under `user/plugins/` is GPM-managed and git-ignored): **`cache-on-save`** (clears page-tree cache on `new-entry` submits + injects the write target from `site.active_trip`), **`story-blocks`** (story shortcodes), **`entry-actions`** (owner-only entry delete via the API).
|
||||
|
||||
### Local plugin patches
|
||||
|
||||
@@ -66,96 +53,28 @@ The site is structured around Trip entities. Key facts:
|
||||
- `01.dailies/` and `04.stories/` are `routable:false` **data containers** — visiting `/trips/<slug>/dailies` or `/stories` directly 404s/redirects; their children (entries/stories) render at their own detail URLs and are aggregated by the trip page
|
||||
- Site nav in `base.html.twig` has Home + Past Trips only — does not link to trip sub-sections
|
||||
- New journal entries are written to the active trip's `dailies` — the write target is derived from `site.active_trip` at submit time by the `cache-on-save` plugin (post-form.md no longer hardcodes `pageconfig.parent`)
|
||||
- The trip page (`trip.html.twig`) uses a **client-side filter bar** (All content / Journal / Stories). The standalone `/dailies`, `/map`, `/stats`, `/stories` view pages no longer exist — do NOT try to re-create them or link to them. This filter bar + stats chrome is shared with the home active-trip view via the `trip-feed-col` partial (see "Shared trip-feed-col partial" below)
|
||||
- The trip page (`trip.html.twig`) uses a **client-side filter bar** (All content / Journal / Stories). The standalone `/dailies`, `/map`, `/stats`, `/stories` view pages no longer exist — do NOT try to re-create them or link to them. This filter bar + stats chrome is shared with the home active-trip view via the `trip-feed-col` partial (see "Two shared partials" below)
|
||||
- Stats are shown inline on the trip page via a toggle (the standalone `/stats` view was removed)
|
||||
- GPX route files live as media on the trip page itself, parsed client-side via toGeoJSON (bundled into `js/map.js`) and drawn on the trip/home map
|
||||
- Manage GPX files (view/upload/delete) at `/gpx-manager` — requires admin login; filenames are auto-slugified on upload
|
||||
|
||||
### One map path: `MapUtils.initEntryMap` + the `entry-map` partial
|
||||
### Two shared partials — the rules
|
||||
|
||||
There is a **single** map code path on the site. The engine is `MapUtils.initEntryMap(opts)` in `js/src/maplibre-utils.js` (bundled into `js/map.js` via `make build-assets` — never hand-edit `js/map.js`). It builds the MapLibre map, places markers/popups, fits bounds, draws the GPX journey, and wires the fullscreen toggle.
|
||||
Trip and home render the same map and feed chrome through **two** shared partials, both included with `{% include … with {…} only %}`. Full parameter contracts: [`docs/reference/architecture.md`](docs/reference/architecture.md) → "Shared partial contracts". The rules that must not be broken:
|
||||
|
||||
The map **markup + invocation** is shared via one partial:
|
||||
|
||||
- **Partial:** `user/themes/intotheeast/templates/partials/entry-map.html.twig`
|
||||
- **Used by:** `trip.html.twig` and the active branch of `home.html.twig` (both via `{% include ... with {...} only %}`)
|
||||
|
||||
It renders the `.home-map-col` column (map div `#{{ map_id }}` + fullscreen button) and, when `entries` is non-empty, a thin `<script>` that assigns `window.{{ map_global }}` from `initEntryMap`. Callers resolve header values (use_gpx / autoconnect) and pass them in.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
| 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'` |
|
||||
|
||||
The map globals `window.tripMap` / `window.homeMap` are asserted by the Playwright map specs, so any surface using this partial must keep assigning them.
|
||||
|
||||
> History: this replaced the old three-variant setup (a `feed-map.html.twig` partial with its own inline init, plus a full-page `map.html.twig`). Those were deleted in the 2026-07-04 standalone-page cleanup; the `2026-06-27-map-init-consolidation` plan had already moved trip + home onto `initEntryMap`.
|
||||
|
||||
### Shared trip-feed-col partial
|
||||
|
||||
The home page's active-trip view and the trip page render the **same feed-col chrome** (date-range header, filter bar, stats/cycling panels, feed loop) via one shared Twig partial. This is separate from the `entry-map` partial above — it is the column **beside** the map, not the map.
|
||||
|
||||
- **Partial:** `user/themes/intotheeast/templates/partials/trip-feed-col.html.twig`
|
||||
- **Used by:** `trip.html.twig` and the active branch of `home.html.twig` (both via `{% include ... with {...} only %}`)
|
||||
- **Sibling:** `partials/home-predeparture.html.twig` — the home-only "Coming soon" landing state. `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.
|
||||
|
||||
**Parameters (`trip-feed-col`):**
|
||||
|
||||
| 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, no sort button) |
|
||||
| `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`, and `.trip-header-banner` cover strip) that renders between the counts and the filter bar. `trip.html.twig` passes `true`; `home.html.twig` omits it so those extras never leak onto the home route (the `only` include keeps it off by default).
|
||||
|
||||
`home-predeparture` takes only `trip_page`.
|
||||
|
||||
**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` (rebuild with `make build-assets`; never hand-edit `js/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).
|
||||
- **`partials/entry-map.html.twig` is the only map path.** The engine is `MapUtils.initEntryMap(opts)` in `js/src/maplibre-utils.js`. Do not add a second map implementation — an older three-variant setup (`feed-map.html.twig`, full-page `map.html.twig`) was consolidated away on 2026-07-04.
|
||||
- **It must keep assigning `window.tripMap` / `window.homeMap`** — the Playwright map specs assert these globals.
|
||||
- **`partials/trip-feed-col.html.twig` is the feed column beside the map** (date-range header, filter bar, stats/cycling panels, feed loop). Its sibling `partials/home-predeparture.html.twig` is the home-only "Coming soon" state, selected by `home.html.twig` when `all_items` is empty. **Keep `trip-feed-col` single-purpose — do NOT fold the pre-departure branch back into it.**
|
||||
- **Stats glue:** `trip-feed-col` calls `window.initTripStats({…})`, one shared function in `js/src/main.js` that depends on `window.MapUtils` from `map.js`. Both load in the `bottom` asset group.
|
||||
|
||||
### GPX file management
|
||||
|
||||
GPX files are stored as page media on the trip page (`user/pages/01.trips/<slug>/`). They are picked up automatically by `trip.html.twig` (and `home.html.twig`) via `trip_page.media.all`, filtered to `.gpx`, and passed to the shared `entry-map` partial.
|
||||
GPX files are page media on the trip page (`user/pages/01.trips/<slug>/`), auto-detected via `trip_page.media.all` filtered to `.gpx` and passed to the `entry-map` partial — no manual linking. `.gpx` is registered in `user/config/media.yaml`.
|
||||
|
||||
The GPX manager page (`user/pages/03.gpx-manager/`) provides a browser UI at `/gpx-manager`:
|
||||
- **Auth:** enforced by Login plugin via `access.admin.login: true` in frontmatter — shows login form if not authenticated
|
||||
- **Template:** `user/themes/intotheeast/templates/gpx-manager.html.twig`
|
||||
- **API:** uses Grav API v1 with session cookie auth (`session_enabled: true` in `user/plugins/api/api.yaml`)
|
||||
- List: `GET /api/v1/pages{route}/media`
|
||||
- Upload: `POST /api/v1/pages{route}/media` (multipart)
|
||||
- Delete: `DELETE /api/v1/pages{route}/media/{filename}`
|
||||
- **Slugification:** filenames are slugified client-side before upload (spaces/special chars → hyphens, lowercase); the file is sliced to a plain `Blob` so the third argument to `FormData.append` is always used as the filename
|
||||
- **Media type:** `.gpx` is registered in `user/config/media.yaml` so Grav serves and tracks these files
|
||||
|
||||
To add GPX files without the browser UI, drop them directly into `user/pages/01.trips/<slug>/` and run `make content-push`.
|
||||
Manage them at `/gpx-manager` (admin login required; filenames auto-slugified on upload), or drop files into the trip folder and `make content-push`. Wiring details — API routes, session-cookie auth, the `Blob`/`FormData` upload gotcha — are in [`docs/guides/gpx-manager.md`](docs/guides/gpx-manager.md).
|
||||
|
||||
### Switching to a new trip
|
||||
|
||||
The active trip lives in **one** place now: `site.active_trip`. The post form no longer hardcodes a `pageconfig.parent` — the `cache-on-save` plugin derives the write target from `site.active_trip` at submit time (`onFormValidationProcessed` → `setData('parent', …)`), so there is nothing to keep in sync.
|
||||
|
||||
| File | Key | Example value | How to edit |
|
||||
|---|---|---|---|
|
||||
| `user/config/site.yaml` | `active_trip` | `/trips/italy-2027` | Admin → Configuration → Site → **Active Trip** (page-picker rooted at `/trips`; blueprint at `user/blueprints/config/site.yaml`) |
|
||||
|
||||
Note: `system.yaml` `home.alias` is permanently set to `/home` (the real home page) and does **not** need to change when switching trips.
|
||||
|
||||
After updating, also create the new trip's page tree under `user/pages/01.trips/<new-slug>/` with the two content subfolders `01.dailies/` and `04.stories/` (each with an inert `routable:false` container `.md`), plus the trip's `trip.md`. Do **not** recreate `02.map/` or `03.stats/` — those standalone views were retired.
|
||||
The active trip lives in **one** place: `user/config/site.yaml` → `active_trip`. **Never re-add a `pageconfig.parent` to `post-form.md`** — `cache-on-save` derives the write target from `site.active_trip` at submit time, and a static parent would override it and reintroduce the old silent-desync bug. Procedure and the new-trip page tree: [`docs/guides/trip-switching.md`](docs/guides/trip-switching.md).
|
||||
|
||||
### Environment
|
||||
|
||||
@@ -207,57 +126,13 @@ With these settings, Grav rebuilds templates on every request. This is intention
|
||||
|
||||
### Production mode (per-environment override)
|
||||
|
||||
Production needs different Twig settings than dev, but **never change the
|
||||
committed `user/config/system.yaml`** — `twig.cache: false` (and `debug`/
|
||||
`auto_reload: true`) are the *intended dev values*, and committing prod values
|
||||
there breaks local development for everyone.
|
||||
Prod needs different Twig settings than dev, but they are **never** committed to `user/config/system.yaml` — `twig.cache: false` and `debug`/`auto_reload: true` there are the *intended dev values*, and committing prod values breaks local development for everyone. Prod values live in the version-controlled `deploy/env/prod/system.yaml` and deploy to the server's `user/env/<hostname>/` tree via `make remote-apply-env-<env>`.
|
||||
|
||||
Instead, prod values are a **per-environment override** deployed to the server
|
||||
only, via Grav's per-environment config (`environment://config`, keyed on the
|
||||
request hostname):
|
||||
Mechanics, the settings table, and the re-apply-after-install rule: [`docs/guides/deploy-cycle.md`](docs/guides/deploy-cycle.md) → "The env override tree". Two things to carry in your head:
|
||||
|
||||
| Setting | Dev (committed) | Prod (override) | Why prod differs |
|
||||
|---|---|---|---|
|
||||
| `twig.cache` | `false` | `true` | Compile templates once and reuse |
|
||||
| `twig.debug` | `true` | `false` | No debug functions in prod |
|
||||
| `twig.auto_reload` | `true` | `false` | Don't stat templates every request |
|
||||
> **⚠️ Once `user/env/<hostname>/` exists, Grav's Admin saves ALL config there** — system *and* plugin. So (a) config edited via Admin **on the server is server-only** and silently never reaches Gitea or local (good for secrets, invisible to the repo); (b) when reading or writing server config, check **both** `user/config/…` and `user/env/<host>/config/…` — **env wins**, so tooling must look there first.
|
||||
|
||||
- **Source of truth:** `deploy/env/prod/system.yaml` (version-controlled).
|
||||
- **Deploy:** `make remote-apply-env-prod` — writes it to
|
||||
`<webroot>/user/env/<hostname>/config/system.yaml` and clears cache. It
|
||||
deep-merges over the committed `system.yaml`.
|
||||
- **Not synced by content:** `user/env/` is outside the content repo's tracked
|
||||
folders, so `content-push` / git-sync / `remote-fetch-content` do **not**
|
||||
restore it. **Re-run `make remote-apply-env-prod` after any fresh install.**
|
||||
- The hostname segment defaults to `REMOTE_HOST`; override with `WEB_HOST` in
|
||||
`.env.<env>` if Grav sees a different host than the SSH host.
|
||||
|
||||
> **⚠️ Once `user/env/<hostname>/` exists, Grav's Admin saves ALL config there.**
|
||||
> Creating the env override dir has a site-wide side effect: Grav's Admin panel
|
||||
> writes **every** config change (system *and* plugin) into the active
|
||||
> environment's config tree — e.g. editing a plugin on prod saves to
|
||||
> `user/env/intotheeast.com/config/plugins/<name>.yaml`, **not**
|
||||
> `user/config/plugins/<name>.yaml`. Consequences you must remember:
|
||||
> - Config edited via **Admin on the server is server-only**: `user/env/` is
|
||||
> outside the content repo's tracked folders, so it is **not committed** and
|
||||
> **not synced by git-sync** (which syncs only `pages`/`config`/`themes`).
|
||||
> Good for secrets — `git-sync.yaml` (token) safely lives at the env path —
|
||||
> but it means prod Admin config edits silently do **not** reach Gitea/local.
|
||||
> - When reading/writing server config, check **both** `user/config/...` and
|
||||
> `user/env/<host>/config/...` (env wins). Server tooling must search the env
|
||||
> path first — see `scripts/git-sync-toggle.sh` and `make remote-diag`.
|
||||
> - Repo-authored config (`user/config/...` via `make content-push`) still
|
||||
> applies everywhere; the env tree only holds per-host overrides + Admin-on-
|
||||
> server edits. Full details: `docs/working/git-sync-notes.md`.
|
||||
|
||||
**Pre-launch smoke test required:** with the prod override applied, submit one
|
||||
post via `/post` and confirm the entry appears in the trip page feed
|
||||
immediately. This verifies the cache-on-save plugin (BUG-001 fix) works
|
||||
correctly with caching enabled.
|
||||
|
||||
### What the cache-on-save plugin handles
|
||||
|
||||
The custom plugin at `user/plugins/cache-on-save/` clears Grav's page-tree cache on every `new-entry` form submission. This ensures new posts appear in the tracker feed immediately in both modes — it does not depend on whether Twig caching is on or off.
|
||||
**Pre-launch smoke test:** with the prod override applied, submit one post via `/post` and confirm it appears in the trip feed immediately — this proves `cache-on-save` works with caching on.
|
||||
|
||||
## 2. Local development setup
|
||||
|
||||
@@ -302,4 +177,3 @@ Every plan in `docs/working/plans/` must have a `**Status:**` line immediately a
|
||||
- **Auth is a dependency project.** `auth.setup.js` runs first and writes `tests/.auth/user.json`; the `chromium` project reuses it as `storageState`. Don't add per-test logins.
|
||||
- **Base URL:** defaults to `http://localhost:8081`; override with `GRAV_BASE_URL` (required when testing a worktree's isolated server on `8090+`).
|
||||
- Single spec / focused run: `npx playwright test tests/ui/maps` (add `--headed` to watch). `retries: 0` and screenshots-on-failure only, so a failure is a real failure.
|
||||
- **`window.tripMap` / `window.homeMap` are asserted by the map specs** — any surface using the `entry-map` partial must keep assigning them (see "One map path" above).
|
||||
|
||||
@@ -41,6 +41,52 @@ servers use. See `docs/solutions/tooling-decisions/upgrade-local-grav-core-rebui
|
||||
|
||||
---
|
||||
|
||||
## The env override tree (`user/env/<host>/`)
|
||||
|
||||
Prod needs different Twig settings than dev. These are **never** committed to
|
||||
`user/config/system.yaml` — `twig.cache: false` and `debug`/`auto_reload: true`
|
||||
are the *intended dev values*, and committing prod values there breaks local
|
||||
development for everyone. Instead they ship as a per-environment override via
|
||||
Grav's `environment://config`, keyed on the request hostname.
|
||||
|
||||
| Setting | Dev (committed) | Prod (override) | Why prod differs |
|
||||
|---|---|---|---|
|
||||
| `twig.cache` | `false` | `true` | Compile templates once and reuse |
|
||||
| `twig.debug` | `true` | `false` | No debug functions in prod |
|
||||
| `twig.auto_reload` | `true` | `false` | Don't stat templates every request |
|
||||
|
||||
- **Source of truth:** `deploy/env/prod/system.yaml` (version-controlled).
|
||||
- **Deploy:** `make remote-apply-env-prod` — writes it to
|
||||
`<webroot>/user/env/<hostname>/config/system.yaml` and clears cache. It
|
||||
deep-merges over the committed `system.yaml`.
|
||||
- **Hostname segment** defaults to `REMOTE_HOST`; override with `WEB_HOST` in
|
||||
`.env.<env>` if Grav sees a different host than the SSH host.
|
||||
- **Not restored by anything.** `user/env/` is outside the content repo's tracked
|
||||
folders, so `content-push` / git-sync / `remote-fetch-content` do **not** bring
|
||||
it back. **Re-run `make remote-apply-env-<env>` after any fresh install.**
|
||||
|
||||
### Side effect: Admin writes ALL config into the env tree
|
||||
|
||||
Once `user/env/<hostname>/` exists, Grav's Admin saves **every** config change
|
||||
(system *and* plugin) there — e.g. editing a plugin on prod writes
|
||||
`user/env/intotheeast.com/config/plugins/<name>.yaml`, **not**
|
||||
`user/config/plugins/<name>.yaml`. Consequences:
|
||||
|
||||
- Config edited via **Admin on the server is server-only**: the env tree is not
|
||||
committed and not synced by git-sync (which syncs only `pages`/`config`/
|
||||
`themes`), so prod Admin edits silently never reach Gitea or local. This is
|
||||
*good* for secrets — `git-sync.yaml` (token), the JWT and CSRF salt safely
|
||||
live there — but it means config drift is invisible to the repo.
|
||||
- When reading or writing server config, check **both** `user/config/…` and
|
||||
`user/env/<host>/config/…` (env wins). Server tooling must search the env path
|
||||
first — see `scripts/git-sync-toggle.sh` and `make remote-diag`.
|
||||
- Repo-authored config (`user/config/…` via `make content-push`) still applies
|
||||
everywhere; the env tree holds only per-host overrides + Admin-on-server edits.
|
||||
|
||||
Full details: `docs/working/git-sync-notes.md`.
|
||||
|
||||
---
|
||||
|
||||
## Phase 0 — Local (author + prove the change)
|
||||
|
||||
1. Make the change in the repo:
|
||||
|
||||
@@ -30,7 +30,7 @@ The GPX manager at `/gpx-manager` requires admin login (redirects to login form
|
||||
Drop the file directly into the trip folder and push:
|
||||
|
||||
```bash
|
||||
cp your-route.gpx /path/to/user/pages/01.trips/japan-korea-2026/
|
||||
cp your-route.gpx /path/to/user/pages/01.trips/denmark-2026/
|
||||
make content-push
|
||||
```
|
||||
|
||||
@@ -83,3 +83,25 @@ GPX files are registered as a valid media type in `user/config/media.yaml`, so G
|
||||
```
|
||||
|
||||
No manual linking is needed — upload and it appears.
|
||||
|
||||
---
|
||||
|
||||
## How the manager is wired
|
||||
|
||||
| Piece | Detail |
|
||||
|---|---|
|
||||
| Page | `user/pages/03.gpx-manager/` |
|
||||
| Template | `user/themes/intotheeast/templates/gpx-manager.html.twig` |
|
||||
| Auth | Login plugin, via `access.admin.login: true` in the page frontmatter — renders the login form when unauthenticated |
|
||||
| API | Grav API v1 with **session cookie** auth (`session_enabled: true` in `user/plugins/api/api.yaml`) |
|
||||
|
||||
API calls the page makes:
|
||||
|
||||
```
|
||||
GET /api/v1/pages{route}/media # list
|
||||
POST /api/v1/pages{route}/media # upload (multipart)
|
||||
DELETE /api/v1/pages{route}/media/{filename} # delete
|
||||
```
|
||||
|
||||
**Upload gotcha:** the selected file is sliced into a plain `Blob` before `FormData.append`, so the third argument is always honoured as the filename. Appending the original `File` lets the browser keep the unslugified name and the slugification is silently ignored.
|
||||
|
||||
|
||||
@@ -39,8 +39,8 @@ Two ways to post: the **mobile form** at `/post` (quick, phone-friendly) or the
|
||||
```
|
||||
Browser → /post (post-form.md)
|
||||
└─ Grav Form plugin validates fields
|
||||
└─ cache-on-save injects parent from site.active_trip
|
||||
└─ add-page-by-form plugin
|
||||
├─ reads pageconfig.parent (/trips/<active_trip>/dailies)
|
||||
├─ writes user/pages/01.trips/<active_trip>/01.dailies/<slug>/entry.md
|
||||
└─ moves uploaded photos into the page folder
|
||||
└─ cache-on-save plugin
|
||||
@@ -53,7 +53,7 @@ Example: `2026-07-20-0930-first-day-in-kyoto.entry`
|
||||
|
||||
**Entry folder structure:**
|
||||
```
|
||||
user/pages/01.trips/japan-korea-2026/01.dailies/
|
||||
user/pages/01.trips/denmark-2026/01.dailies/
|
||||
└─ 2026-07-20-0930-first-day-in-kyoto.entry/
|
||||
├─ entry.md ← frontmatter + markdown body
|
||||
├─ temple.jpg ← hero image (or set hero_image in frontmatter)
|
||||
@@ -103,7 +103,7 @@ Every entry supports these frontmatter fields:
|
||||
## Troubleshooting
|
||||
|
||||
**Entry doesn't appear in feed after submit**
|
||||
→ Check that `active_trip` in `user/config/site.yaml` matches the parent in `user/pages/02.post/post-form.md` (`pageconfig.parent`). If they're out of sync, entries go to the wrong folder. See [trip switching guide](trip-switching.md).
|
||||
→ Check `active_trip` in `user/config/site.yaml` — the write target is derived from it at submit time, so a wrong value sends entries to the wrong trip's dailies. See [trip switching guide](trip-switching.md).
|
||||
|
||||
**Get Weather button shows an error**
|
||||
→ Fill in Lat/Lng first (tap Get Location or enter manually). Open-Meteo requires coordinates.
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
# Switching to a New Trip
|
||||
|
||||
When you start a new trip, **two files must be updated together** — if only one is changed, new entries will be posted to the wrong folder silently (no error, wrong trip).
|
||||
The active trip lives in **one** place: `user/config/site.yaml` → `active_trip`. Set it, create the new page tree, push.
|
||||
|
||||
> **Changed 2026-07:** this used to require editing two files in lockstep (`site.yaml` **and** `post-form.md` → `pageconfig.parent`), and they silently desynced. The `cache-on-save` plugin now derives the write target from `site.active_trip` at submit time (`onFormValidationProcessed` → `setData('parent', …)`), so `post-form.md` no longer carries a `parent` at all. **Do not re-add one** — it would override the derived target and reintroduce the desync.
|
||||
|
||||
---
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] Update `user/config/site.yaml` → `active_trip`
|
||||
- [ ] Update `user/pages/02.post/post-form.md` → `pageconfig.parent`
|
||||
- [ ] Create the new trip page tree (see below)
|
||||
- [ ] Run `make content-push` to push the changes to production
|
||||
|
||||
@@ -15,38 +16,28 @@ When you start a new trip, **two files must be updated together** — if only on
|
||||
|
||||
## Step 1 — Update site.yaml
|
||||
|
||||
In `user/config/site.yaml`, set `active_trip` to the new trip slug:
|
||||
In `user/config/site.yaml`, set `active_trip` to the new trip's **route**:
|
||||
|
||||
```yaml
|
||||
active_trip: japan-korea-2026 # ← change this
|
||||
active_trip: /trips/denmark-2026 # ← change this
|
||||
```
|
||||
|
||||
The slug must exactly match the folder name under `user/pages/01.trips/`.
|
||||
The final segment must exactly match the folder name under `user/pages/01.trips/`.
|
||||
|
||||
You can also set this from Admin → Configuration → Site → **Active Trip** (a page-picker rooted at `/trips`; blueprint at `user/blueprints/config/site.yaml`).
|
||||
|
||||
> `system.yaml` → `home.alias` is permanently `/home` (the real home page) and does **not** change when switching trips.
|
||||
|
||||
---
|
||||
|
||||
## Step 2 — Update post-form.md
|
||||
|
||||
In `user/pages/02.post/post-form.md`, set `pageconfig.parent` to the new dailies path:
|
||||
|
||||
```yaml
|
||||
pageconfig:
|
||||
parent: /trips/japan-korea-2026/dailies # ← change this
|
||||
```
|
||||
|
||||
**Why both?** Grav's config and page frontmatter are static YAML — no variable substitution is possible, so `post-form.md` can't read from `site.yaml` automatically. They must match manually.
|
||||
|
||||
**What breaks if they're out of sync:** `active_trip` controls which trip page is featured on the home page and trip page. `pageconfig.parent` controls where new entries land. If they differ, new posts go to the old trip's dailies folder while the home page shows the new trip — entries appear to vanish.
|
||||
|
||||
---
|
||||
|
||||
## Step 3 — Create the new trip page tree
|
||||
## Step 2 — Create the new trip page tree
|
||||
|
||||
Create the two content subfolders under `user/pages/01.trips/<new-slug>/`:
|
||||
|
||||
```
|
||||
user/pages/01.trips/japan-korea-2026/
|
||||
user/pages/01.trips/denmark-2026/
|
||||
├─ trip.md ← title, date_start, date_end, cover_image, album_url
|
||||
├─ *.gpx ← route files (optional; page media, auto-detected)
|
||||
├─ 01.dailies/
|
||||
│ └─ dailies.md ← inert container: template: default, routable: false, visible: false
|
||||
└─ 04.stories/
|
||||
@@ -55,13 +46,13 @@ user/pages/01.trips/japan-korea-2026/
|
||||
|
||||
Copy these files from an existing trip and update the frontmatter (especially `title` and `date_start` in `trip.md`).
|
||||
|
||||
> The `02.map/` and `03.stats/` standalone views were retired (2026-07-04) — the map and stats render inline on the trip page. The `01.dailies/` and `04.stories/` folders now exist only as data containers holding the entry/story children; their own routes are non-routable.
|
||||
> The `02.map/` and `03.stats/` standalone views were retired (2026-07-04) — the map and stats render inline on the trip page. The `01.dailies/` and `04.stories/` folders now exist only as data containers holding the entry/story children; their own routes are non-routable. Do **not** recreate `02.map/` or `03.stats/`.
|
||||
|
||||
Fields in `trip.md` to update:
|
||||
|
||||
| Field | Example | Notes |
|
||||
|---|---|---|
|
||||
| `title` | `Japan & Korea 2026` | Displayed in nav and trip header |
|
||||
| `title` | `Denmark 2026` | Displayed in nav and trip header |
|
||||
| `date_start` | `2026-07-15` | Used for "X days on the road" stat |
|
||||
| `date_end` | *(leave blank while travelling)* | Set when you return |
|
||||
| `cover_image` | `cover.jpg` | Shown on the trips listing page |
|
||||
@@ -69,7 +60,7 @@ Fields in `trip.md` to update:
|
||||
|
||||
---
|
||||
|
||||
## Step 4 — Push
|
||||
## Step 3 — Push
|
||||
|
||||
```bash
|
||||
make content-push
|
||||
|
||||
@@ -8,8 +8,8 @@ How the intotheeast site hangs together.
|
||||
|
||||
| Layer | Technology | Notes |
|
||||
|---|---|---|
|
||||
| CMS | Grav 2.0.4 stable | Flat-file PHP CMS; no database. Server upgrades in place via `bin/gpm self-upgrade` |
|
||||
| Admin | Admin2 v2.0.10 | Plugin slug: `admin2` (not `admin`) |
|
||||
| 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.yaml` → `gpm.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` |
|
||||
@@ -29,15 +29,19 @@ Browser POST /post
|
||||
├─ Grav Form plugin (built-in)
|
||||
│ └─ validates required fields; handles file uploads
|
||||
│
|
||||
├─ add-page-by-form (third-party, patched)
|
||||
├─ 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.parent → target folder (e.g. /trips/japan-korea-2026/dailies)
|
||||
│ ├─ pageconfig.slug_field → slug from date + title
|
||||
│ └─ pagefrontmatter → template: entry, published: true
|
||||
│ └─ 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 (custom, user/plugins/cache-on-save/)
|
||||
└─ 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
|
||||
```
|
||||
@@ -49,13 +53,15 @@ Other notable plugins:
|
||||
| `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.txt` → `make 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`. Versioned in the user repo.
|
||||
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.
|
||||
|
||||
---
|
||||
@@ -77,17 +83,62 @@ templates/
|
||||
|
||||
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), and `weather-icons.html.twig`.
|
||||
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.yaml` → `active_trip`.
|
||||
The site is organized around Trip entities. The active trip is set in `user/config/site.yaml` → `active_trip`, as a **route** (e.g. `/trips/denmark-2026`), not a bare slug.
|
||||
|
||||
```
|
||||
user/pages/01.trips/
|
||||
└─ japan-korea-2026/
|
||||
└─ 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)
|
||||
@@ -126,18 +177,20 @@ Rendered as route polyline on map
|
||||
```
|
||||
1. User fills /post form and taps Submit
|
||||
2. Grav Form plugin validates: title and content required
|
||||
3. add-page-by-form reads post-form.md:
|
||||
pageconfig.parent: /trips/japan-korea-2026/dailies
|
||||
pageconfig.slug: {date}-{title|slugify}
|
||||
pagefrontmatter: template: entry, published: true
|
||||
4. New page written to:
|
||||
user/pages/01.trips/japan-korea-2026/01.dailies/
|
||||
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
|
||||
5. Photos moved into the same folder
|
||||
6. cache-on-save calls $grav['cache']->deleteAll()
|
||||
7. Browser: form shows success message
|
||||
8. Feed at /trips/japan-korea-2026 immediately shows new entry
|
||||
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
|
||||
```
|
||||
|
||||
---
|
||||
@@ -146,7 +199,7 @@ Rendered as route polyline on map
|
||||
|
||||
| File | Purpose |
|
||||
|---|---|
|
||||
| `user/config/site.yaml` | `active_trip` slug; site title/description |
|
||||
| `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 |
|
||||
|
||||
Reference in New Issue
Block a user