# 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 | 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 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. ### 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 | ### 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//` - 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//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) - 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 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. 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 `