m038andClaude Opus 5 d946eaaa7e docs(solutions): compound the doc-reconciliation learnings; grow CONCEPTS.md
Captures the durable learnings from the 2026-07-25 whole-repo documentation
reconciliation as a knowledge-track learning, plus the vocabulary it introduced.

New learning — conventions/reconciling-drifted-docs-tense-tiering-and-a-supersession-ledger.md:

- Tier docs by TENSE, not only by content type. claude-md-content-tiering.md
  established "descriptions drift, rules don't" and tiered by content type; that
  axis could not say what to do with 41 completed plans, which are neither rules
  nor current descriptions. Present-tense docs are defects when stale; past-tense
  records are supposed to be stale and get annotated, never rewritten.
- Ledger AND inline notes, because each covers the other's failure: a ledger
  alone is a pointer you may not follow, inline notes alone give no changelog
  view. Prefer annotation patterns the repo already uses.
- Record what was NOT reversed, or a ledger of only reversals makes every old
  doc look suspect and settled decisions get re-litigated.
- Separate "docs are wrong" from "code is wrong" — route code-side findings to a
  recommendations doc so a docs diff stays reviewable.
- Verify against the artifact that decides behaviour: the Makefile for commands
  (including macro-generated targets a grep misses), the build script for
  outputs, imports for source-vs-output, branch history for whether a plan
  shipped. An audit that never withdraws a finding has not been checking itself
  — one finding here was withdrawn after reading package.json.
- Audit the state that actually runs: a fresh worktree checks out the submodule
  PIN, which lagged real HEAD and would have hidden a whole merged feature.

Three structural lessons in "Why This Matters":

- An index describing another document's role is a factual claim that can rot,
  and it is worse than the stale document itself — it defeats the reader's
  judgement before it engages. This was the tree's single most misleading line.
- Wrong beats absent again, now for commands: README's server runbook documented
  every remote-* target without the -test/-prod suffix guard-env requires.
  deploy-cycle.md had it right — the defect was a second copy drifting.
- Promoting a doc to "the authoritative list of X" creates a completeness
  obligation it did not have as prose, and nothing enforces it.
- A removal is not finished when the code is gone, but when every consumer and
  every description of it is gone — travel-memories left a compose service
  behind, hidden by a cached Docker image. Local state can mask a breakage
  indefinitely, so "it works here" is not evidence.

Overlap with conventions/claude-md-content-tiering.md scored MODERATE (2 of 5
dimensions: same root-cause thesis, overlapping files; different tiering axis
and different prevention), so a new doc was written rather than folding into it.
Flagged in the Related section as a consolidation candidate if a third
documentation learning appears.

CONCEPTS.md — new Documentation cluster (Historical record, Superseded decision,
Plan status) and one flagged ambiguity recording that a present-tense historical
record is not a claim about the current system. These three are now referenced by
CLAUDE.md and both doc READMEs, so they needed defining.

Discoverability check: no edit needed — CLAUDE.md's entry-point table already
surfaces docs/solutions/ with its frontmatter fields and CONCEPTS.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 00:18:36 +02:00

into the east — Grav CMS

Grav CMS travel blog. Local dev via Docker; production on a VPS managed entirely through make.


Repository structure

Two git repos:

Repo Contents Location
intotheeast.com (this repo) Docker setup, Makefile, scripts, tests, docs, plugins.txt ./
intotheeast.com-content Site config, pages, theme user/ (git submodule)

user/ is tracked by this repo as a git submodule — it has its own Gitea remote and its own push/pull cadence (make content-push / make content-pull), and this repo pins an exact user/ commit. The Git Sync plugin on the server pulls from Gitea automatically when content is pushed. A persistent M user / m user in git status is normal, not a problem; see docs/solutions/architecture-patterns/dual-repo-submodule-workflow.md.

Folder map

Path Contents
user/ Site content, config, pages, theme (the content submodule)
user/themes/intotheeast/js/src/ JS sources — esbuild inputs; run make build-assets after editing. Note js/maplibre-utils.js and js/nav.js are also sources, despite sitting beside the generated bundles
deploy/env/ Per-environment Grav config overrides (e.g. prod Twig settings)
deploy/patches/ Tracked patches for third-party plugins, which are otherwise git-ignored
scripts/ Server install and maintenance scripts
tests/ Playwright suite — see docs/reference/testing.md
php/ Local PHP ini overrides
docs/ All project documentation — start at docs/README.md
docs/guides/ Operational how-tos (posting, GPX, trip switching, setup, deploy cycle)
docs/reference/ Stable facts: architecture, design system, testing
docs/solutions/ Write-ups of bugs and workflow traps already hit, with YAML frontmatter (module, tags, problem_type)
docs/working/ Specs, plans, backlog, QA — work in flight
CONCEPTS.md Shared domain vocabulary (Trip, Entry, Story, Active Trip)
CLAUDE.md Rules and gotchas loaded into every Claude Code session

Prerequisites

  • Docker (for local dev)
  • SSH access to the production server
  • Both Gitea repos created and accessible
  • A Gitea personal access token with repo read/write access

Local development setup

cp .env.example .env        # fill in your values — never commit this file
git submodule update --init user
mkdir -p user/plugins user/data
make build && make start-grav && make install-plugins && make fix-perms

Site runs at http://localhost:8081.

user/ is a git submodule — initialise it with git submodule update --init user. Do not git clone into user/ by hand; that detaches it from the pin the outer repo tracks.

⚠️ Use make start-grav, not make setup, on a clean checkout. make setup runs make start (docker compose up -d), which still tries to build the travel-memories service — but its source was moved to a separate project (a80b0a9) and services/ is gitignored, so the build context is missing and the command fails. make start-grav brings up Grav only. Machines with a cached travel-memories image will not see this until their next rebuild. See docs/reference/superseded-decisions.md → R11.


First-time server setup

1. Fill in .env — copy .env.example and set all values including REMOTE_USER, REMOTE_HOST, USER_REPO, MAIN_REPO, and Gitea credentials.

2. Run the install:

make remote-install-prod     # or -test

This SSHes into the server, downloads Grav, clones both repos (user content + this config repo), installs plugins, and prints the server's SSH public key.

3. Add the SSH key to Gitea — copy the printed public key and add it as a read-only deploy key to both Gitea repos. After this, make remote-fetch-prod works without credentials.


Content sync workflow

To pull editor changes locally:

make content-pull   # pull latest user/ content from Gitea → local

To push local changes to Gitea (triggers server sync):

git -C user add -A && git -C user commit -m "content: describe change"
make content-push   # push local user/ commits → Gitea

All commands

Local

Command Description
make setup First run: build → start → install plugins → fix perms. ⚠️ Currently fails on a clean checkout — see the setup note above; use the start-grav sequence instead
make start Start all compose services. ⚠️ Fails where services/travel-memories is absent
make start-grav Start the Grav service only — the reliable option
make stop Stop the local Docker container
make install-plugins (Re)install plugins from plugins.txt, then apply local plugin patches
make apply-plugin-patches Idempotently re-apply the patches in deploy/patches/
make fix-perms Reset file ownership inside the container
make build-assets Run esbuild over user/themes/intotheeast/js/src/required after editing any JS source
make content-push Push local user/ commits to Gitea (triggers the production pull)
make content-pull Pull latest user/ content from Gitea

Testing

Command Description
make test Everything: test-configtest-posttest-ui
make test-config Form/config sanity checks
make test-post End-to-end post submission
make test-ui Playwright suite

Details and conventions: docs/reference/testing.md.

Demo content and imports

Command Description
make demo-load Copy every fixture trip under user/docs/demo/trips/ into the pages tree (add a fixture by dropping a folder there — no Makefile edit needed)
make demo-reset Remove those demo trips from the pages tree and clear cache
make pixelfed-import Import posts from Pixelfed via scripts/pixelfed-import.py

Parallel work

Command Description
make worktree-new NAME=<feature> Create a worktree with its own user/ checkout and an isolated dev server on port 8090+
make worktree-rm NAME=<feature> Tear one down cleanly (compose down → submodule deinit → worktree remove → prune)

Remote targets — every one needs an environment suffix

All remote-* targets require -test or -prod. A bare make remote-fetch fails via guard-env with "no environment. Use an env-suffixed target". The suffixed variants are generated by a macro in the Makefile, so they will not show up in a grep for literal target names.

The runbook for shipping a change through test → prod is docs/guides/deploy-cycle.md. The tables below are the inventory.

Credentials — always run remote-env-remove-<env> when done; credentials must not persist on the server.

Command Description
make remote-env-setup-<env> Write Gitea credentials to ~/.env-intotheeast on the server
make remote-env-remove-<env> Delete ~/.env-intotheeast from the server
make remote-secrets-audit-<env> Check the server for exposed secrets
make remote-seed-api-salt-<env> Generate the API/CSRF salt on the server

Install and sync

Command Description
make remote-install-<env> First-time install: download Grav, clone both repos, install plugins
make remote-fetch-<env> Pull latest config repo (Makefile, scripts, plugins.txt) on the server
make remote-fetch-content-<env> Pull latest user/ content on the server
make remote-content-status-<env> Show the server's content-repo state
make remote-apply-env-<env> Apply deploy/env/<env>/ config into the server's env tree — re-run after any fresh install
make remote-apply-plugin-patches-<env> Re-apply deploy/patches/ on the server

Plugins and core

Command Description
make remote-install-plugins-<env> Install plugins from local plugins.txt on the server
make remote-update-plugins-<env> Update installed plugins via GPM
make remote-gpm-install-<env> Install a single plugin via GPM
make remote-upgrade-grav-<env> Upgrade Grav core on the server (in place — servers have no image)

Operations

Command Description
make remote-clean-<env> Clear Grav cache on the server
make remote-warmup-<env> Clear and warm the cache after a deploy
make remote-maintenance-on-<env> Enable maintenance mode (visitors see offline page)
make remote-maintenance-off-<env> Disable maintenance mode
make remote-diag-<env> Diagnostics on the server
make remote-git-sync-enable-<env> / -disable-<env> Toggle the remote-only git-sync plugin
make remote-wipe-<env> ⚠️ Destroy the server install

Typical upgrade workflow

Run against test first — it is a full dress rehearsal of prod.

make remote-maintenance-on-prod
make remote-upgrade-grav-prod
make remote-install-plugins-prod
make remote-apply-env-prod        # env tree is not restored by anything else
make remote-warmup-prod
make remote-maintenance-off-prod

Plugins

Plugins are not committed to git. The full list is in plugins.txt — one plugin name per line.

  • Locally: make install-plugins
  • On server: make remote-install-plugins

Template behaviour

Key design decisions that affect how pages render:

Context Sort order Reason
Trip page (trip.html.twig) Ascending (oldest first) Trip reads as a narrative from start to finish
Homepage active-trip feed (home.html.twig) Descending (newest first) Visitors want to see what's happening right now

Homepage modes — controlled by travelling in user/config/site.yaml:

travelling Homepage shows
true Active trip map + chronological feed (newest first)
false Map with highlight markers + curated highlights grid (max 6, 1 per trip, random)

Entries and stories opt into the highlights grid via featured: true in their frontmatter. The active_trip field stores a full page route (e.g. /trips/italy-2026-demo), not a bare slug.

Per-trip map settings — configurable in Admin2 under the Trip tab:

Setting Values Default Notes
use_gpx Yes / No Yes Draws uploaded GPX files as route lines on the map
autoconnect off / on / manual / intelligent_gpx on Controls connector lines between location markers

Connect markers behaviour:

Value Behaviour
off No connector lines; force_connect on entries is also ignored
on Dashed connector between every entry in date order
manual No automatic lines; only entries with force_connect: true are linked
intelligent_gpx Suppresses the connector where a GPX track covers the route; force_connect overrides. Requires use_gpx enabled — falls back to on if GPX is off or no files are present

use_gpx and autoconnect are independent: you can show GPX tracks without connector lines or vice versa.


Security

  • .env is gitignored. Never commit it — it contains your server credentials and Gitea token.
  • GITEA_TOKEN exists only in .env locally, and in ~/.env-intotheeast on the server only during active sessions. Always run make remote-env-remove after use.
  • ~/.env-intotheeast has chmod 600 — readable only by the SSH user.
  • The server pulls from Gitea using its SSH deploy key (read-only). No long-lived token is stored on the server after initial install.
  • scripts/server-install.sh writes ~/.netrc for the initial clone only, and deletes it immediately after via a trap handler — even if the script fails.
  • Credentials are never passed as command-line arguments (they would appear in server process listings). They are passed as environment variables within the SSH session.
S
Description
No description provided
Readme
1.5 MiB
Languages
JavaScript 81.7%
Makefile 10.4%
Shell 6.3%
Python 1.2%
Dockerfile 0.4%