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>
180 lines
17 KiB
Markdown
180 lines
17 KiB
Markdown
# CLAUDE.md
|
|
|
|
## 0. Project specifics
|
|
|
|
**Only ever write changes in this folder (travel-blog-intotheeast/) or its subfolders.**
|
|
|
|
### Folder explanation
|
|
|
|
- **./**: Grav CMS dev environment for intotheeast travel blog
|
|
- **scripts/**: Server install and maintenance scripts
|
|
- **user/**: Site content, config, pages, and theme — its own git repo (`intotheeast-com-content.git`), tracked by the outer repo as a **git submodule** (pinned commit). See "Dual-repo submodule structure" below and `docs/solutions/architecture-patterns/dual-repo-submodule-workflow.md`
|
|
- **docs/**: All plans, specs, and project documentation (moved here from `user/docs/` on 2026-06-19)
|
|
- **docs/solutions/**: documented solutions to past problems (bugs, patterns, workflow gotchas), organized by category with YAML frontmatter (`module`, `tags`, `problem_type`). Relevant when implementing or debugging in a documented area
|
|
- **CONCEPTS.md** (repo root): shared domain vocabulary (Trip, Entry, Story, Active Trip). Relevant when orienting to the codebase or discussing domain concepts
|
|
|
|
### Current stack
|
|
|
|
- **Grav:** 2.0.7 stable (baked into the custom Docker image via `Dockerfile`; server upgrades in place via `bin/gpm self-upgrade`)
|
|
- **Admin:** Admin2 v2.0.12 (plugin slug: `admin2`, NOT `admin`)
|
|
- **GPM channel:** `stable` — set in `user/config/system.yaml` → `gpm.releases` (authoritative). `GRAV_CHANNEL=production` in `docker-compose.yml` is cosmetic/consistency only
|
|
- **Plugin management:** `admin2`, `api`, and `flex-objects` are now **GPM-managed via `plugins.txt`** (installed by `make install-plugins`), no longer hand-extracted from the core bundle. `git-sync` stays **remote-only** — never in `plugins.txt`
|
|
- **Docker image:** `getgrav/grav` with `GRAV_CHANNEL=production`
|
|
- **PHP session:** `session.save_path = /tmp` set in `php/php-local.ini`
|
|
|
|
### Dev server
|
|
|
|
The Docker dev server runs at **http://localhost:8081** (mapped from container port 80 in `docker-compose.yml`). A second service, `travel-memories`, runs at **http://localhost:8082**. Both ports and the container name are overridable via `GRAV_PORT` / `TM_PORT` / `GRAV_CONTAINER` — a worktree's `.worktree-env` sets these so isolated servers never collide (see "Dual-repo submodule structure").
|
|
|
|
### Local dev commands
|
|
|
|
`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/`** — 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): **`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
|
|
|
|
Third-party plugins live in the **git-ignored** `user/plugins/`, so local fixes to them do not travel with the content repo and are **overwritten by `make install-plugins`** or a fresh image build. Keep the fix as a tracked patch in `deploy/patches/` instead:
|
|
|
|
- `make apply-plugin-patches` — idempotent `git apply` (skips already-applied patches). `make install-plugins` runs it automatically as its last step
|
|
- `make remote-apply-plugin-patches-test` / `-prod` — piped over SSH into `patch -p1 --forward`; also runs automatically after a remote plugin install
|
|
- Details and the current patch list: `deploy/patches/README.md`
|
|
|
|
### Trip entity architecture
|
|
|
|
The site is structured around Trip entities. Key facts:
|
|
- Active trip is set in `user/config/site.yaml` → `active_trip` (currently `/trips/denmark-2026`). The value is a **route**, not a bare slug
|
|
- Trip pages live at `user/pages/01.trips/<slug>/`
|
|
- Each trip has two content subfolders: `01.dailies/` (journal entries) and `04.stories/` (stories). The former `02.map/` and `03.stats/` standalone views were **removed** (2026-07-04, see `docs/working/plans/2026-07-04-standalone-page-cleanup.md`) — map and stats now render inline on the trip page
|
|
- `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 "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
|
|
|
|
### Two shared partials — the rules
|
|
|
|
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:
|
|
|
|
- **`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 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`.
|
|
|
|
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: `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
|
|
|
|
**Never read `.env`, `.env.prod`, or `.env.test`** — they contain sensitive credentials. You may pass them to commands (e.g. `docker compose`, `make`) but never read their contents directly. Ask the user if you need environment-specific information.
|
|
|
|
### Remote operations
|
|
|
|
Always use `make` commands for anything on the production server (`make remote-install-plugins`, `make remote-clean`, etc.) — never SSH directly since credentials live in `.env`. If a remote operation isn't covered by an existing `make` command, either ask the user to run it manually or suggest adding a new `make` command if it seems reusable.
|
|
|
|
For a full upgrade/deploy through local → test → prod (ordered steps, smoke checklist, rollback), follow the runbook at [`docs/guides/deploy-cycle.md`](docs/guides/deploy-cycle.md).
|
|
|
|
### Content sync
|
|
|
|
- `make content-push` — commit and push `user/` to Gitea (triggers production pull via webhook)
|
|
- `make content-pull` — pull latest from Gitea to local
|
|
- `plugins.txt` is manually maintained — installing a plugin via Admin does NOT update it
|
|
- `make demo-load` — load **every** fixture trip under `user/docs/demo/trips/` into the pages tree (currently `italy-2026-demo` and `no-photos-demo`). Add a new fixture by dropping a trip folder there; no Makefile edit needed
|
|
- `make demo-reset` — remove the demo trips' pages folders and clear cache (full reset; re-run `demo-load` to restore)
|
|
- `make pixelfed-import` — import posts from Pixelfed via `scripts/pixelfed-import.py`
|
|
|
|
### User repo gitignore
|
|
|
|
Only these folders are tracked in the `user/` Git repo: `pages/`, `config/`, `accounts/`, `themes/`. The `plugins/` and `data/` folders are excluded — **except** the three site-owned plugins, which are un-ignored explicitly (see "Custom plugins" below). Also ignored: the test accounts, `italy-2026-demo` pages, secrets (`config/plugins/git-sync.yaml`, `config/security.yaml`, `api-private.php`), and the whole `env/` override tree.
|
|
|
|
### Dual-repo submodule structure
|
|
|
|
`user/` is a **git submodule** of the outer repo (`.gitmodules` at the root; git dir absorbed into `.git/modules/user`). Full workflow: `docs/solutions/architecture-patterns/dual-repo-submodule-workflow.md`. The essentials:
|
|
|
|
- **Two repos, two cadences.** Outer repo = dev environment (tests/docs/scripts/Docker). `user/` = content + theme, with its own remote and `make content-push` cadence. The outer repo pins an exact `user/` commit via the `user` gitlink.
|
|
- **Pointer-bump convention.** Routine content changes → **do not** bump the pin (leave it stale; harmless). At the **end of a cross-repo feature** → bump the pin once to the finished `user/` commit. Pin a commit reachable from `user/`'s published `main` (prefer the merge-to-main commit, not a squash-away branch tip), and **push `user/` before the outer repo** (superproject references a child SHA that must already exist upstream). The pin is dev-side coordination only — production pulls `user/` via the content webhook independently.
|
|
- **`M user` / `m user` is normal.** `M` = pin differs from `user/` HEAD (bump pending/intentional). `m` = submodule working tree dirty (e.g. local-testing `config/site.yaml`). Neither is an error — do not "fix" them by committing the gitlink or the `site.yaml`.
|
|
- **Worktrees for parallel work — use the make targets, don't do it by hand.** `make worktree-new NAME=<feature>` (from the main checkout) creates the outer worktree off `main`, initialises its own `user/` submodule, branches both, and starts an **isolated** dev server (own container name + auto-assigned port `8090+`, persisted in a git-ignored `.worktree-env` so every `make`/compose command in that worktree targets its own server). `make worktree-rm NAME=<feature>` tears it down cleanly (compose down → `submodule deinit` → `worktree remove` → `prune`) — skipping the deinit is what leaves orphaned `.worktrees/` dirs. Worktrees live under `.worktrees/` (excluded via `.git/info/exclude`). A fresh worktree's `user/` is empty until the submodule init runs, and `M user`/`m user` is normal (see above) — do not "fix" either. To add a commit to `main` while the main checkout is on another branch, use a throwaway `main` worktree rather than `git checkout main`.
|
|
|
|
## 1. Environment modes
|
|
|
|
### Rule: do not switch modes during development
|
|
|
|
**Never toggle between development and production mode mid-session.** If a caching or config issue appears, fix it at the application level (plugin, template logic) rather than temporarily flipping a mode flag to work around it. Mode switches introduce inconsistent state and make bugs harder to reproduce.
|
|
|
|
### Development mode (current)
|
|
|
|
Active settings in `user/config/system.yaml`:
|
|
|
|
| Setting | Dev value | Why |
|
|
|---|---|---|
|
|
| `twig.cache` | `false` | Theme file edits take effect immediately; no stale compile errors |
|
|
|
|
With these settings, Grav rebuilds templates on every request. This is intentionally slower but means you never need to flush cache after editing a `.html.twig` file.
|
|
|
|
### Production mode (per-environment override)
|
|
|
|
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>`.
|
|
|
|
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:
|
|
|
|
> **⚠️ 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.
|
|
|
|
**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
|
|
|
|
Full setup guide: [`docs/guides/local-setup.md`](docs/guides/local-setup.md)
|
|
|
|
### Superpowers skill paths
|
|
|
|
Specs: `docs/working/specs/YYYY-MM-DD-<topic>-design.md`
|
|
Plans: `docs/working/plans/YYYY-MM-DD-<topic>.md`
|
|
|
|
The brainstorming and writing-plans skills default to `docs/superpowers/`; these lines override that default.
|
|
|
|
### Plan status convention
|
|
|
|
Every plan in `docs/working/plans/` must have a `**Status:**` line immediately after the title heading:
|
|
|
|
| Status | Meaning |
|
|
|---|---|
|
|
| `📋 Not started` | Plan written; work not yet begun |
|
|
| `🔄 In progress — <note>` | Actively being worked on |
|
|
| `⏸️ Deferred — <reason>` | Intentionally postponed |
|
|
| `✅ Complete (YYYY-MM-DD)` | Done |
|
|
| `❌ Abandoned — <reason>` | Won't implement |
|
|
|
|
**When asked what's open:** surface `Not started` and `In progress` plans. Show `Deferred` plans but label them clearly. Omit `Complete` and `Abandoned` unless explicitly asked.
|
|
|
|
**When finishing a plan:** update the `**Status:**` field in the plan file to `✅ Complete (YYYY-MM-DD)` before closing the session. This applies whether execution was done by Claude directly, via the superpowers:executing-plans skill, or via superpowers:subagent-driven-development.
|
|
|
|
## 3. Testing
|
|
|
|
**The dev server must be running** (`make start`) — every suite drives the live site over HTTP.
|
|
|
|
| Command | Scope |
|
|
|---|---|
|
|
| `make test` | Everything: `test-config` → `test-post` → `test-ui` |
|
|
| `make test-config` | Form/config sanity via `scripts/test-form-config.sh` |
|
|
| `make test-post` | End-to-end post submission via `scripts/test-post.sh` |
|
|
| `make test-ui` | Playwright suite (`npx playwright test`) |
|
|
|
|
- **Test account is automatic.** `test-post` and `test-ui` depend on `test-account`, which creates a `testrunner` admin (password `Testpass1234`) inside the container if absent. It is git-ignored — never commit it, and keep the password free of shell/Make/URL-special characters since several consumers interpolate it.
|
|
- **Playwright layout:** config at `playwright.config.js`, specs under `tests/ui/` (`a11y`, `auth`, `dailies`, `gpx`, `home`, `maps`, `nav`, `post`, `stories`, `trip`), shared helpers in `tests/ui/helpers.js`, global setup/teardown in `tests/`.
|
|
- **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.
|