Files
intotheeast-com/CLAUDE.md
T

8.8 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.9 (installed manually — see §3 below)
  • 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

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/
  • Nav in base.html.twig derives all links from config.site.active_trip
  • Post form parent (post-form.mdpageconfig.parent) must be kept in sync with active_trip
  • 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 entries for both trips (Japan/Korea 2026 + Italy 2025 with real GPX)
  • make demo-reset — remove demo entries (keeps trip page structure, removes entries only)

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/japan-korea-2026/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)

GPM (php bin/gpm selfupgrade) does not serve Grav 2.0 RC — it still reports 1.7.x as latest even on the testing channel. To upgrade locally:

# Download grav-admin bundle (includes Grav core + admin2 plugin)
docker exec -w /tmp intotheeast_grav bash -c "
  curl -sL 'https://getgrav.org/download/core/grav-admin/2.0.0-rc.9?testing' -o grav-admin.zip && \
  unzip -q grav-admin.zip
"
# Copy core files only (not user/)
docker exec -w /tmp intotheeast_grav bash -c "
  cp -rf grav-admin/{assets,bin,system,vendor,webserver-configs,index.php,composer.json,composer.lock,robots.txt,CHANGELOG.md,LICENSE.txt} /var/www/html/
"
# Install Admin2 from the bundle (it's named admin2, not admin)
docker exec -w /tmp intotheeast_grav bash -c "
  cp -rf grav-admin/user/plugins/admin2 /var/www/html/user/plugins/admin2
"
make fix-perms
docker exec -w /var/www/html intotheeast_grav php bin/grav cache --all
# Cleanup
docker exec intotheeast_grav rm -rf /tmp/grav-admin /tmp/grav-admin.zip

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.