Files
intotheeast-com/CLAUDE.md
T
m038andClaude Opus 4.8 7984b3a75e docs: sync CLAUDE.md + reference/guides with standalone-page cleanup
- Replace 'Shared feed-map partial' section with 'entry-map' partial + the
  single MapUtils.initEntryMap map path
- Trip architecture: two content subfolders (dailies/stories as inert
  routable:false containers); map/stats inline on trip page
- Fix stale references to deleted map/stats/dailies/stories templates in
  architecture.md (template list, page tree, GPX flow) and trip-switching.md
  (scaffold + verify steps)
- Correct GPX rendering: toGeoJSON bundled in map.js, no leaflet CDN
- Pre-launch smoke test now checks the trip page feed, not /dailies

Left docs/working/* (milestones, learnings, QA, plans) as point-in-time history.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 14:06:55 +02:00

199 lines
13 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 (standalone git repo — do not modify from here)
- **docs/**: All plans, specs, and project documentation (moved here from `user/docs/` on 2026-06-19)
### Current stack
- **Grav:** 2.0.0-rc.10 (baked into the custom Docker image via `Dockerfile`)
- **Admin:** Admin2 v2.0.0-rc.15 (plugin slug: `admin2`, NOT `admin`)
- **Docker image:** `getgrav/grav` with `GRAV_CHANNEL=beta`
- **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`).
### Trip entity architecture
The site is structured around Trip entities. Key facts:
- Active trip is set in `user/config/site.yaml``active_trip: japan-korea-2026`
- 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
- Post form parent (`post-form.md``pageconfig.parent`) **must be kept in sync** with `active_trip`
- 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 `<script>` that assigns `window.{{ map_global }}` from `initEntryMap`. Callers resolve header values (use_gpx / autoconnect) and pass them in.
**Parameters:**
| Parameter | Type | Trip passes | Home passes |
|---|---|---|---|
| `map_id` | string | `'trip-map'` | `'home-map'` |
| `map_global` | string | `'tripMap'` | `'homeMap'` |
| `entries` | array | `[{lat, lng, slug, title, url, type?, force_connect, ...}]` | same |
| `card_prefix` | string | `'entry-'` | `'entry-'` |
| `story_markers` | bool | `true` (diamond markers) | `false` |
| `gpx_urls` | array | `gpx_urls` | `home_gpx_urls` |
| `use_gpx` | bool | `page.header.use_gpx ?? true` | derived from `trip.header` |
| `autoconnect` | string | `page.header.autoconnect ?? 'on'` | derived from `trip.header` |
| `gpx_source_prefix` | string | `'gpx'` | `'home-gpx'` |
| `journey_id` | string | `'trip-journey'` | `'home-journey'` |
The map globals `window.tripMap` / `window.homeMap` are asserted by the Playwright map specs, so any surface using this partial must keep assigning them.
> History: this replaced the old three-variant setup (a `feed-map.html.twig` partial with its own inline init, plus a full-page `map.html.twig`). Those were deleted in the 2026-07-04 standalone-page cleanup; the `2026-06-27-map-init-consolidation` plan had already moved trip + home onto `initEntryMap`.
### Shared trip-feed-col partial
The home page's active-trip view and the trip page render the **same feed-col chrome** (date-range header, filter bar, stats/cycling panels, feed loop) via one shared Twig partial. This is separate from the `entry-map` partial above — it is the column **beside** the map, not the map.
- **Partial:** `user/themes/intotheeast/templates/partials/trip-feed-col.html.twig`
- **Used by:** `trip.html.twig` and the active branch of `home.html.twig` (both via `{% include ... with {...} only %}`)
- **Sibling:** `partials/home-predeparture.html.twig` — the home-only "Coming soon" landing state. `home.html.twig` picks it with `{% if all_items|length == 0 %}``home-predeparture` `{% else %}``trip-feed-col`. Keep `trip-feed-col` single-purpose — do NOT fold the pre-departure branch back into it.
**Parameters (`trip-feed-col`):**
| Parameter | Type | Trip passes | Home-active passes |
|---|---|---|---|
| `trip_page` | Page | `page` | `trip` |
| `all_items` | array | sorted by date, flag 4 (oldest→newest) | sorted by date, flag 3 (newest→oldest) |
| `journal_entries` | array | dailies children | dailies children |
| `journal_count` / `story_count` | int | counts | counts |
| `has_gpx` | bool | `has_gpx` | `home_gpx_urls\|length > 0` |
| `gpx_urls` | array | `gpx_urls` | `home_gpx_urls` |
| `gps_points` | array | `gps_points` | `gps_points` |
| `show_sort` | bool | `true` | `false` (home keeps its own feed order, no sort button) |
`home-predeparture` takes only `trip_page`.
**Stats/cycling JS glue:** the partial emits an inline `DOMContentLoaded` script calling `window.initTripStats({ gpxUrls, gpsPoints, hasGpx })` — one shared function in `js/src/main.js` (rebuild with `make build-assets`; never hand-edit `js/main.js`). It no-ops when `#stat-distance` is absent, populates exact distance + cycling stats from GPX, and falls back to a `~`-prefixed haversine estimate (or `—` for `<2` points) when there is no GPX. It depends on `window.MapUtils` from `map.js` (loaded in the `bottom` asset group on both pages).
### GPX file management
GPX files are stored as page media on the trip page (`user/pages/01.trips/<slug>/`). They are picked up automatically by `trip.html.twig` (and `home.html.twig`) via `trip_page.media.all`, filtered to `.gpx`, and passed to the shared `entry-map` partial.
The GPX manager page (`user/pages/03.gpx-manager/`) provides a browser UI at `/gpx-manager`:
- **Auth:** enforced by Login plugin via `access.admin.login: true` in frontmatter — shows login form if not authenticated
- **Template:** `user/themes/intotheeast/templates/gpx-manager.html.twig`
- **API:** uses Grav API v1 with session cookie auth (`session_enabled: true` in `user/plugins/api/api.yaml`)
- List: `GET /api/v1/pages{route}/media`
- Upload: `POST /api/v1/pages{route}/media` (multipart)
- Delete: `DELETE /api/v1/pages{route}/media/{filename}`
- **Slugification:** filenames are slugified client-side before upload (spaces/special chars → hyphens, lowercase); the file is sliced to a plain `Blob` so the third argument to `FormData.append` is always used as the filename
- **Media type:** `.gpx` is registered in `user/config/media.yaml` so Grav serves and tracks these files
To add GPX files without the browser UI, drop them directly into `user/pages/01.trips/<slug>/` and run `make content-push`.
### Switching to a new trip
Two places hardcode the active trip slug. Grav's config and page frontmatter are static YAML — no variable substitution is possible, so these cannot read from `site.yaml` automatically. **Both must be updated together** when starting a new trip, or entries will be posted to the wrong folder.
| File | Key | Example value |
|---|---|---|
| `user/config/site.yaml` | `active_trip` | `italy-2027` |
| `user/pages/02.post/post-form.md` | `pageconfig.parent` | `/trips/italy-2027/dailies` |
Note: `system.yaml` `home.alias` is permanently set to `/home` (the real home page) and does **not** need to change when switching trips.
After updating, also create the new trip's page tree under `user/pages/01.trips/<new-slug>/` with the two content subfolders `01.dailies/` and `04.stories/` (each with an inert `routable:false` container `.md`), plus the trip's `trip.md`. Do **not** recreate `02.map/` or `03.stats/` — those standalone views were retired.
### 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.
### 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 demo content into `italy-2026-demo` trip (12 journal entries + 4 stories + 7 GPX files); source in `user/docs/demo/trips/italy-2026-demo/`
- `make demo-reset` — remove the entire `italy-2026-demo` pages folder and clear cache (full reset; re-run demo-load to restore)
### User repo gitignore
Only these folders are tracked in the `user/` Git repo: `pages/`, `config/`, `accounts/`, `themes/`. The `plugins/` and `data/` folders are excluded.
## 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 (not yet configured)
Before going live, change in `user/config/system.yaml`:
| Setting | Prod value | Why |
|---|---|---|
| `twig.cache` | `true` | Templates compiled once and reused; safe because theme files don't change at runtime |
**Pre-launch smoke test required:** with `twig.cache: true`, submit one post via `/post` and confirm the entry appears in the trip page feed at `/trips/italy-2026-demo` immediately. This verifies the cache-on-save plugin (BUG-001 fix) works correctly with caching enabled.
### What the cache-on-save plugin handles
The custom plugin at `user/plugins/cache-on-save/` clears Grav's page-tree cache on every `new-entry` form submission. This ensures new posts appear in the tracker feed immediately in both modes — it does not depend on whether Twig caching is on or off.
## 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.