Two-part handover following the repo's existing convention: Part A for a fresh Claude session, Part B for the owner. Part A leads with the trap rather than the deliverables, because it is the thing most likely to cause damage: main advanced 13 commits mid-audit and had already fixed two of the audit's findings, so this branch would have reverted them without the merge. Includes the fetch-and-merge commands to re-check the baseline if main has moved again overnight. Also records, so a future session does not undo them: - The tense split — past-tense records are annotated, never rewritten. "Finishing the job" by rewriting the milestone docs or the 41 completed plans is the wrong instinct and destroys the audit trail. - One finding was WITHDRAWN during the audit (the asset table lists esbuild entry points, so imported-only sources are correctly absent). Adding them back would reintroduce a non-defect. - The known false-positive link-check hit inside a ```diff fence. - The submodule invariant: git diff main..HEAD -- user must stay empty. An earlier git reset -- user here silently reverted main's pin bump; that was caught, and the check is written down so it is not re-broken. - P5's booby trap: removing shortcode-gallery-plusplus may take shortcode-core with it and break stories. Part B is the two things only the owner can do: one command to open the PR (tea needs an interactive TTY), and the 7 recommendation decisions with why each needs a human. Notes that the PR body file lives in a Claude job dir and is reconstructable from the spec if it has been cleaned up. 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.