Files
intotheeast-com/CLAUDE.md
T
m038andClaude Opus 4.8 2695bce835 docs: compound learning — retiring a consolidated Grav sub-page
Capture the safe procedure + three non-obvious traps from the standalone-
page cleanup: keep the folder as a routable:false data container, repoint
Back-link fallbacks to the grandparent (silent direct-landing regression),
and sync the gitignored demo source + Makefile or the next demo-load undoes it.

Seed CONCEPTS.md (Trip, Entry, Story, Active Trip) and surface docs/solutions/
+ CONCEPTS.md in CLAUDE.md so future sessions discover them.

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

14 KiB

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)
  • 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.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.yamlactive_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.mdpageconfig.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

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.