Adds a 'Shared trip-feed-col partial' section (callers, param table, the home-predeparture sibling, and the initTripStats JS glue) and points the trip-page filter-bar note at it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BftDn9vu9SonFAY4vxu4uk
11 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)
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, NOTadmin) - Docker image:
getgrav/gravwithGRAV_CHANNEL=beta - PHP session:
session.save_path = /tmpset inphp/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:
01.dailies/,02.map/,03.stats/,04.stories/ - Site nav in
base.html.twighas Home + Past Trips only — does not link to trip sub-sections - Post form parent (
post-form.md→pageconfig.parent) must be kept in sync withactive_trip - The trip page (
trip.html.twig) uses a client-side filter bar (All content / Journal / Stories) — do NOT add nav links back to/dailies,/stats,/storieson the trip page. This filter bar + stats chrome is shared with the home active-trip view via thetrip-feed-colpartial (see "Shared trip-feed-col partial" below) - Stats are shown inline on the trip page via a toggle; the standalone
/statssub-page still exists as a URL but is not linked from the trip page - GPX route files live as media on the trip page itself, served via leaflet-gpx CDN
- Manage GPX files (view/upload/delete) at
/gpx-manager— requires admin login; filenames are auto-slugified on upload
Shared feed-map partial
The mini-map above the feed is shared across two pages via a Twig partial:
- Partial:
user/themes/intotheeast/templates/partials/feed-map.html.twig - Used by:
dailies.html.twigandstories.html.twig - NOT used by:
trip.html.twig(uses its own#trip-map/.home-map-collayout)
Parameters (passed via {% include ... with {...} only %}):
| Parameter | Type | Description |
|---|---|---|
map_entries |
array | [{lat, lng, title, slug, url, type, force_connect, transport_mode}] |
map_id |
string | HTML id for map div: 'feed-map' or 'stories-map' |
map_var |
string | JS global variable: 'feedMap' or 'storiesMap' |
link_href |
string|null | "View full map" link URL; null hides it |
card_prefix |
string | Scroll-to ID prefix: 'entry-' (dailies) or 'story-' (stories) |
trip_page |
Page | Trip page object for autoconnect setting |
show_journey |
bool | true draws the route connector; false skips it |
The partial always: starts attribution collapsed, shows the fullscreen button (mobile-only, CSS display:none ≥769px), and on marker click scrolls to #<card_prefix><slug> + flashes .is-highlighted.
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 feed-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.twigand the active branch ofhome.html.twig(both via{% include ... with {...} only %}) - Sibling:
partials/home-predeparture.html.twig— the home-only "Coming soon" landing state.home.html.twigpicks it with{% if all_items|length == 0 %}→home-predeparture{% else %}→trip-feed-col. Keeptrip-feed-colsingle-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 map.html.twig via trip_page.media.all.
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: truein 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: trueinuser/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}
- List:
- Slugification: filenames are slugified client-side before upload (spaces/special chars → hyphens, lowercase); the file is sliced to a plain
Blobso the third argument toFormData.appendis always used as the filename - Media type:
.gpxis registered inuser/config/media.yamlso 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 standard four subfolders.
Environment
Never read .env — it contains sensitive credentials. You may pass it to commands (e.g. docker compose, make) but never read its 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 pushuser/to Gitea (triggers production pull via webhook)make content-pull— pull latest from Gitea to localplugins.txtis manually maintained — installing a plugin via Admin does NOT update itmake demo-load— load demo content intoitaly-2026-demotrip (12 journal entries + 4 stories + 7 GPX files); source inuser/docs/demo/trips/italy-2026-demo/make demo-reset— remove the entireitaly-2026-demopages 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 /trips/italy-2026-demo/dailies 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.