main advanced 13 commits while this documentation audit ran — the location-override work was merged into the outer repo — and it independently fixed two of the audit's findings. Without this merge the branch would have REVERTED them, which is the worst outcome for a cleanup pass because it arrives disguised as an improvement. Both conflicts resolved in main's favour; main's wording was better informed: - CLAUDE.md, the single-map-path rule: main's carve-out (829325c) states the exception as its own top-level bullet, names MAP_STYLE as the one shared thing, and spells out both prohibitions ("do not fold it into initEntryMap", "do not add a third path"). Taken verbatim over the version drafted here. - 2026-07-23-post-form-location-override.md: main (a517331) had already set the status to Complete, with far richer detail — the multi-agent review findings, the green-run numbers, the DEL4 regression still open, and the merge SHAs. Taken in full; the audit's claim that the status "lagged" was dropped, since it was true only of this branch's older branch point. Submodule pin: main bumped user/ to dd19995 and this merge preserves that. The audit's own no-gitlink-commit discipline applies to bumping the pin as a side effect of routine work, not to discarding a bump main already made. main touched none of the other nine corrected documents, so the remaining 18 findings stand unchanged. Audit notes corrected to match reality rather than left overstated: - superseded-decisions.md R13 now dates the carve-out to 2026-07-24 (829325c) rather than implying this pass introduced it. - The reconciliation spec gains an "audit baseline moved twice" section: the submodule pin lagged real HEAD, and then the base branch advanced mid-audit. - The compounded learning's section 6 is rewritten from "audit the current state" to "re-check the baseline before publishing, not only before starting", with the two habits that actually follow: merge the base branch in before opening the PR and read conflicts as findings, and when the incoming version is better, take it wholesale. An audit has no special authority over the work it audits. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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, notmake setup, on a clean checkout.make setuprunsmake start(docker compose up -d), which still tries to build thetravel-memoriesservice — but its source was moved to a separate project (a80b0a9) andservices/is gitignored, so the build context is missing and the command fails.make start-gravbrings up Grav only. Machines with a cachedtravel-memoriesimage will not see this until their next rebuild. Seedocs/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-config → test-post → test-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-testor-prod. A baremake remote-fetchfails viaguard-envwith "no environment. Use an env-suffixed target". The suffixed variants are generated by a macro in theMakefile, 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
.envis gitignored. Never commit it — it contains your server credentials and Gitea token.GITEA_TOKENexists only in.envlocally, and in~/.env-intotheeaston the server only during active sessions. Always runmake remote-env-removeafter use.~/.env-intotheeasthaschmod 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.shwrites~/.netrcfor the initial clone only, and deletes it immediately after via atraphandler — 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.