Files
intotheeast-com/CLAUDE.md
T

9.3 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, 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: 01.dailies/, 02.map/, 03.stats/, 04.stories/
  • 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) — do NOT add nav links back to /dailies, /stats, /stories on the trip page
  • Sort order is intentionally different per context: trip.html.twig sorts ascending (sort_by_key('date', 4)) so the trip reads chronologically from start to finish; home.html.twig active-trip mode sorts descending (sort_by_key('date', 3)) so the latest entry appears first
  • Stats are shown inline on the trip page via a toggle; the standalone /stats sub-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

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: 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 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 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 /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

First-time setup after cloning

user/plugins/ and user/data/ are excluded from git but Grav requires them to exist. Create them once after cloning:

mkdir -p user/plugins user/data

Then run make setup (starts Docker + installs plugins).

After make install-plugins: fix cache permissions

If the site returns a 500 error after plugin installation or after recreating the container, run make fix-perms. This creates uid 1000 in the container, chowns /var/www/html to 1000:1000, and reloads Apache. Always run make setup (not just make start) after docker compose down && up to ensure permissions are correct.

Grav 2.0 upgrade (local)

Grav 2.0 is baked into the custom Docker image via Dockerfile. The base getgrav/grav image ships 1.7 — the Dockerfile downloads the 2.0-rc.10 bundle from GitHub and overwrites the core files at build time, so the image always contains 2.0.

make setup = build → start → install-plugins → fix-perms. After any docker compose down, run make setup to get back to a fully working state. docker compose restart (soft restart) also preserves the image, so Grav 2.0 stays.

To upgrade to a newer RC: update the URL in Dockerfile and run make setup — Docker rebuilds the image layer automatically.

After upgrading, ensure these settings in user/config/system.yaml:

accounts:
  type: flex      # required for Admin2 API
pages:
  type: flex      # required for Admin2 pages API

And ensure the admin user account has api.* permissions (Admin2 uses a new permission namespace):

# user/accounts/<username>.yaml
access:
  admin:
    login: true
    super: true
  api:
    super: true
    access: true

Disable the old admin plugin once admin2 is installed — both route to /admin and conflict:

# In user/plugins/admin/admin.yaml:
enabled: false

JWT secret: Leave jwt_secret: '' in user/plugins/api/api.yaml — it works for local dev and production installs generate a secure secret automatically.

Language URL prefix

If Grav redirects to /en/... URLs, ensure user/config/system.yaml contains:

languages:
  supported: [en]
  include_default_lang: false

Without include_default_lang: false, Grav adds a language prefix to all URLs even for single-language sites.