feat: trip entity restructure + Grav 2.0 upgrade
- Switch to getgrav/grav Docker image with GRAV_CHANNEL=beta (Grav 1.7.53)
- Apache runs as host UID 1000; fix-perms target handles container setup
- Rename tracker → dailies throughout (URL slug, templates, tests)
- Trip entity: /trips/<slug>/{dailies,map,stats,stories} hierarchy
- Nav driven by active_trip in site.yaml
- GPX route rendering on map via leaflet-gpx CDN
- Italy 2025 demo trip with real Tuscany GPS routes
- Admin blueprint for trip pages (date range, cover image, album URL)
- Dark mode + visual polish plan ready at docs/superpowers/plans/2026-06-19-dark-mode.md
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+3
-1
@@ -3,10 +3,12 @@
|
||||
|
||||
# Grav CMS
|
||||
/user/
|
||||
!/user/
|
||||
!/user/plugins/
|
||||
!/user/plugins/cache-on-save/
|
||||
user/accounts/
|
||||
user/data/
|
||||
user/cache/
|
||||
user/plugins/
|
||||
|
||||
# Claude
|
||||
.claude/
|
||||
|
||||
@@ -72,11 +72,10 @@ 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, the cache/logs/tmp directories may have wrong ownership (gpm runs as root inside the container). Fix with:
|
||||
|
||||
```bash
|
||||
docker exec intotheeast_grav chown -R abc:users /app/www/public/cache /app/www/public/logs /app/www/public/tmp
|
||||
```
|
||||
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.
|
||||
|
||||
### Language URL prefix
|
||||
|
||||
|
||||
@@ -27,23 +27,38 @@ start:
|
||||
stop:
|
||||
docker compose down
|
||||
|
||||
setup: start install-plugins
|
||||
setup: start install-plugins fix-perms
|
||||
|
||||
fix-perms:
|
||||
docker exec intotheeast_grav bash -c "getent passwd 1000 > /dev/null || useradd -u 1000 -M hostuser"
|
||||
docker exec intotheeast_grav chown -R 1000:1000 /var/www/html
|
||||
docker exec intotheeast_grav apachectl graceful
|
||||
|
||||
install-plugins:
|
||||
docker exec -w /app/www/public intotheeast_grav php bin/gpm install $(shell cat plugins.txt | tr '\n' ' ') -y
|
||||
docker exec -w /var/www/html intotheeast_grav php bin/gpm install $(shell cat plugins.txt | tr '\n' ' ') -y
|
||||
|
||||
# ── Demo content ──────────────────────────────────────────────────────────────
|
||||
|
||||
demo-load:
|
||||
cp -r user/docs/demo/tracker/. user/pages/01.tracker/
|
||||
docker exec intotheeast_grav bash -c "cd /app/www/public && php bin/grav clearcache"
|
||||
# Load japan-korea-2026 dailies
|
||||
cp -r user/docs/demo/trips/japan-korea-2026/dailies/. user/pages/01.trips/japan-korea-2026/01.dailies/
|
||||
# Load italy-2025 trip (create pages if absent)
|
||||
mkdir -p user/pages/01.trips/italy-2025/01.dailies user/pages/01.trips/italy-2025/02.map user/pages/01.trips/italy-2025/03.stats user/pages/01.trips/italy-2025/04.stories
|
||||
cp user/docs/demo/trips/italy-2025/trip.md user/pages/01.trips/italy-2025/trip.md 2>/dev/null || true
|
||||
cp user/docs/demo/trips/italy-2025/map.md user/pages/01.trips/italy-2025/02.map/map.md 2>/dev/null || true
|
||||
cp user/docs/demo/trips/italy-2025/stats.md user/pages/01.trips/italy-2025/03.stats/stats.md 2>/dev/null || true
|
||||
cp user/docs/demo/trips/italy-2025/stories.md user/pages/01.trips/italy-2025/04.stories/stories.md 2>/dev/null || true
|
||||
cp -r user/docs/demo/trips/italy-2025/dailies/. user/pages/01.trips/italy-2025/01.dailies/
|
||||
cp user/docs/demo/trips/italy-2025/*.gpx user/pages/01.trips/italy-2025/ 2>/dev/null || true
|
||||
docker exec intotheeast_grav bash -c "cd /var/www/html && php bin/grav clearcache"
|
||||
|
||||
demo-reset:
|
||||
@for dir in user/docs/demo/tracker/*/; do \
|
||||
@for dir in user/docs/demo/trips/japan-korea-2026/dailies/*/; do \
|
||||
folder=$$(basename "$$dir"); \
|
||||
rm -rf "user/pages/01.tracker/$$folder"; \
|
||||
rm -rf "user/pages/01.trips/japan-korea-2026/01.dailies/$$folder"; \
|
||||
done
|
||||
docker exec intotheeast_grav bash -c "cd /app/www/public && php bin/grav clearcache"
|
||||
rm -rf user/pages/01.trips/italy-2025
|
||||
docker exec intotheeast_grav bash -c "cd /var/www/html && php bin/grav clearcache"
|
||||
|
||||
# ── Content sync (user repo ↔ Gitea) ──────────────────────────────────────────
|
||||
|
||||
|
||||
+6
-5
@@ -1,13 +1,14 @@
|
||||
services:
|
||||
grav:
|
||||
image: lscr.io/linuxserver/grav:latest
|
||||
image: getgrav/grav
|
||||
container_name: intotheeast_grav
|
||||
environment:
|
||||
- PUID=1000
|
||||
- PGID=1000
|
||||
- GRAV_CHANNEL=beta
|
||||
- APACHE_RUN_USER=#1000
|
||||
- APACHE_RUN_GROUP=#1000
|
||||
ports:
|
||||
- "8081:80"
|
||||
volumes:
|
||||
- ./user:/config/www/user
|
||||
- ./php/php-local.ini:/config/php/php-local.ini
|
||||
- ./user:/var/www/html/user
|
||||
- ./php/php-local.ini:/usr/local/etc/php/conf.d/php-local.ini
|
||||
restart: unless-stopped
|
||||
|
||||
@@ -0,0 +1,508 @@
|
||||
# Grav 2.0 Upgrade Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Upgrade the local dev Docker environment from linuxserver/grav 1.7 to getgrav/grav 2.0 RC, validate the full Milestone 1 posting workflow, and update the production install script for a fresh Grav 2.0 deploy.
|
||||
|
||||
**Architecture:** Two tracks in sequence — (1) swap the Docker image and update all dependent config/paths, boot the site with `make setup`, run the existing test suite; (2) update `server-install.sh` so `make remote-install` deploys Grav 2.0 fresh on the production PHP 8.4 server. The `user/` directory (content, config, theme, custom plugins) is already isolated as a git repo and requires only a small compatibility addition to `cache-on-save`.
|
||||
|
||||
**Tech Stack:** Grav CMS 2.0.0-rc.9, PHP 8.4 (production) / Docker `getgrav/grav` with PHP 8.3 (dev), Apache, Twig 3, Symfony 7, Playwright (UI tests).
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- All work on branch `update-to-2.0` (already created)
|
||||
- Never read `.env` — contains sensitive credentials
|
||||
- Only modify files in the project root or `user/` subfolders
|
||||
- `user/config/system.yaml` is tracked in the **user/ git repo** — commit it with `git -C user add config/system.yaml && git -C user commit ...`, NOT from the main repo
|
||||
- `user/plugins/cache-on-save/` is tracked in the **main repo** (after adding `.gitignore` exception) — commit blueprints.yaml with `git add user/plugins/cache-on-save/blueprints.yaml` from the project root
|
||||
- Container name stays `intotheeast_grav`; local port stays `8081`
|
||||
- `make` commands are the only way to interact with the remote server
|
||||
- Grav 2.0 requires PHP ≥ 8.3 (dev container uses 8.3 default; production uses 8.4 — both compliant)
|
||||
- Production download URL format: `https://getgrav.org/download/core/grav-admin/${GRAV_VERSION}${GRAV_CHANNEL_SUFFIX}`
|
||||
|
||||
---
|
||||
|
||||
## Files Changed
|
||||
|
||||
| File | Action | Reason |
|
||||
|---|---|---|
|
||||
| `docker-compose.yml` | Modify | Switch image, update volume + PHP ini path, add env var |
|
||||
| `Makefile` | Modify | Three `docker exec` targets hardcode linuxserver's `/app/www/public` path |
|
||||
| `.gitignore` | Modify | Add `!user/plugins/cache-on-save/` exception to track the custom plugin in the main repo |
|
||||
| `user/plugins/cache-on-save/blueprints.yaml` | Create | Grav 2.0 compat flag (required by GPM) — committed to main repo |
|
||||
| `user/config/system.yaml` | Modify | Switch GPM channel from `stable` to `testing` |
|
||||
| `scripts/server-install.sh` | Modify | Support `GRAV_CHANNEL_SUFFIX` for `?testing` query param on 2.0 RC download |
|
||||
|
||||
---
|
||||
|
||||
## Task 1: Swap Docker image and fix container paths
|
||||
|
||||
**Files:**
|
||||
- Modify: `docker-compose.yml`
|
||||
- Modify: `Makefile`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: A running Grav 2.0 container reachable at `http://localhost:8081` with `user/` mounted at `/var/www/html/user` and PHP upload limits applied via `/usr/local/etc/php/conf.d/php-local.ini`
|
||||
|
||||
- [ ] **Step 1: Stop and remove the current container**
|
||||
|
||||
```bash
|
||||
cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast
|
||||
docker compose down
|
||||
```
|
||||
|
||||
Expected: container `intotheeast_grav` stops and is removed.
|
||||
|
||||
- [ ] **Step 2: Update `docker-compose.yml`**
|
||||
|
||||
Replace the entire contents of `docker-compose.yml` with:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
grav:
|
||||
image: getgrav/grav
|
||||
container_name: intotheeast_grav
|
||||
environment:
|
||||
- GRAV_CHANNEL=beta
|
||||
ports:
|
||||
- "8081:80"
|
||||
volumes:
|
||||
- ./user:/var/www/html/user
|
||||
- ./php/php-local.ini:/usr/local/etc/php/conf.d/php-local.ini
|
||||
restart: unless-stopped
|
||||
```
|
||||
|
||||
Key changes from old file:
|
||||
- `image`: `lscr.io/linuxserver/grav:latest` → `getgrav/grav`
|
||||
- `environment`: removed `PUID`/`PGID` (linuxserver-specific), added `GRAV_CHANNEL=beta`
|
||||
- `volumes[0]`: `/config/www/user` → `/var/www/html/user`
|
||||
- `volumes[1]`: `/config/php/php-local.ini` → `/usr/local/etc/php/conf.d/php-local.ini`
|
||||
|
||||
- [ ] **Step 3: Update Makefile — three targets use the old container path**
|
||||
|
||||
In `Makefile`, make these three targeted replacements:
|
||||
|
||||
**`install-plugins` target** — change working directory flag:
|
||||
|
||||
Old:
|
||||
```makefile
|
||||
install-plugins:
|
||||
docker exec -w /app/www/public intotheeast_grav php bin/gpm install $(shell cat plugins.txt | tr '\n' ' ') -y
|
||||
```
|
||||
|
||||
New:
|
||||
```makefile
|
||||
install-plugins:
|
||||
docker exec -w /var/www/html intotheeast_grav php bin/gpm install $(shell cat plugins.txt | tr '\n' ' ') -y
|
||||
```
|
||||
|
||||
**`demo-load` target** — change cache clear path:
|
||||
|
||||
Old:
|
||||
```makefile
|
||||
demo-load:
|
||||
cp -r user/docs/demo/tracker/. user/pages/01.tracker/
|
||||
docker exec intotheeast_grav bash -c "cd /app/www/public && php bin/grav clearcache"
|
||||
```
|
||||
|
||||
New:
|
||||
```makefile
|
||||
demo-load:
|
||||
cp -r user/docs/demo/tracker/. user/pages/01.tracker/
|
||||
docker exec intotheeast_grav bash -c "cd /var/www/html && php bin/grav clearcache"
|
||||
```
|
||||
|
||||
**`demo-reset` target** — change cache clear path:
|
||||
|
||||
Old:
|
||||
```makefile
|
||||
demo-reset:
|
||||
@for dir in user/docs/demo/tracker/*/; do \
|
||||
folder=$$(basename "$$dir"); \
|
||||
rm -rf "user/pages/01.tracker/$$folder"; \
|
||||
done
|
||||
docker exec intotheeast_grav bash -c "cd /app/www/public && php bin/grav clearcache"
|
||||
```
|
||||
|
||||
New:
|
||||
```makefile
|
||||
demo-reset:
|
||||
@for dir in user/docs/demo/tracker/*/; do \
|
||||
folder=$$(basename "$$dir"); \
|
||||
rm -rf "user/pages/01.tracker/$$folder"; \
|
||||
done
|
||||
docker exec intotheeast_grav bash -c "cd /var/www/html && php bin/grav clearcache"
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Validate docker-compose syntax**
|
||||
|
||||
```bash
|
||||
docker compose config
|
||||
```
|
||||
|
||||
Expected: prints merged compose config with no errors. If you see `Error`, re-check the YAML indentation in `docker-compose.yml`.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add docker-compose.yml Makefile
|
||||
git commit -m "feat: switch to getgrav/grav 2.0 RC docker image
|
||||
|
||||
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: Add Grav 2.0 compat flag and switch GPM to testing channel
|
||||
|
||||
**Files:**
|
||||
- Modify: `.gitignore` (add exception for `user/plugins/cache-on-save/`)
|
||||
- Create: `user/plugins/cache-on-save/blueprints.yaml` (committed to main repo)
|
||||
- Modify: `user/config/system.yaml` (committed to user/ git repo, not main repo)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: Running container from Task 1
|
||||
- Produces: GPM resolves 2.0-compatible plugin versions on install; `cache-on-save` is recognized as 2.0-compatible by Grav's plugin registry
|
||||
|
||||
- [ ] **Step 1: Create `user/plugins/cache-on-save/blueprints.yaml`**
|
||||
|
||||
Create the file with this exact content:
|
||||
|
||||
```yaml
|
||||
name: Cache On Save
|
||||
version: 1.0.0
|
||||
description: Clears Grav cache on new-entry form submission
|
||||
author:
|
||||
name: Mischa
|
||||
email: mischa@gorinskat.nl
|
||||
license: MIT
|
||||
|
||||
dependencies:
|
||||
- { name: grav, version: '>=1.6.0' }
|
||||
|
||||
grav:
|
||||
version: ['1.7', '2.0']
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Update GPM channel in `user/config/system.yaml`**
|
||||
|
||||
Find the `gpm:` section (around line 200 in the file) and change `releases: stable` to `releases: testing`:
|
||||
|
||||
Old:
|
||||
```yaml
|
||||
gpm:
|
||||
releases: stable
|
||||
official_gpm_only: true
|
||||
```
|
||||
|
||||
New:
|
||||
```yaml
|
||||
gpm:
|
||||
releases: testing
|
||||
official_gpm_only: true
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Add gitignore exception and commit blueprints.yaml to main repo**
|
||||
|
||||
```bash
|
||||
cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast
|
||||
|
||||
# Add exception so cache-on-save is tracked in the main repo
|
||||
# Insert after the existing "user/plugins/" line in .gitignore:
|
||||
# !user/plugins/cache-on-save/
|
||||
|
||||
# Then commit to the main repo:
|
||||
git add .gitignore user/plugins/cache-on-save/blueprints.yaml
|
||||
git commit -m "feat: track cache-on-save plugin in main repo; add Grav 2.0 compat flag
|
||||
|
||||
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Commit system.yaml to the user/ git repo**
|
||||
|
||||
```bash
|
||||
cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast
|
||||
git -C user add config/system.yaml
|
||||
git -C user commit -m "feat: switch GPM to testing channel for Grav 2.0
|
||||
|
||||
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: Boot Grav 2.0 and install plugins
|
||||
|
||||
**Files:** None (runtime only)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: docker-compose.yml from Task 1, GPM config from Task 2
|
||||
- Produces: Running Grav 2.0 instance at `http://localhost:8081` with all plugins installed
|
||||
|
||||
- [ ] **Step 1: Run setup**
|
||||
|
||||
```bash
|
||||
cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast
|
||||
make setup
|
||||
```
|
||||
|
||||
This starts the container and installs all plugins from `plugins.txt`. First run may take 1-2 minutes as `getgrav/grav` downloads and extracts Grav 2.0 RC.
|
||||
|
||||
Expected output ends with something like:
|
||||
```
|
||||
GPM Packages Installed: admin, email, error, form, login, problems, add-page-by-form, shortcode-gallery-plusplus
|
||||
```
|
||||
|
||||
If `make setup` fails on plugin install with a permission error, fix with:
|
||||
```bash
|
||||
docker exec intotheeast_grav chown -R www-data:www-data /var/www/html/cache /var/www/html/logs /var/www/html/tmp
|
||||
make install-plugins
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Verify PHP upload limits are applied**
|
||||
|
||||
```bash
|
||||
docker exec intotheeast_grav php -r "echo ini_get('upload_max_filesize') . ' / ' . ini_get('post_max_size');"
|
||||
```
|
||||
|
||||
Expected: `100M / 500M`
|
||||
|
||||
If you see `2M / 8M` (PHP defaults), the ini mount path is wrong. Verify with:
|
||||
```bash
|
||||
docker exec intotheeast_grav php -r "echo php_ini_scanned_files();"
|
||||
```
|
||||
It should include `/usr/local/etc/php/conf.d/php-local.ini`.
|
||||
|
||||
- [ ] **Step 3: Verify site loads**
|
||||
|
||||
```bash
|
||||
curl -s -o /dev/null -w "%{http_code}" http://localhost:8081/
|
||||
```
|
||||
|
||||
Expected: `200`
|
||||
|
||||
If you get `500`, check container logs:
|
||||
```bash
|
||||
docker logs intotheeast_grav --tail 50
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Verify Admin2 loads**
|
||||
|
||||
```bash
|
||||
curl -s -o /dev/null -w "%{http_code}" http://localhost:8081/admin
|
||||
```
|
||||
|
||||
Expected: `200` (Admin2 SPA login page, not the old Twig admin)
|
||||
|
||||
- [ ] **Step 5: Run config and HTTP tests**
|
||||
|
||||
```bash
|
||||
make test-config
|
||||
make test-post
|
||||
```
|
||||
|
||||
`test-config` validates the form YAML config. `test-post` submits the posting form via HTTP and checks an entry is created.
|
||||
|
||||
Expected: both exit 0.
|
||||
|
||||
If `test-post` fails, check the output of:
|
||||
```bash
|
||||
bash scripts/test-post.sh
|
||||
```
|
||||
This is the critical `add-page-by-form` go/no-go test. If it fails with a 500 or the entry isn't created, see the **If add-page-by-form fails** section at the bottom of this plan.
|
||||
|
||||
- [ ] **Step 6: Commit task completion note**
|
||||
|
||||
No new files to commit. Move to Task 4.
|
||||
|
||||
---
|
||||
|
||||
## Task 4: Run Playwright test suite and fix any Admin2 regressions
|
||||
|
||||
**Files:**
|
||||
- Modify: `tests/*.spec.js` (only if tests fail due to Admin2 DOM changes)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: Running Grav 2.0 from Task 3
|
||||
- Produces: All Playwright tests passing (or updated for Admin2's new DOM)
|
||||
|
||||
- [ ] **Step 1: Run the full UI test suite**
|
||||
|
||||
```bash
|
||||
cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast
|
||||
make test-ui
|
||||
```
|
||||
|
||||
Expected: 25 tests pass.
|
||||
|
||||
- [ ] **Step 2: If any tests fail, classify the failure**
|
||||
|
||||
For each failing test, determine whether it is:
|
||||
|
||||
**A) A genuine regression** (e.g., posting form broken, tracker page missing entries, gallery not rendering) — these are blockers. Stop, investigate the root cause, and fix the underlying Grav/plugin issue before updating the test.
|
||||
|
||||
**B) An Admin2 DOM change** (e.g., selectors targeting old admin HTML structure like `.admin-menu`, `.grav-nav`, admin-specific CSS classes) — these are acceptable test updates. Update the selector in the test file to match Admin2's new HTML.
|
||||
|
||||
To inspect the current Admin2 DOM for a failing selector:
|
||||
```bash
|
||||
# Check what the admin page actually renders
|
||||
curl -s http://localhost:8081/admin | grep -o '<[^>]*class="[^"]*admin[^"]*"[^>]*>' | head -20
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Update any Admin2 selector regressions**
|
||||
|
||||
For each type-(B) failure, open the relevant test file in `tests/` and update the selector. Example pattern for updating an admin navigation selector:
|
||||
|
||||
Old (targeting classic admin):
|
||||
```js
|
||||
await page.click('.grav-nav-toggle')
|
||||
```
|
||||
|
||||
New (targeting Admin2 SPA — find actual selector from step 2's output):
|
||||
```js
|
||||
await page.click('[data-testid="nav-toggle"]') // replace with actual Admin2 selector
|
||||
```
|
||||
|
||||
After each fix, re-run just that test:
|
||||
```bash
|
||||
npx playwright test tests/<filename>.spec.js --headed
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Re-run full suite to confirm all pass**
|
||||
|
||||
```bash
|
||||
make test-ui
|
||||
```
|
||||
|
||||
Expected: all tests pass.
|
||||
|
||||
- [ ] **Step 5: Commit any test updates**
|
||||
|
||||
If any test files were modified:
|
||||
```bash
|
||||
git add tests/
|
||||
git commit -m "test: update Playwright selectors for Admin2 DOM
|
||||
|
||||
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
If no test files changed, no commit needed.
|
||||
|
||||
---
|
||||
|
||||
## Task 5: Update production install script for Grav 2.0
|
||||
|
||||
**Files:**
|
||||
- Modify: `scripts/server-install.sh`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: Nothing from prior tasks (independent of Docker)
|
||||
- Produces: `make remote-install` deploys a fresh Grav 2.0 on the production PHP 8.4 server when `GRAV_VERSION=2.0.0-rc.9` and `GRAV_CHANNEL_SUFFIX=?testing` are set in `.env`
|
||||
|
||||
- [ ] **Step 1: Update the wget download line in `scripts/server-install.sh`**
|
||||
|
||||
The script currently downloads Grav with:
|
||||
```bash
|
||||
wget --no-verbose "https://getgrav.org/download/core/grav-admin/$GRAV_VERSION" -O grav-admin.zip
|
||||
```
|
||||
|
||||
Grav 2.0 RC requires `?testing` appended to the URL. Add `GRAV_CHANNEL_SUFFIX` support:
|
||||
|
||||
Old (line ~15 in the file):
|
||||
```bash
|
||||
echo "==> Downloading Grav $GRAV_VERSION"
|
||||
cd "$WEBROOT"
|
||||
wget --no-verbose "https://getgrav.org/download/core/grav-admin/$GRAV_VERSION" -O grav-admin.zip
|
||||
```
|
||||
|
||||
New:
|
||||
```bash
|
||||
echo "==> Downloading Grav $GRAV_VERSION"
|
||||
cd "$WEBROOT"
|
||||
wget --no-verbose "https://getgrav.org/download/core/grav-admin/${GRAV_VERSION}${GRAV_CHANNEL_SUFFIX:-}" -O grav-admin.zip
|
||||
```
|
||||
|
||||
The `${GRAV_CHANNEL_SUFFIX:-}` expands to empty string if unset, keeping stable releases working without any changes to `.env`.
|
||||
|
||||
- [ ] **Step 2: Add GRAV_CHANNEL_SUFFIX to the env var validation block**
|
||||
|
||||
At the top of the script the required vars are validated. `GRAV_CHANNEL_SUFFIX` is optional, so do NOT add it to the `:?` required list. Instead, add a comment above the download step:
|
||||
|
||||
After the `set -e` and required var block, add a comment before the download line:
|
||||
|
||||
```bash
|
||||
# GRAV_CHANNEL_SUFFIX: optional, set to '?testing' for RC/beta releases (e.g. 2.0.0-rc.9)
|
||||
# Leave unset or empty for stable releases.
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Verify the script logic looks correct**
|
||||
|
||||
```bash
|
||||
# Dry-run: simulate what the URL would be with 2.0 RC vars
|
||||
GRAV_VERSION=2.0.0-rc.9 GRAV_CHANNEL_SUFFIX='?testing' bash -c \
|
||||
'echo "https://getgrav.org/download/core/grav-admin/${GRAV_VERSION}${GRAV_CHANNEL_SUFFIX:-}"'
|
||||
```
|
||||
|
||||
Expected output:
|
||||
```
|
||||
https://getgrav.org/download/core/grav-admin/2.0.0-rc.9?testing
|
||||
```
|
||||
|
||||
```bash
|
||||
# Dry-run: simulate stable release (no suffix)
|
||||
GRAV_VERSION=1.7.53 bash -c \
|
||||
'echo "https://getgrav.org/download/core/grav-admin/${GRAV_VERSION}${GRAV_CHANNEL_SUFFIX:-}"'
|
||||
```
|
||||
|
||||
Expected output:
|
||||
```
|
||||
https://getgrav.org/download/core/grav-admin/1.7.53
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add scripts/server-install.sh
|
||||
git commit -m "feat: support GRAV_CHANNEL_SUFFIX for Grav 2.0 RC production install
|
||||
|
||||
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## If `add-page-by-form` fails (contingency)
|
||||
|
||||
If `make test-post` in Task 3 step 5 returns a non-zero exit code or the entry is not created, `add-page-by-form` is incompatible with Grav 2.0. The fallback is to write a custom replacement plugin.
|
||||
|
||||
**Do not proceed to Task 4 if the posting workflow is broken.** Instead:
|
||||
|
||||
1. Check the container logs for the specific error:
|
||||
```bash
|
||||
docker logs intotheeast_grav --tail 100 | grep -i "error\|exception\|warning"
|
||||
```
|
||||
|
||||
2. Note the error, stop work, and report back. The custom replacement plugin is a separate task requiring design input from the project owner before implementation.
|
||||
|
||||
The custom plugin would:
|
||||
- Hook `onFormProcessed` (same as `cache-on-save`)
|
||||
- Read form field values (`title`, `content`, `photo`)
|
||||
- Build the page path under `user/pages/01.tracker/`
|
||||
- Write the page file to disk using `Grav\Common\Page\Page`
|
||||
- Merge `cache-on-save` functionality (call `$this->grav['cache']->deleteAll()`)
|
||||
- Replace both `add-page-by-form` and `cache-on-save` with a single plugin
|
||||
|
||||
This is ~200 lines of PHP and ~1 day of work. It should be planned separately.
|
||||
|
||||
---
|
||||
|
||||
## Final smoke test (after all tasks complete)
|
||||
|
||||
Run the full test suite one last time:
|
||||
|
||||
```bash
|
||||
cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast
|
||||
make test
|
||||
```
|
||||
|
||||
Expected: all three suites (`test-config`, `test-post`, `test-ui`) exit 0.
|
||||
|
||||
Then verify the go/no-go criteria from the spec are all met before merging to `main` or deploying to production.
|
||||
@@ -0,0 +1,316 @@
|
||||
# Dark Mode & Visual Polish Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use `superpowers:subagent-driven-development` (recommended) or `superpowers:executing-plans` to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Replace the warm-paper light theme with a warm-dark "notebook at night" aesthetic — dark-only, no toggle, paper grain texture, dark terrain map tiles, typography polish.
|
||||
|
||||
**Architecture:** Pure CSS token swap in `tokens.css` (all components update automatically), grain overlay via `body::after` SVG data URI in `style.css`, map tile URL swap in two Twig templates. No new dependencies, no JS changes, no structural changes.
|
||||
|
||||
**Tech Stack:** CSS custom properties, inline SVG noise filter, Stadia Maps Alidade Smooth Dark tile CDN, Leaflet.js (already present)
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- All changes in `user/` — commit with `git -C user`, not main-repo git
|
||||
- Dark-only — no `prefers-color-scheme` media query, no light-mode fallback, no toggle
|
||||
- Existing token names in `tokens.css` must not change — only values swap
|
||||
- No new npm/JS dependencies
|
||||
- `make test-ui` must pass after every task (pre-existing P2 FilePond failure is acceptable)
|
||||
- Stadia Maps tile URL: `https://tiles.stadiamaps.com/tiles/alidade_smooth_dark/{z}/{x}/{y}{r}.png`
|
||||
- Stadia attribution (exact): `© <a href="https://stadiamaps.com/">Stadia Maps</a> © <a href="https://openmaptiles.org/">OpenMapTiles</a> © <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors`
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Dark color tokens
|
||||
|
||||
**Files:**
|
||||
- Modify: `user/themes/intotheeast/css/tokens.css`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: CSS custom properties consumed by every component in `style.css` and Twig templates
|
||||
|
||||
- [ ] **Step 1: Read the current tokens file**
|
||||
|
||||
```bash
|
||||
cat user/themes/intotheeast/css/tokens.css
|
||||
```
|
||||
|
||||
Confirm these token names exist before editing: `--color-paper`, `--color-canvas`, `--color-ink`, `--color-ink-2`, `--color-ink-muted`, `--color-border`, `--color-border-soft`, `--color-accent`, `--color-accent-hover`, `--color-accent-light`, `--color-accent-on`.
|
||||
|
||||
- [ ] **Step 2: Replace the color block in tokens.css**
|
||||
|
||||
Replace the entire `:root` color block (from `--color-paper` through `--color-accent-on`) with:
|
||||
|
||||
```css
|
||||
/* ── Dark palette (warm notebook) ──────────────────────────────────────── */
|
||||
--color-paper: #1A1814; /* page background — warm near-black */
|
||||
--color-canvas: #22201B; /* card surfaces, form backgrounds */
|
||||
--color-ink: #EDE8DF; /* primary text — warm cream */
|
||||
--color-ink-2: #B8B0A4; /* body text — muted warm */
|
||||
--color-ink-muted: #7A7268; /* labels, timestamps, captions */
|
||||
--color-border: #2E2B25; /* standard dividers */
|
||||
--color-border-soft: #252219; /* subtle dividers */
|
||||
--color-accent: #2A8C73; /* teal — lightened for dark contrast */
|
||||
--color-accent-hover: #236655; /* hover/pressed teal */
|
||||
--color-accent-light: #1A2E29; /* pale teal tint backgrounds */
|
||||
--color-accent-on: #FFFFFF; /* text on accent surfaces */
|
||||
--color-surface-raised: #2A2720; /* elevated surfaces: tooltips, hover */
|
||||
--color-ink-inverse: #17171A; /* text on accent-coloured buttons */
|
||||
```
|
||||
|
||||
Keep all non-color tokens (`--text-*`, `--leading-*`, `--space-*`, font variables, etc.) unchanged.
|
||||
|
||||
- [ ] **Step 3: Verify no syntax errors**
|
||||
|
||||
```bash
|
||||
docker exec intotheeast_grav bash -c "cd /var/www/html && php bin/grav clearcache" && curl -s -o /dev/null -w "%{http_code}" http://localhost:8081/trips/japan-korea-2026/dailies
|
||||
```
|
||||
|
||||
Expected: `200`
|
||||
|
||||
- [ ] **Step 4: Visual smoke check**
|
||||
|
||||
```bash
|
||||
curl -s http://localhost:8081/trips/japan-korea-2026/dailies | grep -o 'color: var(--color-paper)' | head -3
|
||||
```
|
||||
|
||||
Not a definitive check — just confirm the page renders. Open a browser and verify the background is dark and text is cream.
|
||||
|
||||
- [ ] **Step 5: Run test suite**
|
||||
|
||||
```bash
|
||||
make test-ui
|
||||
```
|
||||
|
||||
Expected: 24/25 pass (P2 FilePond is pre-existing failure, all others pass).
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git -C user add themes/intotheeast/css/tokens.css
|
||||
git -C user commit -m "feat: switch to warm-dark color tokens"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Paper grain texture + hardcoded color fixes + typography
|
||||
|
||||
**Files:**
|
||||
- Modify: `user/themes/intotheeast/css/style.css`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: dark color tokens from Task 1
|
||||
|
||||
- [ ] **Step 1: Find all hardcoded color literals in style.css**
|
||||
|
||||
```bash
|
||||
grep -n '#[0-9a-fA-F]\{3,6\}\|background: white\|background:#fff\|color: #\|background-color: #' user/themes/intotheeast/css/style.css
|
||||
```
|
||||
|
||||
Make note of every hit — each one is a candidate to replace with a token. Exceptions: the CSS SVG data URI you are about to add (the noise filter hex values are part of the graphic, not UI colors).
|
||||
|
||||
- [ ] **Step 2: Add paper grain texture to body**
|
||||
|
||||
Find the `body` rule in `style.css`. It will look something like:
|
||||
|
||||
```css
|
||||
body {
|
||||
background-color: var(--color-paper);
|
||||
color: var(--color-ink);
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
Add a `body::after` rule immediately after the `body` rule:
|
||||
|
||||
```css
|
||||
body::after {
|
||||
content: '';
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
z-index: 9998;
|
||||
opacity: 0.035;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.65' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='200' height='200' filter='url(%23noise)' opacity='1'/%3E%3C/svg%3E");
|
||||
background-repeat: repeat;
|
||||
background-size: 200px 200px;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Fix hardcoded login form colors**
|
||||
|
||||
Find this rule (around line 497):
|
||||
|
||||
```css
|
||||
.login-form .button.secondary { background: #f0f0f0; color: #333; text-decoration: none; line-height: 44px; padding: 0 1rem; }
|
||||
```
|
||||
|
||||
Replace with:
|
||||
|
||||
```css
|
||||
.login-form .button.secondary { background: var(--color-canvas); color: var(--color-ink); text-decoration: none; line-height: 44px; padding: 0 1rem; }
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Fix any other hardcoded colors found in Step 1**
|
||||
|
||||
For each hardcoded literal found in Step 1 (excluding the data URI you added):
|
||||
- `#fff` / `white` → `var(--color-canvas)` (if a surface) or `var(--color-paper)` (if a page background)
|
||||
- `#333` / dark grays → `var(--color-ink)` or `var(--color-ink-2)`
|
||||
- `#eee` / light grays → `var(--color-border)` or `var(--color-border-soft)`
|
||||
- `#f0f0f0` / near-white → `var(--color-canvas)`
|
||||
|
||||
Use judgment: if a hex is inside a gradient or SVG path data, leave it alone.
|
||||
|
||||
- [ ] **Step 5: Typography — increase entry body paragraph spacing**
|
||||
|
||||
Find:
|
||||
|
||||
```css
|
||||
.entry-body p { margin-bottom: 1.1em; font-size: var(--text-md); line-height: var(--leading-normal); color: var(--color-ink-2); }
|
||||
```
|
||||
|
||||
Change `margin-bottom: 1.1em` to `margin-bottom: 1.4em`.
|
||||
|
||||
- [ ] **Step 6: Typography — tighten h1/h2 tracking**
|
||||
|
||||
Find the `h1` and `h2` rules. Any rule that applies `letter-spacing: -0.01em` to an `h1` or `h2` — change it to `-0.02em`. Do not touch h3/h4/h5/h6.
|
||||
|
||||
- [ ] **Step 7: Stats page — tabular numbers**
|
||||
|
||||
Find any CSS rule targeting stats numbers (look for `.stat-value`, `.stats-number`, or similar). Add `font-variant-numeric: tabular-nums` to it. If no such specific rule exists, search the template:
|
||||
|
||||
```bash
|
||||
grep -n 'stat\|number\|count' user/themes/intotheeast/templates/stats.html.twig | head -20
|
||||
```
|
||||
|
||||
Then add a targeted rule in style.css for whatever class wraps the numeric values.
|
||||
|
||||
- [ ] **Step 8: Verify no syntax errors and visual check**
|
||||
|
||||
```bash
|
||||
docker exec intotheeast_grav bash -c "cd /var/www/html && php bin/grav clearcache" && curl -s -o /dev/null -w "%{http_code}" http://localhost:8081/trips/japan-korea-2026/dailies
|
||||
```
|
||||
|
||||
Expected: `200`. Open browser — grain should be subtly visible on the dark background.
|
||||
|
||||
- [ ] **Step 9: Run test suite**
|
||||
|
||||
```bash
|
||||
make test-ui
|
||||
```
|
||||
|
||||
Expected: 24/25 (P2 pre-existing).
|
||||
|
||||
- [ ] **Step 10: Commit**
|
||||
|
||||
```bash
|
||||
git -C user add themes/intotheeast/css/style.css
|
||||
git -C user commit -m "feat: add paper grain texture, fix hardcoded colors, improve typography"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Dark terrain map tiles
|
||||
|
||||
**Files:**
|
||||
- Modify: `user/themes/intotheeast/templates/map.html.twig`
|
||||
- Modify: `user/themes/intotheeast/templates/dailies.html.twig`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: Leaflet.js already loaded in both templates
|
||||
- Produces: Stadia Alidade Smooth Dark tiles replacing OpenStreetMap tiles in both map views
|
||||
|
||||
- [ ] **Step 1: Read current tile setup in both templates**
|
||||
|
||||
```bash
|
||||
grep -n "tileLayer\|openstreetmap\|attribution\|stadia" user/themes/intotheeast/templates/map.html.twig user/themes/intotheeast/templates/dailies.html.twig
|
||||
```
|
||||
|
||||
Confirm the current tile URL pattern (`{s}.tile.openstreetmap.org`) in both files.
|
||||
|
||||
- [ ] **Step 2: Replace tile layer in map.html.twig**
|
||||
|
||||
Find:
|
||||
|
||||
```javascript
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
||||
}).addTo(map);
|
||||
```
|
||||
|
||||
Replace with:
|
||||
|
||||
```javascript
|
||||
// TODO: add Stadia API key before launch — free dev use requires no key, production does
|
||||
L.tileLayer('https://tiles.stadiamaps.com/tiles/alidade_smooth_dark/{z}/{x}/{y}{r}.png', {
|
||||
maxZoom: 20,
|
||||
attribution: '© <a href="https://stadiamaps.com/">Stadia Maps</a> © <a href="https://openmaptiles.org/">OpenMapTiles</a> © <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
||||
}).addTo(map);
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Replace tile layer in dailies.html.twig (mini-map)**
|
||||
|
||||
Apply the identical tile swap to the mini-map `L.tileLayer` call in `dailies.html.twig`. Find the OpenStreetMap tile URL and replace it with the Stadia dark URL (same as Step 2, same attribution, same TODO comment).
|
||||
|
||||
- [ ] **Step 4: Verify tiles load**
|
||||
|
||||
```bash
|
||||
curl -s -o /dev/null -w "%{http_code}" http://localhost:8081/trips/japan-korea-2026/map
|
||||
```
|
||||
|
||||
Expected: `200`.
|
||||
|
||||
Check the tile URL is in the HTML:
|
||||
|
||||
```bash
|
||||
curl -s http://localhost:8081/trips/japan-korea-2026/map | grep -o 'stadiamaps'
|
||||
```
|
||||
|
||||
Expected: `stadiamaps` (appears in the tile URL).
|
||||
|
||||
Open the map in a browser and confirm:
|
||||
- Dark terrain tiles render (not the default light OSM tiles)
|
||||
- GPX polyline is visible in teal on the dark background
|
||||
- Entry pins render correctly on top
|
||||
- Attribution footer is present
|
||||
|
||||
- [ ] **Step 5: Check mini-map on dailies page**
|
||||
|
||||
```bash
|
||||
curl -s http://localhost:8081/trips/japan-korea-2026/dailies | grep -o 'stadiamaps'
|
||||
```
|
||||
|
||||
Expected: `stadiamaps`.
|
||||
|
||||
- [ ] **Step 6: Run test suite**
|
||||
|
||||
```bash
|
||||
make test-ui
|
||||
```
|
||||
|
||||
Expected: 24/25 (P2 pre-existing).
|
||||
|
||||
- [ ] **Step 7: Commit**
|
||||
|
||||
```bash
|
||||
git -C user add themes/intotheeast/templates/map.html.twig themes/intotheeast/templates/dailies.html.twig
|
||||
git -C user commit -m "feat: switch to Stadia Alidade Smooth Dark map tiles"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Final verification
|
||||
|
||||
After all 3 tasks:
|
||||
|
||||
1. `make test-config && make test-post && make test-ui` — all pass
|
||||
2. Visual check list (browser, not curl):
|
||||
- `/trips/japan-korea-2026/dailies` — dark warm background, cream text, grain visible, teal accents
|
||||
- `/trips/japan-korea-2026/map` — dark terrain tiles, teal GPX polyline, entry pins
|
||||
- `/trips/japan-korea-2026/dailies/<any-entry>` — dark canvas card, no white boxes
|
||||
- `/post` — form fields readable, no black-on-black inputs
|
||||
- `/trips/japan-korea-2026/stats` — numbers align (tabular-nums)
|
||||
3. Final hardcoded-literal check:
|
||||
```bash
|
||||
grep -n '#[0-9a-fA-F]\{3,6\}' user/themes/intotheeast/css/style.css | grep -v 'data:image'
|
||||
```
|
||||
All remaining hits should be either intentional (e.g. SVG path data) or documented.
|
||||
@@ -0,0 +1,538 @@
|
||||
# Trip Entity Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use `superpowers:subagent-driven-development` to implement this plan task-by-task.
|
||||
|
||||
**Goal:** Restructure the site around a Trip entity — tracker/map/stats/stories become children of `/trips/japan-korea-2026/`, GPX route files live as media on the trip page, and `site.yaml` holds an `active_trip` slug so the nav can switch trips via config.
|
||||
|
||||
**Architecture:** Trip = a Grav page (`trip.html.twig`) at `/trips/<slug>/`. Map/stats templates find the tracker via `page.parent().route ~ '/tracker'` instead of the hardcoded `/tracker` path. Leaflet-gpx (CDN) loads all `*.gpx` media files from the trip page. A `trips.html.twig` listing page provides the multi-trip root. Stories is stubbed with a placeholder template.
|
||||
|
||||
**Tech Stack:** Grav CMS 1.7/2.0, Twig, Leaflet.js, leaflet-gpx (CDN, vanilla JS — consistent with existing inline JS pattern)
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- All content/theme edits go in `user/` — commit with `git -C user`, not main-repo git
|
||||
- Entry URLs change: `/tracker/<slug>` → `/trips/japan-korea-2026/tracker/<slug>` — acceptable pre-launch
|
||||
- `make test-post` (6/6) and `make test-ui` (25/25) must pass after every task
|
||||
- No new JS framework dependencies; leaflet-gpx is 3KB vanilla JS
|
||||
- `user/config/media.yaml` must whitelist `.gpx` so Grav serves it as a file
|
||||
- The `02.post/post-form.md` `pageconfig.parent` must stay in sync with the tracker path
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Restructure pages under `/trips/`
|
||||
|
||||
**Files:**
|
||||
- Create: `user/pages/01.trips/trips.md`
|
||||
- Create: `user/pages/01.trips/japan-korea-2026/trip.md`
|
||||
- Create: `user/pages/01.trips/japan-korea-2026/01.tracker/tracker.md` (copy from `user/pages/01.tracker/tracker.md`, no content change)
|
||||
- Move: all `*.entry/` folders from `user/pages/01.tracker/` → `user/pages/01.trips/japan-korea-2026/01.tracker/`
|
||||
- Create: `user/pages/01.trips/japan-korea-2026/02.map/map.md` (copy from `user/pages/03.map/map.md`)
|
||||
- Create: `user/pages/01.trips/japan-korea-2026/03.stats/stats.md` (copy from `user/pages/04.stats/stats.md`)
|
||||
- Create: `user/pages/01.trips/japan-korea-2026/04.stories/stories.md`
|
||||
- Delete: `user/pages/01.tracker/`, `user/pages/03.map/`, `user/pages/04.stats/`
|
||||
- Modify: `user/config/site.yaml` — add `active_trip: japan-korea-2026`
|
||||
- Modify (create if absent): `user/config/media.yaml` — whitelist GPX
|
||||
|
||||
- [ ] **Step 1: Verify current structure before touching anything**
|
||||
|
||||
```bash
|
||||
find user/pages -name "*.md" | sort
|
||||
```
|
||||
Expected: entries under `01.tracker/`, map at `03.map/map.md`, stats at `04.stats/stats.md`.
|
||||
|
||||
- [ ] **Step 2: Create trips hierarchy**
|
||||
|
||||
```bash
|
||||
mkdir -p user/pages/01.trips/japan-korea-2026/01.tracker
|
||||
mkdir -p user/pages/01.trips/japan-korea-2026/02.map
|
||||
mkdir -p user/pages/01.trips/japan-korea-2026/03.stats
|
||||
mkdir -p user/pages/01.trips/japan-korea-2026/04.stories
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Write `trips.md`**
|
||||
|
||||
`user/pages/01.trips/trips.md`:
|
||||
```yaml
|
||||
---
|
||||
title: Trips
|
||||
template: trips
|
||||
content:
|
||||
items: '@self.children'
|
||||
order:
|
||||
by: date
|
||||
dir: desc
|
||||
---
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Write `trip.md`**
|
||||
|
||||
`user/pages/01.trips/japan-korea-2026/trip.md`:
|
||||
```yaml
|
||||
---
|
||||
title: 'Japan & Korea 2026'
|
||||
template: trip
|
||||
date: '2026-06-17'
|
||||
date_start: '2026-06-17'
|
||||
date_end: ''
|
||||
cover_image: ''
|
||||
content:
|
||||
items: '@self.children'
|
||||
---
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Copy tracker.md, move entries**
|
||||
|
||||
```bash
|
||||
cp user/pages/01.tracker/tracker.md user/pages/01.trips/japan-korea-2026/01.tracker/tracker.md
|
||||
mv user/pages/01.tracker/*.entry user/pages/01.trips/japan-korea-2026/01.tracker/
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Copy map.md and stats.md**
|
||||
|
||||
```bash
|
||||
cp user/pages/03.map/map.md user/pages/01.trips/japan-korea-2026/02.map/map.md
|
||||
cp user/pages/04.stats/stats.md user/pages/01.trips/japan-korea-2026/03.stats/stats.md
|
||||
```
|
||||
|
||||
- [ ] **Step 7: Write stories stub**
|
||||
|
||||
`user/pages/01.trips/japan-korea-2026/04.stories/stories.md`:
|
||||
```yaml
|
||||
---
|
||||
title: Stories
|
||||
template: stories
|
||||
published: true
|
||||
---
|
||||
```
|
||||
|
||||
- [ ] **Step 8: Delete old top-level pages**
|
||||
|
||||
```bash
|
||||
rm -rf user/pages/01.tracker user/pages/03.map user/pages/04.stats
|
||||
```
|
||||
|
||||
- [ ] **Step 9: Add `active_trip` to site.yaml**
|
||||
|
||||
Add to `user/config/site.yaml`:
|
||||
```yaml
|
||||
active_trip: japan-korea-2026
|
||||
```
|
||||
|
||||
- [ ] **Step 10: Whitelist GPX in media.yaml**
|
||||
|
||||
`user/config/media.yaml` (create if absent):
|
||||
```yaml
|
||||
gpx:
|
||||
type: file
|
||||
extensions: ['gpx']
|
||||
mime: application/gpx+xml
|
||||
```
|
||||
|
||||
- [ ] **Step 11: Verify pages load at new URLs**
|
||||
|
||||
```bash
|
||||
curl -s -o /dev/null -w "%{http_code}" http://localhost:8081/trips/japan-korea-2026/tracker
|
||||
# Expected: 200
|
||||
curl -s -o /dev/null -w "%{http_code}" http://localhost:8081/trips/japan-korea-2026/map
|
||||
# Expected: 200
|
||||
curl -s -o /dev/null -w "%{http_code}" http://localhost:8081/trips/japan-korea-2026/stats
|
||||
# Expected: 200
|
||||
```
|
||||
|
||||
- [ ] **Step 12: Commit**
|
||||
|
||||
```bash
|
||||
git -C user add pages/01.trips config/site.yaml config/media.yaml
|
||||
git -C user rm -r --cached pages/01.tracker pages/03.map pages/04.stats
|
||||
git -C user commit -m "feat: restructure pages under trips/japan-korea-2026 entity"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Update templates for trip-relative paths + new trip/trips/stories templates
|
||||
|
||||
**Files:**
|
||||
- Modify: `user/themes/intotheeast/templates/map.html.twig` — change hardcoded `/tracker` path
|
||||
- Modify: `user/themes/intotheeast/templates/stats.html.twig` — same
|
||||
- Modify: `user/themes/intotheeast/templates/partials/base.html.twig` — nav uses `active_trip`
|
||||
- Create: `user/themes/intotheeast/templates/trip.html.twig`
|
||||
- Create: `user/themes/intotheeast/templates/trips.html.twig`
|
||||
- Create: `user/themes/intotheeast/templates/stories.html.twig`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `config.site.active_trip` from site.yaml (set in Task 1)
|
||||
- Produces: map/stats find entries via `page.parent().route ~ '/tracker'`
|
||||
|
||||
- [ ] **Step 1: Fix `map.html.twig` — tracker path**
|
||||
|
||||
Replace:
|
||||
```twig
|
||||
{% set tracker_page = grav.pages.find('/tracker') %}
|
||||
{% set all_entries = tracker_page ? tracker_page.children.published() : [] %}
|
||||
```
|
||||
With:
|
||||
```twig
|
||||
{% set tracker_page = grav.pages.find(page.parent().route ~ '/tracker') %}
|
||||
{% set all_entries = tracker_page ? tracker_page.children.published() : [] %}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Fix `stats.html.twig` — tracker path**
|
||||
|
||||
Same replacement as Step 1 (identical pattern in stats.html.twig).
|
||||
|
||||
- [ ] **Step 3: Update `base.html.twig` nav**
|
||||
|
||||
Replace hardcoded nav href values with `active_trip`-driven paths. The pattern in base.html.twig currently sets hrefs to `/tracker`, `/map`, `/stats`. Replace with:
|
||||
|
||||
```twig
|
||||
{% set active_trip = config.site.active_trip %}
|
||||
{% set trip_base = '/trips/' ~ active_trip %}
|
||||
```
|
||||
|
||||
Nav links become:
|
||||
- Journal: `{{ trip_base }}/tracker`
|
||||
- Map: `{{ trip_base }}/map`
|
||||
- Stats: `{{ trip_base }}/stats`
|
||||
|
||||
Active state detection: replace `page.url starts with '/tracker'` checks with `page.url starts with trip_base ~ '/tracker'` (and similarly for map/stats).
|
||||
|
||||
- [ ] **Step 4: Create `trip.html.twig`**
|
||||
|
||||
`user/themes/intotheeast/templates/trip.html.twig`:
|
||||
```twig
|
||||
{% extends 'partials/base.html.twig' %}
|
||||
|
||||
{% block content %}
|
||||
{% set tracker_page = grav.pages.find(page.route ~ '/tracker') %}
|
||||
{% set entries = tracker_page ? tracker_page.children.published() : [] %}
|
||||
|
||||
<div class="trip-hero">
|
||||
<h1>{{ page.title }}</h1>
|
||||
{% if page.header.date_start %}
|
||||
<p class="trip-dates">
|
||||
{{ page.header.date_start|date('d M Y') }}
|
||||
{% if page.header.date_end %} — {{ page.header.date_end|date('d M Y') }}{% endif %}
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<nav class="trip-nav">
|
||||
<a href="{{ page.route }}/tracker">Journal</a>
|
||||
<a href="{{ page.route }}/map">Map</a>
|
||||
<a href="{{ page.route }}/stats">Stats</a>
|
||||
<a href="{{ page.route }}/stories">Stories</a>
|
||||
</nav>
|
||||
|
||||
{% if entries|length > 0 %}
|
||||
<section class="trip-recent">
|
||||
<h2>Recent entries</h2>
|
||||
{% for entry in entries|slice(0, 3) %}
|
||||
<a href="{{ entry.url }}">
|
||||
<span>{{ entry.date|date('d M Y') }}</span>
|
||||
{{ entry.title }}
|
||||
{% if entry.header.location_city %} · {{ entry.header.location_city }}{% endif %}
|
||||
</a>
|
||||
{% endfor %}
|
||||
</section>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Create `trips.html.twig`**
|
||||
|
||||
`user/themes/intotheeast/templates/trips.html.twig`:
|
||||
```twig
|
||||
{% extends 'partials/base.html.twig' %}
|
||||
|
||||
{% block content %}
|
||||
<h1>{{ page.title }}</h1>
|
||||
{% set trips = page.children.published() %}
|
||||
{% if trips|length == 0 %}
|
||||
<p>No trips yet.</p>
|
||||
{% else %}
|
||||
<ul class="trips-list">
|
||||
{% for trip in trips %}
|
||||
<li>
|
||||
<a href="{{ trip.url }}">
|
||||
<strong>{{ trip.title }}</strong>
|
||||
{% if trip.header.date_start %}
|
||||
<span>{{ trip.header.date_start|date('d M Y') }}</span>
|
||||
{% endif %}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Create `stories.html.twig` stub**
|
||||
|
||||
`user/themes/intotheeast/templates/stories.html.twig`:
|
||||
```twig
|
||||
{% extends 'partials/base.html.twig' %}
|
||||
|
||||
{% block content %}
|
||||
<h1>{{ page.title }}</h1>
|
||||
<p>Stories coming soon.</p>
|
||||
{% endblock %}
|
||||
```
|
||||
|
||||
- [ ] **Step 7: Verify templates render**
|
||||
|
||||
```bash
|
||||
curl -s -o /dev/null -w "%{http_code}" http://localhost:8081/trips/japan-korea-2026
|
||||
# Expected: 200
|
||||
curl -s -o /dev/null -w "%{http_code}" http://localhost:8081/trips
|
||||
# Expected: 200
|
||||
curl -s -o /dev/null -w "%{http_code}" http://localhost:8081/trips/japan-korea-2026/stories
|
||||
# Expected: 200
|
||||
```
|
||||
|
||||
Check nav links resolve correctly on tracker/map/stats pages.
|
||||
|
||||
- [ ] **Step 8: Commit**
|
||||
|
||||
```bash
|
||||
git -C user add themes/intotheeast/templates/
|
||||
git -C user commit -m "feat: add trip/trips/stories templates, update nav and map/stats to use trip-relative paths"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Add GPX route support to map template
|
||||
|
||||
**Files:**
|
||||
- Modify: `user/themes/intotheeast/templates/map.html.twig`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `*.gpx` files uploaded as media to the trip page (`page.parent()`)
|
||||
- Produces: GPX tracks rendered as colored polylines on the Leaflet map, underneath entry pins
|
||||
|
||||
- [ ] **Step 1: Add leaflet-gpx script tag**
|
||||
|
||||
In `map.html.twig`, after the existing Leaflet script tag, add:
|
||||
```html
|
||||
<script src="https://cdn.jsdelivr.net/npm/leaflet-gpx@2.1.2/gpx.min.js"></script>
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Collect GPX URLs from trip media**
|
||||
|
||||
After the `{% set trip_page = page.parent() %}` line (add this at the top of the template, alongside the tracker_page lookup), add:
|
||||
|
||||
```twig
|
||||
{% set gpx_urls = [] %}
|
||||
{% for name, media in trip_page.media.all %}
|
||||
{% if name|split('.')|last == 'gpx' %}
|
||||
{% set gpx_urls = gpx_urls|merge([media.url]) %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Pass GPX URLs to JavaScript**
|
||||
|
||||
In the `<script>` block, after the map is initialized and before the entry markers loop, add:
|
||||
|
||||
```javascript
|
||||
// GPX route tracks
|
||||
const gpxUrls = {{ gpx_urls|json_encode|raw }};
|
||||
gpxUrls.forEach(url => {
|
||||
new L.GPX(url, {
|
||||
async: true,
|
||||
polyline_options: { color: '#1F6B5A', weight: 2, opacity: 0.7 },
|
||||
marker_options: { startIconUrl: null, endIconUrl: null, shadowUrl: null }
|
||||
}).addTo(map);
|
||||
});
|
||||
```
|
||||
|
||||
Disabling start/end markers keeps the map clean — the entry pins already mark key stops.
|
||||
|
||||
- [ ] **Step 4: Test with a sample GPX**
|
||||
|
||||
Create a minimal 3-point GPX file to test without a real Komoot export:
|
||||
|
||||
```xml
|
||||
<?xml version="1.0"?>
|
||||
<gpx version="1.1" creator="test">
|
||||
<trk><trkseg>
|
||||
<trkpt lat="35.6762" lon="139.6503"><time>2026-03-25T10:00:00Z</time></trkpt>
|
||||
<trkpt lat="35.0116" lon="135.7681"><time>2026-03-27T10:00:00Z</time></trkpt>
|
||||
<trkpt lat="37.5665" lon="126.9780"><time>2026-04-01T10:00:00Z</time></trkpt>
|
||||
</trkseg></trk>
|
||||
</gpx>
|
||||
```
|
||||
|
||||
Upload via Grav Admin to the trip page media, then verify the map at `/trips/japan-korea-2026/map` renders the polyline. Remove the test file after verification.
|
||||
|
||||
- [ ] **Step 5: Verify map still works without GPX**
|
||||
|
||||
Confirm map renders normally when no `.gpx` files are present (gpxUrls = []).
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git -C user add themes/intotheeast/templates/map.html.twig
|
||||
git -C user commit -m "feat: add GPX route rendering to trip map via leaflet-gpx"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Update post form, Makefile, demo content, and tests
|
||||
|
||||
**Files:**
|
||||
- Modify: `user/pages/02.post/post-form.md` — `pageconfig.parent`
|
||||
- Modify: `Makefile` — `demo-load` and `demo-reset` paths
|
||||
- Modify: `scripts/test-post.sh` — `TRACKER` variable
|
||||
- Modify: `scripts/test-form-config.sh` — expected parent value
|
||||
- Modify: `tests/ui/tracker.spec.js` — any hardcoded `/tracker` URL references
|
||||
- Modify: `user/docs/demo/` — move demo entries to new path structure
|
||||
|
||||
- [ ] **Step 1: Update post form parent**
|
||||
|
||||
In `user/pages/02.post/post-form.md`, change:
|
||||
```yaml
|
||||
pageconfig:
|
||||
parent: '/tracker'
|
||||
```
|
||||
To:
|
||||
```yaml
|
||||
pageconfig:
|
||||
parent: '/trips/japan-korea-2026/tracker'
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Update demo content structure**
|
||||
|
||||
```bash
|
||||
mkdir -p user/docs/demo/trips/japan-korea-2026/tracker
|
||||
mv user/docs/demo/tracker/* user/docs/demo/trips/japan-korea-2026/tracker/
|
||||
rmdir user/docs/demo/tracker
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Update Makefile demo targets**
|
||||
|
||||
In `Makefile`, update `demo-load` and `demo-reset`:
|
||||
|
||||
```makefile
|
||||
demo-load:
|
||||
cp -r user/docs/demo/trips/japan-korea-2026/tracker/. user/pages/01.trips/japan-korea-2026/01.tracker/
|
||||
docker exec intotheeast_grav bash -c "cd /var/www/html && php bin/grav clearcache"
|
||||
|
||||
demo-reset:
|
||||
@for dir in user/docs/demo/trips/japan-korea-2026/tracker/*/; do \
|
||||
folder=$$(basename "$$dir"); \
|
||||
rm -rf "user/pages/01.trips/japan-korea-2026/01.tracker/$$folder"; \
|
||||
done
|
||||
docker exec intotheeast_grav bash -c "cd /var/www/html && php bin/grav clearcache"
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Update `test-post.sh` TRACKER path**
|
||||
|
||||
Find the line setting `TRACKER=` in `scripts/test-post.sh` and change it to:
|
||||
```bash
|
||||
TRACKER="user/pages/01.trips/japan-korea-2026/01.tracker"
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Update `test-form-config.sh` expected parent**
|
||||
|
||||
Find the assertion that checks `parent: '/tracker'` and update to check for `parent: '/trips/japan-korea-2026/tracker'`.
|
||||
|
||||
- [ ] **Step 6: Check Playwright tests for hardcoded paths**
|
||||
|
||||
Search `tests/ui/` for any hardcoded `/tracker` URL references:
|
||||
```bash
|
||||
grep -rn "tracker\|/map\|/stats" tests/ui/
|
||||
```
|
||||
|
||||
Update any that reference the old paths to use the new trip-scoped paths.
|
||||
|
||||
- [ ] **Step 7: Run full test suite**
|
||||
|
||||
```bash
|
||||
make test-config && make test-post && make test-ui
|
||||
```
|
||||
Expected: all pass (14/14, 6/6, 25/25).
|
||||
|
||||
- [ ] **Step 8: Commit**
|
||||
|
||||
```bash
|
||||
# Main repo changes (Makefile + test scripts)
|
||||
git add Makefile scripts/test-post.sh scripts/test-form-config.sh tests/
|
||||
git commit -m "fix: update paths for trips/japan-korea-2026 restructure"
|
||||
|
||||
# User repo changes
|
||||
git -C user add pages/02.post/post-form.md docs/demo/
|
||||
git -C user commit -m "fix: update post form parent and demo content paths for trip structure"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: Admin blueprint for trip page type
|
||||
|
||||
**Files:**
|
||||
- Create: `user/themes/intotheeast/blueprints/trip.yaml`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: "Trip" tab in Grav Admin when editing the trip page, with date range and cover image fields
|
||||
|
||||
- [ ] **Step 1: Create `trip.yaml` blueprint**
|
||||
|
||||
`user/themes/intotheeast/blueprints/trip.yaml`:
|
||||
```yaml
|
||||
title: 'Trip'
|
||||
'@extends':
|
||||
type: default
|
||||
context: blueprints://pages
|
||||
|
||||
form:
|
||||
fields:
|
||||
tabs:
|
||||
type: tabs
|
||||
active: 1
|
||||
fields:
|
||||
trip:
|
||||
type: tab
|
||||
title: Trip
|
||||
fields:
|
||||
header.date_start:
|
||||
type: date
|
||||
label: 'Start Date'
|
||||
placeholder: '2026-06-17'
|
||||
help: 'First day of the trip'
|
||||
|
||||
header.date_end:
|
||||
type: date
|
||||
label: 'End Date'
|
||||
placeholder: ''
|
||||
help: 'Leave blank if trip is ongoing'
|
||||
|
||||
header.cover_image:
|
||||
type: text
|
||||
label: 'Cover Image Filename'
|
||||
placeholder: 'cover.jpg'
|
||||
help: 'Used in the trips listing page'
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Verify blueprint appears in Admin**
|
||||
|
||||
Open Grav Admin → Pages → Trips → Japan & Korea 2026 → Edit. Confirm the "Trip" tab appears with start date, end date, cover image fields.
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git -C user add themes/intotheeast/blueprints/trip.yaml
|
||||
git -C user commit -m "feat: add Admin blueprint for trip page type"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Verification
|
||||
|
||||
After all tasks, run end-to-end check:
|
||||
|
||||
1. `make test-config && make test-post && make test-ui` — all must pass
|
||||
2. Navigate to `http://localhost:8081/trips/japan-korea-2026/tracker` — entries display in date order
|
||||
3. Navigate to `http://localhost:8081/trips/japan-korea-2026/map` — entry pins render, GPX polyline renders if a `.gpx` file is present on the trip page
|
||||
4. Navigate to `http://localhost:8081/trips/japan-korea-2026/stats` — stats compute correctly
|
||||
5. Navigate to `http://localhost:8081/trips` — trip listing shows Japan & Korea 2026
|
||||
6. Submit a post via `/post` — new entry appears under `/trips/japan-korea-2026/tracker`
|
||||
7. Grav Admin: edit the trip page → "Trip" tab visible with date fields
|
||||
@@ -0,0 +1,168 @@
|
||||
# Grav 2.0 Upgrade — Design Spec
|
||||
|
||||
**Goal:** Upgrade the intotheeast travel blog from Grav 1.7.x to Grav 2.0 RC on a feature branch, validate full Milestone 1 functionality, and prepare a clean production fresh-install path.
|
||||
|
||||
**Context:** Departure date is 2026-07-15. The production server has never been deployed, so production gets a fresh Grav 2.0 install — no in-place migration required. Local dev uses Docker; production uses PHP 8.4 directly.
|
||||
|
||||
---
|
||||
|
||||
## Scope
|
||||
|
||||
Two tracks:
|
||||
|
||||
1. **Local dev track** — swap Docker image to Grav 2.0, validate all functionality
|
||||
2. **Production track** — update `server-install.sh` and `.env` so `make remote-install` deploys Grav 2.0 fresh
|
||||
|
||||
All work on branch `update-to-2.0`.
|
||||
|
||||
---
|
||||
|
||||
## Compatibility Assessment
|
||||
|
||||
| Component | Status | Action required |
|
||||
|---|---|---|
|
||||
| `form`, `login`, `email`, `error`, `problems`, `flex-objects` | ✅ First-party | Auto-updated to 2.0 versions via GPM |
|
||||
| `shortcode-core` | ✅ First-party | Same |
|
||||
| `cache-on-save` (custom) | ✅ Should work | Add Grav 2.0 compat flag to `blueprints.yaml`; uses `onFormProcessed` which is unchanged |
|
||||
| `shortcode-gallery-plusplus` | ✅ Likely works | Plugin arch unchanged; test and confirm |
|
||||
| `add-page-by-form` | ⚠️ Archived Aug 2024 | Try as-is (plugin arch unchanged, may work); if broken, write a custom replacement |
|
||||
| Custom `intotheeast` theme | ✅ Should work | Twig 3 compat mode covers existing templates; test rendering |
|
||||
| `linuxserver/grav` Docker image | ❌ Not supported | Replace with `getgrav/grav` + `GRAV_CHANNEL=beta` |
|
||||
|
||||
---
|
||||
|
||||
## Track 1 — Local Dev
|
||||
|
||||
### Changes
|
||||
|
||||
**`docker-compose.yml`**
|
||||
|
||||
Replace:
|
||||
```yaml
|
||||
image: lscr.io/linuxserver/grav:latest
|
||||
environment:
|
||||
- PUID=1000
|
||||
- PGID=1000
|
||||
volumes:
|
||||
- ./user:/config/www/user
|
||||
```
|
||||
|
||||
With:
|
||||
```yaml
|
||||
image: getgrav/grav
|
||||
environment:
|
||||
- GRAV_CHANNEL=beta
|
||||
volumes:
|
||||
- ./user:/var/www/html/user
|
||||
```
|
||||
|
||||
**`Makefile`** — three targets reference the linuxserver internal path `/app/www/public`; replace with `/var/www/html`:
|
||||
- `install-plugins`: `docker exec -w /app/www/public` → `docker exec -w /var/www/html`
|
||||
- `demo-load` clear cache: `/app/www/public` → `/var/www/html`
|
||||
- `demo-reset` clear cache: same
|
||||
|
||||
**`user/plugins/cache-on-save/blueprints.yaml`** (create — does not exist yet) — minimal blueprint with Grav 2.0 compat flag:
|
||||
```yaml
|
||||
name: Cache On Save
|
||||
version: 1.0.0
|
||||
description: Clears Grav cache on new-entry form submission
|
||||
author:
|
||||
name: Mischa
|
||||
license: MIT
|
||||
|
||||
dependencies:
|
||||
- { name: grav, version: '>=1.6.0' }
|
||||
|
||||
grav:
|
||||
version: ['1.7', '2.0']
|
||||
```
|
||||
|
||||
**`user/config/system.yaml`** — switch GPM to testing channel so `make setup` resolves 2.0-compatible plugin versions:
|
||||
```yaml
|
||||
gpm:
|
||||
releases: testing
|
||||
```
|
||||
|
||||
### Validation Checklist (smoke test after `make setup`)
|
||||
|
||||
Run in order — stop and investigate if any step fails:
|
||||
|
||||
1. **Site loads** — `http://localhost:8081` returns the tracker page (200, no PHP errors)
|
||||
2. **Admin2 loads** — `/admin` renders the new SPA admin (not the old Twig admin)
|
||||
3. **Login works** — log in via Admin2 with existing credentials
|
||||
4. **Posting form** — submit `/post` form with title + text; entry appears immediately in `/tracker`
|
||||
5. **Photo upload** — submit `/post` form with a photo; image renders in the entry
|
||||
6. **Gallery** — visit an entry with multiple photos; `shortcode-gallery-plusplus` renders gallery with lightbox
|
||||
7. **Cache invalidation** — submit a second post; it appears without a manual cache clear (validates `cache-on-save`)
|
||||
8. **Theme rendering** — check tracker, entry, map, post-form, and stats templates for layout/CSS regressions
|
||||
9. **Playwright suite** — `make test-ui` passes all 25 tests. If any tests fail, investigate whether the failure is a genuine regression (blocker) or a test that needs updating for Admin2's new DOM structure (acceptable — update the test)
|
||||
|
||||
### If `add-page-by-form` fails
|
||||
|
||||
If step 4 fails due to `add-page-by-form` incompatibility, the fallback is to write a custom replacement plugin. The existing `cache-on-save` plugin is a good template — it hooks `onFormProcessed` and that API is unchanged. The replacement would use the same hook to:
|
||||
1. Build the page path and slug from form fields
|
||||
2. Create the page file on disk (same logic `add-page-by-form` does in PHP)
|
||||
3. Clear cache (merging `cache-on-save` functionality)
|
||||
|
||||
This is ~1 day of work and should be planned as a follow-up task if needed.
|
||||
|
||||
---
|
||||
|
||||
## Track 2 — Production (Fresh Install)
|
||||
|
||||
Production has PHP 8.4 (compatible with Grav 2.0's PHP 8.3+ requirement) and has never been deployed.
|
||||
|
||||
### Changes
|
||||
|
||||
**`server-install.sh`** — the download URL for Grav 2.0 RC requires a `?testing` query parameter:
|
||||
|
||||
Current:
|
||||
```bash
|
||||
wget --no-verbose "https://getgrav.org/download/core/grav-admin/$GRAV_VERSION" -O grav-admin.zip
|
||||
```
|
||||
|
||||
Updated (conditionally append `?testing` for pre-release versions, or accept a full URL suffix via env var):
|
||||
```bash
|
||||
wget --no-verbose "https://getgrav.org/download/core/grav-admin/${GRAV_VERSION}${GRAV_CHANNEL_SUFFIX}" -O grav-admin.zip
|
||||
```
|
||||
|
||||
Where `GRAV_CHANNEL_SUFFIX` is `?testing` for RC versions and empty for stable.
|
||||
|
||||
**`.env`** (not committed — edit on the server directly or locally before `make remote-install`) — update:
|
||||
```
|
||||
GRAV_VERSION=2.0.0-rc.9
|
||||
GRAV_CHANNEL_SUFFIX=?testing
|
||||
```
|
||||
|
||||
When Grav 2.0 goes stable, remove `GRAV_CHANNEL_SUFFIX` and update `GRAV_VERSION` to the stable version number.
|
||||
|
||||
**`user/config/system.yaml`** — keep `gpm.releases: testing` (already set in Track 1) so production also installs 2.0-compatible plugin versions.
|
||||
|
||||
### Production deploy
|
||||
|
||||
When local validation passes:
|
||||
```bash
|
||||
make remote-install
|
||||
```
|
||||
|
||||
That's it — fresh Grav 2.0 install from scratch with all plugins, content from Gitea, and the existing `user/` config.
|
||||
|
||||
---
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- MCP server setup (`grav-mcp` Node.js binary) — a separate task after Grav 2.0 is stable on production
|
||||
- Admin2 theming or customization
|
||||
- Grav 2.0 REST API integration
|
||||
- Switching `add-page-by-form` to the API-based approach (only if the plugin breaks)
|
||||
|
||||
---
|
||||
|
||||
## Go/No-Go Criteria
|
||||
|
||||
Ship to production before departure (2026-07-15) **only if**:
|
||||
- All 9 smoke test steps pass
|
||||
- Playwright suite passes
|
||||
- `add-page-by-form` posting workflow works end-to-end (or a custom replacement is in place and tested)
|
||||
|
||||
If any of these fail and cannot be resolved with time to spare before departure, stay on Grav 1.7 for the trip and revisit post-trip.
|
||||
@@ -0,0 +1,157 @@
|
||||
# Dark Mode & Visual Polish Design Spec
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use `superpowers:subagent-driven-development` (recommended) or `superpowers:executing-plans` to implement this plan task-by-task.
|
||||
|
||||
**Goal:** Replace the existing warm-paper light theme with a warm-dark "notebook/sketchbook at night" aesthetic — dark-only, no toggle, no system preference detection. Add paper grain texture, switch to dark terrain map tiles, and tighten typography.
|
||||
|
||||
**Architecture:** All changes are CSS and one Twig template update. Color tokens live in `tokens.css` (swap values, keep names). Grain texture is a pure-CSS SVG noise layer on `body::after`. Map tiles swap in `map.html.twig`. No new dependencies, no JS changes.
|
||||
|
||||
**Approach chosen:** B — color token swap + paper grain + typography refinements. Card/hero treatment (Approach C) deferred to a future visual polish pass.
|
||||
|
||||
**Tech Stack:** CSS custom properties, inline SVG data URI for grain, Stadia Maps tile CDN for dark terrain.
|
||||
|
||||
---
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- Dark-only — no light mode, no `prefers-color-scheme` media query, no toggle
|
||||
- All changes in `user/` — commit with `git -C user`
|
||||
- No new npm/JS dependencies
|
||||
- Existing token names (`--color-paper`, `--color-ink`, etc.) must not change — only values
|
||||
- Teal accent `#1F6B5A` lightens to `#2A8C73` for dark-background contrast
|
||||
- Map tile provider: Stadia Maps Alidade Smooth Dark (free tier; API key needed for production — see Task 2)
|
||||
- `make test-ui` must pass after implementation (25/25 or pre-existing P2 exception)
|
||||
|
||||
---
|
||||
|
||||
## 1. Color System
|
||||
|
||||
Replace all values in `user/themes/intotheeast/css/tokens.css`. Token names are unchanged.
|
||||
|
||||
### Dark palette
|
||||
|
||||
| Token | Old value | New value | Role |
|
||||
|---|---|---|---|
|
||||
| `--color-paper` | `#F7F5F2` | `#1A1814` | Page background — warm near-black |
|
||||
| `--color-canvas` | `#FFFFFF` | `#22201B` | Card surfaces, form backgrounds |
|
||||
| `--color-ink` | `#17171A` | `#EDE8DF` | Primary text — warm cream |
|
||||
| `--color-ink-2` | `#4A4850` | `#B8B0A4` | Body text — muted warm |
|
||||
| `--color-ink-muted` | `#9896A0` | `#7A7268` | Labels, timestamps, captions |
|
||||
| `--color-border` | `#E8E6E3` | `#2E2B25` | Standard dividers |
|
||||
| `--color-border-soft` | `#F0EDEA` | `#252219` | Subtle dividers |
|
||||
| `--color-accent` | `#1F6B5A` | `#2A8C73` | Teal — lightened for dark contrast |
|
||||
| `--color-accent-hover` | `#185647` | `#236655` | Hover/pressed teal |
|
||||
| `--color-accent-light` | `#EBF5F2` | `#1A2E29` | Pale teal tint backgrounds |
|
||||
| `--color-accent-on` | `#FFFFFF` | `#FFFFFF` | Text on accent surfaces (unchanged) |
|
||||
|
||||
### Additional dark-only tokens (add to tokens.css)
|
||||
|
||||
```css
|
||||
--color-surface-raised: #2A2720; /* elevated surfaces: tooltips, hover states */
|
||||
--color-ink-inverse: #17171A; /* text on accent-colored buttons */
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Paper Grain Texture
|
||||
|
||||
Add to `style.css`, in the `body` section:
|
||||
|
||||
```css
|
||||
body::after {
|
||||
content: '';
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
z-index: 9998;
|
||||
opacity: 0.035;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.65' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='200' height='200' filter='url(%23noise)' opacity='1'/%3E%3C/svg%3E");
|
||||
background-repeat: repeat;
|
||||
background-size: 200px 200px;
|
||||
}
|
||||
```
|
||||
|
||||
This overlays a fixed noise texture across the entire viewport. `pointer-events: none` ensures it never blocks clicks. `z-index: 9998` keeps it below any modals or dropdowns (which should use z-index 9999+). Opacity 3.5% — subtle enough to feel like paper texture without being distracting on photography.
|
||||
|
||||
---
|
||||
|
||||
## 3. Map Tiles — Stadia Alidade Smooth Dark
|
||||
|
||||
Replace the tile layer in `user/themes/intotheeast/templates/map.html.twig`.
|
||||
|
||||
**Old:**
|
||||
```javascript
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
||||
}).addTo(map);
|
||||
```
|
||||
|
||||
**New:**
|
||||
```javascript
|
||||
L.tileLayer('https://tiles.stadiamaps.com/tiles/alidade_smooth_dark/{z}/{x}/{y}{r}.png', {
|
||||
maxZoom: 20,
|
||||
attribution: '© <a href="https://stadiamaps.com/">Stadia Maps</a> © <a href="https://openmaptiles.org/">OpenMapTiles</a> © <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
||||
}).addTo(map);
|
||||
```
|
||||
|
||||
**Production note:** Stadia Maps requires a free API key for production domains. Add the key as a query param when ready: `?api_key=YOUR_KEY`. During development on localhost no key is needed. Add a `<!-- TODO: add Stadia API key before launch -->` comment above the tile layer call so it's not forgotten.
|
||||
|
||||
Also update the mini-map tile layer in `dailies.html.twig` (same swap — same tile URL, same attribution).
|
||||
|
||||
---
|
||||
|
||||
## 4. Typography Refinements
|
||||
|
||||
Targeted improvements to `style.css` — not a full type system rewrite.
|
||||
|
||||
### 4a. Entry body readability
|
||||
The entry body text (`--text-md` / 1.125rem) already uses `--leading-normal` (1.65) which is good. Increase the paragraph bottom margin slightly for breathing room:
|
||||
|
||||
```css
|
||||
/* current */
|
||||
.entry-body p { margin-bottom: 1.1em; ... }
|
||||
|
||||
/* new */
|
||||
.entry-body p { margin-bottom: 1.4em; ... }
|
||||
```
|
||||
|
||||
### 4b. Heading tracking
|
||||
DM Serif Display at large sizes benefits from slightly tighter tracking. Find heading rules that currently have `letter-spacing: -0.01em` and tighten to `-0.02em`. Only apply to `h1` and `h2` — smaller headings keep current tracking.
|
||||
|
||||
### 4c. Login form dark surface
|
||||
The login form currently hardcodes `background: #f0f0f0; color: #333` on the secondary button (line ~497 in style.css). Replace with tokens:
|
||||
|
||||
```css
|
||||
/* current */
|
||||
.login-form .button.secondary { background: #f0f0f0; color: #333; ... }
|
||||
|
||||
/* new */
|
||||
.login-form .button.secondary { background: var(--color-canvas); color: var(--color-ink); ... }
|
||||
```
|
||||
|
||||
### 4d. Stats numbers
|
||||
On the stats page, numeric values should feel deliberate. Add `font-variant-numeric: tabular-nums` to the stat value elements so columns of numbers align cleanly.
|
||||
|
||||
---
|
||||
|
||||
## 5. Incidental dark-mode fixes
|
||||
|
||||
Some existing styles use hardcoded light colors that will look wrong in dark mode. Audit and fix these in `style.css`:
|
||||
|
||||
- Any `background: #fff` or `background: white` → `var(--color-canvas)`
|
||||
- Any `color: #333` or similar hardcoded dark text → `var(--color-ink)` or `var(--color-ink-2)`
|
||||
- Any `border: 1px solid #eee` or similar → `var(--color-border)`
|
||||
- Focus outline: currently likely a light-mode color — ensure `outline-color` uses `var(--color-accent)`
|
||||
|
||||
Run a grep for literal hex values after implementation: `grep -n '#[0-9a-fA-F]\{3,6\}' user/themes/intotheeast/css/style.css` — every hit is a candidate to tokenize.
|
||||
|
||||
---
|
||||
|
||||
## Verification
|
||||
|
||||
After implementation:
|
||||
1. `make test-ui` — all tests pass
|
||||
2. Visual check at `http://localhost:8081/trips/japan-korea-2026/dailies` — warm dark background, cream text, teal accents visible, subtle grain
|
||||
3. Visual check at `http://localhost:8081/trips/japan-korea-2026/map` — dark terrain tiles load, GPX polyline visible, entry pins visible
|
||||
4. Check the post form at `/post` — form fields readable on dark canvas, no white-on-white or black-on-black surfaces
|
||||
5. Run the hardcoded-hex grep and confirm any remaining literals are intentional
|
||||
@@ -11,6 +11,9 @@ set -e
|
||||
: "${GITEA_USER:?GITEA_USER is not set}"
|
||||
: "${GITEA_TOKEN:?GITEA_TOKEN is not set}"
|
||||
|
||||
# GRAV_CHANNEL_SUFFIX: optional, set to '?testing' for RC/beta releases (e.g. 2.0.0-rc.9)
|
||||
# Leave unset or empty for stable releases.
|
||||
|
||||
trap 'rm -f ~/.netrc' EXIT
|
||||
|
||||
echo "==> Setting up credentials (temporary)"
|
||||
@@ -19,7 +22,7 @@ chmod 600 ~/.netrc
|
||||
|
||||
echo "==> Downloading Grav $GRAV_VERSION"
|
||||
cd "$WEBROOT"
|
||||
wget --no-verbose "https://getgrav.org/download/core/grav-admin/$GRAV_VERSION" -O grav-admin.zip
|
||||
wget --no-verbose "https://getgrav.org/download/core/grav-admin/${GRAV_VERSION}${GRAV_CHANNEL_SUFFIX:-}" -O grav-admin.zip
|
||||
unzip -oq grav-admin.zip
|
||||
cp -rf grav-admin/. .
|
||||
rm -rf grav-admin grav-admin.zip
|
||||
|
||||
@@ -26,7 +26,7 @@ grep -q "add_page:\|addpage:" "$FORM" && ok "Process action is 'add_page' (plugi
|
||||
|
||||
# Config must be in frontmatter, not in the process block
|
||||
check_grep "pageconfig block exists in frontmatter" "^pageconfig:"
|
||||
check_grep "parent set to /tracker" "parent: '/tracker'"
|
||||
check_grep "parent set to /trips/japan-korea-2026/dailies" "parent: '/trips/japan-korea-2026/dailies'"
|
||||
check_grep "slug_field set (determines entry folder name)" "slug_field:"
|
||||
check_grep "pagefrontmatter block exists in frontmatter" "^pagefrontmatter:"
|
||||
check_grep "template: entry (creates entry.md filename)" "template: entry"
|
||||
|
||||
+31
-28
@@ -7,7 +7,7 @@ set -euo pipefail
|
||||
BASE_URL="${GRAV_BASE_URL:-http://localhost:8081}"
|
||||
USER="${GRAV_TEST_USER:-}"
|
||||
PASS="${GRAV_TEST_PASS:-}"
|
||||
TRACKER="user/pages/01.tracker"
|
||||
TRACKER="user/pages/01.trips/japan-korea-2026/01.dailies"
|
||||
COOKIE_JAR="$(mktemp /tmp/grav-test-cookies.XXXXXX)"
|
||||
PASS_COUNT=0
|
||||
FAIL_COUNT=0
|
||||
@@ -16,8 +16,13 @@ TEST_SLUG=""
|
||||
cleanup() {
|
||||
rm -f "$COOKIE_JAR"
|
||||
if [ -n "$TEST_SLUG" ] && [ -d "$TRACKER/$TEST_SLUG" ]; then
|
||||
rm -rf "$TRACKER/$TEST_SLUG"
|
||||
# Entry files are created by www-data inside Docker; use docker exec to remove
|
||||
if docker exec intotheeast_grav rm -rf "/var/www/html/$TRACKER/$TEST_SLUG" 2>/dev/null; then
|
||||
echo " [cleanup] Removed test entry: $TEST_SLUG"
|
||||
else
|
||||
rm -rf "$TRACKER/$TEST_SLUG" 2>/dev/null || \
|
||||
echo " [cleanup] Warning: could not remove $TEST_SLUG (permission denied — remove manually)"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
trap cleanup EXIT
|
||||
@@ -37,27 +42,27 @@ echo "────────────────────────
|
||||
LOGIN_HTML=$(curl -sf -c "$COOKIE_JAR" -b "$COOKIE_JAR" "$BASE_URL/login") \
|
||||
|| die "Could not reach $BASE_URL/login"
|
||||
|
||||
LOGIN_NONCE=$(echo "$LOGIN_HTML" | grep -o 'name="form-nonce" value="[^"]*"' | head -1 | sed 's/.*value="\([^"]*\)".*/\1/')
|
||||
LOGIN_NONCE=$(echo "$LOGIN_HTML" | grep -o 'name="login-form-nonce" value="[^"]*"' | head -1 | sed 's/.*value="\([^"]*\)".*/\1/')
|
||||
[ -n "$LOGIN_NONCE" ] || die "Could not extract login form nonce — is the site running?"
|
||||
|
||||
# ── Step 2: log in ───────────────────────────────────────────────────────────
|
||||
LOGIN_STATUS=$(curl -sf -o /dev/null -w "%{http_code}" \
|
||||
-c "$COOKIE_JAR" -b "$COOKIE_JAR" \
|
||||
-L \
|
||||
-d "username=${USER}&password=${PASS}&form-nonce=${LOGIN_NONCE}&task=login" \
|
||||
-d "username=${USER}&password=${PASS}&login-form-nonce=${LOGIN_NONCE}&task=login.login" \
|
||||
"$BASE_URL/login")
|
||||
|
||||
# After login, check we can access /post (302 → 200 means logged in)
|
||||
POST_STATUS=$(curl -sf -o /dev/null -w "%{http_code}" \
|
||||
-c "$COOKIE_JAR" -b "$COOKIE_JAR" \
|
||||
"$BASE_URL/post")
|
||||
# After login, fetch /post and verify we see the post form (not the login form)
|
||||
# /post returns 200 for both auth and unauth users — check for form-nonce to confirm login
|
||||
POST_CHECK_HTML=$(curl -sf -c "$COOKIE_JAR" -b "$COOKIE_JAR" "$BASE_URL/post") \
|
||||
|| die "Could not reach $BASE_URL/post"
|
||||
|
||||
[ "$POST_STATUS" = "200" ] && ok "Login succeeded and /post is accessible" \
|
||||
|| die "Login failed or /post returned $POST_STATUS — check GRAV_TEST_USER / GRAV_TEST_PASS"
|
||||
POST_STATUS=$(echo "$POST_CHECK_HTML" | grep -c 'name="form-nonce"' || true)
|
||||
[ "$POST_STATUS" -gt 0 ] && ok "Login succeeded and /post is accessible" \
|
||||
|| die "Login failed (post form not visible) — check GRAV_TEST_USER / GRAV_TEST_PASS"
|
||||
|
||||
# ── Step 3: get post form + nonce ────────────────────────────────────────────
|
||||
POST_HTML=$(curl -sf -c "$COOKIE_JAR" -b "$COOKIE_JAR" "$BASE_URL/post") \
|
||||
|| die "Could not fetch post form"
|
||||
# ── Step 3: extract post form nonce from already-fetched HTML ────────────────
|
||||
POST_HTML="$POST_CHECK_HTML"
|
||||
|
||||
POST_NONCE=$(echo "$POST_HTML" | grep -o 'name="form-nonce" value="[^"]*"' | head -1 | sed 's/.*value="\([^"]*\)".*/\1/')
|
||||
[ -n "$POST_NONCE" ] || die "Could not extract post form nonce"
|
||||
@@ -84,33 +89,31 @@ ok "Form submitted"
|
||||
# ── Step 5: verify entry exists on disk ─────────────────────────────────────
|
||||
sleep 1 # give Grav a moment to write the file
|
||||
|
||||
# Look for the entry — slug might have slight timestamp variation
|
||||
FOUND=$(find "$TRACKER" -name "entry.md" -newer "$TRACKER/2026-06-17.entry/entry.md" \
|
||||
-not -path "*/2026-*" 2>/dev/null | head -1)
|
||||
# Find an entry containing the test title — search all .md and .en.md files
|
||||
# add-page-by-form may produce date-based slugs in various formats
|
||||
ENTRY_FILE=$(grep -rl "$TEST_TITLE" "$TRACKER" --include="*.md" 2>/dev/null | head -1)
|
||||
|
||||
# Also look for today's dated entries
|
||||
FOUND_TODAY=$(find "$TRACKER" -maxdepth 1 -type d -name "$(date '+%Y-%m-%d')*" 2>/dev/null | head -1)
|
||||
|
||||
if [ -n "$FOUND_TODAY" ]; then
|
||||
TEST_SLUG=$(basename "$FOUND_TODAY")
|
||||
if [ -n "$ENTRY_FILE" ]; then
|
||||
TEST_SLUG=$(basename "$(dirname "$ENTRY_FILE")")
|
||||
ok "Entry created on disk: $TEST_SLUG"
|
||||
|
||||
# Verify it has an entry.md inside
|
||||
if [ -f "$TRACKER/$TEST_SLUG/entry.md" ]; then
|
||||
ok "entry.md exists inside the entry folder"
|
||||
# Verify file name is entry.md or entry.en.md (template-named file)
|
||||
ENTRY_BASENAME=$(basename "$ENTRY_FILE")
|
||||
if [ "$ENTRY_BASENAME" = "entry.md" ] || [ "$ENTRY_BASENAME" = "entry.en.md" ]; then
|
||||
ok "Entry file exists: $ENTRY_BASENAME"
|
||||
else
|
||||
fail "Entry folder exists but entry.md is missing"
|
||||
fail "Entry file has unexpected name: $ENTRY_BASENAME (expected entry.md or entry.en.md)"
|
||||
fi
|
||||
|
||||
# Verify the title is in the frontmatter
|
||||
if grep -q "$TEST_TITLE" "$TRACKER/$TEST_SLUG/entry.md"; then
|
||||
if grep -q "$TEST_TITLE" "$ENTRY_FILE"; then
|
||||
ok "Title appears in entry frontmatter"
|
||||
else
|
||||
fail "Title not found in entry.md — frontmatter may be malformed"
|
||||
fail "Title not found in $ENTRY_BASENAME — frontmatter may be malformed"
|
||||
fi
|
||||
else
|
||||
fail "No entry created on disk — form processing failed silently"
|
||||
echo " Expected a folder matching: $TRACKER/$(date '+%Y-%m-%d')-*/"
|
||||
echo " Searched $TRACKER for files containing: $TEST_TITLE"
|
||||
fi
|
||||
|
||||
# ── Result ───────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// @ts-check
|
||||
// Tests: T1–T5 — tracker feed and individual entry pages
|
||||
// Tests: T1–T5 — dailies feed and individual entry pages
|
||||
const { test, expect } = require('@playwright/test');
|
||||
|
||||
// Known fixture entries that always exist in the repo
|
||||
@@ -12,18 +12,18 @@ const KNOWN_COUNTRY = 'Japan';
|
||||
const NEWER_SLUG = '2026-06-17.entry'; // most recent fixture (June 17)
|
||||
const OLDER_SLUG = '2026-03-25-1540-wheels-down-narita.entry'; // oldest fixture (March 25)
|
||||
|
||||
// ── T1: Tracker page loads ─────────────────────────────────────────────────────
|
||||
test('T1: /tracker loads and shows at least one entry card', async ({ page }) => {
|
||||
await page.goto('/tracker');
|
||||
// ── T1: Dailies page loads ─────────────────────────────────────────────────────
|
||||
test('T1: /trips/japan-korea-2026/dailies loads and shows at least one entry card', async ({ page }) => {
|
||||
await page.goto('/trips/japan-korea-2026/dailies');
|
||||
await expect(page.locator('.entry-card').first()).toBeVisible();
|
||||
await expect(page.locator('.site-header')).toBeVisible();
|
||||
});
|
||||
|
||||
// ── T2: Entries are newest-first ──────────────────────────────────────────────
|
||||
// Verify using two known fixture entries rather than all entries
|
||||
// (the tracker may contain noisy test-run debris with inconsistent dates).
|
||||
test('T2: tracker shows newer entries before older entries', async ({ page }) => {
|
||||
await page.goto('/tracker');
|
||||
// (the dailies may contain noisy test-run debris with inconsistent dates).
|
||||
test('T2: dailies shows newer entries before older entries', async ({ page }) => {
|
||||
await page.goto('/trips/japan-korea-2026/dailies');
|
||||
|
||||
// Both fixture entries must be visible on the page
|
||||
const newerCard = page.locator(`.entry-card a[href*="${NEWER_SLUG}"]`);
|
||||
@@ -44,15 +44,15 @@ test('T2: tracker shows newer entries before older entries', async ({ page }) =>
|
||||
});
|
||||
|
||||
// ── T3: Individual entry page loads ───────────────────────────────────────────
|
||||
test('T3: individual entry page loads at /tracker/{slug}', async ({ page }) => {
|
||||
await page.goto(`/tracker/${KNOWN_SLUG}`);
|
||||
test('T3: individual entry page loads at /trips/japan-korea-2026/dailies/{slug}', async ({ page }) => {
|
||||
await page.goto(`/trips/japan-korea-2026/dailies/${KNOWN_SLUG}`);
|
||||
await expect(page.locator('article.entry')).toBeVisible();
|
||||
await expect(page.locator('.site-header')).toBeVisible();
|
||||
});
|
||||
|
||||
// ── T4: Entry page shows title, date, and content ─────────────────────────────
|
||||
test('T4: entry page shows title and body content', async ({ page }) => {
|
||||
await page.goto(`/tracker/${KNOWN_SLUG}`);
|
||||
await page.goto(`/trips/japan-korea-2026/dailies/${KNOWN_SLUG}`);
|
||||
await expect(page.locator('.entry-title')).toContainText(KNOWN_TITLE);
|
||||
await expect(page.locator('.entry-body')).not.toBeEmpty();
|
||||
await expect(page.locator('time.entry-date')).toBeVisible();
|
||||
@@ -60,7 +60,7 @@ test('T4: entry page shows title and body content', async ({ page }) => {
|
||||
|
||||
// ── T5: Entry page shows location when present ────────────────────────────────
|
||||
test('T5: entry page shows city and country when set', async ({ page }) => {
|
||||
await page.goto(`/tracker/${KNOWN_SLUG}`);
|
||||
await page.goto(`/trips/japan-korea-2026/dailies/${KNOWN_SLUG}`);
|
||||
await expect(page.locator('.entry-location')).toContainText(KNOWN_CITY);
|
||||
await expect(page.locator('.entry-location')).toContainText(KNOWN_COUNTRY);
|
||||
});
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
const TRACKER_DIR = path.join(__dirname, '../../user/pages/01.tracker');
|
||||
const TRACKER_DIR = path.join(__dirname, '../../user/pages/01.trips/japan-korea-2026/01.dailies');
|
||||
|
||||
/**
|
||||
* Wait for all filepond items to finish XHR upload.
|
||||
|
||||
+13
-13
@@ -2,42 +2,42 @@
|
||||
// Tests: N1–N5 — page loads and navigation links
|
||||
const { test, expect } = require('@playwright/test');
|
||||
|
||||
// ── N1: /tracker renders ──────────────────────────────────────────────────────
|
||||
test('N1: /tracker page loads with site header', async ({ page }) => {
|
||||
// ── N1: /trips/japan-korea-2026/dailies renders ───────────────────────────────
|
||||
test('N1: /trips/japan-korea-2026/dailies page loads with site header', async ({ page }) => {
|
||||
const errors = [];
|
||||
page.on('pageerror', e => errors.push(e.message));
|
||||
|
||||
await page.goto('/tracker');
|
||||
await page.goto('/trips/japan-korea-2026/dailies');
|
||||
await expect(page.locator('.site-header')).toBeVisible();
|
||||
await expect(page).toHaveTitle(/Into the East/i);
|
||||
expect(errors).toHaveLength(0);
|
||||
});
|
||||
|
||||
// ── N2: /map renders without JS errors ───────────────────────────────────────
|
||||
test('N2: /map page loads without JS errors', async ({ page }) => {
|
||||
// ── N2: /trips/japan-korea-2026/map renders without JS errors ─────────────────
|
||||
test('N2: /trips/japan-korea-2026/map page loads without JS errors', async ({ page }) => {
|
||||
const errors = [];
|
||||
page.on('pageerror', e => errors.push(e.message));
|
||||
|
||||
await page.goto('/map');
|
||||
await page.goto('/trips/japan-korea-2026/map');
|
||||
await expect(page.locator('.site-header')).toBeVisible();
|
||||
expect(errors).toHaveLength(0);
|
||||
});
|
||||
|
||||
// ── N3: /stats renders ───────────────────────────────────────────────────────
|
||||
test('N3: /stats page loads with site header', async ({ page }) => {
|
||||
// ── N3: /trips/japan-korea-2026/stats renders ─────────────────────────────────
|
||||
test('N3: /trips/japan-korea-2026/stats page loads with site header', async ({ page }) => {
|
||||
const errors = [];
|
||||
page.on('pageerror', e => errors.push(e.message));
|
||||
|
||||
await page.goto('/stats');
|
||||
await page.goto('/trips/japan-korea-2026/stats');
|
||||
await expect(page.locator('.site-header')).toBeVisible();
|
||||
expect(errors).toHaveLength(0);
|
||||
});
|
||||
|
||||
// ── N4: "Journal" nav link goes to /tracker ───────────────────────────────────
|
||||
test('N4: Journal nav link navigates to /tracker', async ({ page }) => {
|
||||
// ── N4: "Journal" nav link goes to /dailies ───────────────────────────────────
|
||||
test('N4: Journal nav link navigates to /dailies', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.click('nav a[href*="tracker"]');
|
||||
await expect(page).toHaveURL(/\/tracker/);
|
||||
await page.click('nav a[href*="dailies"]');
|
||||
await expect(page).toHaveURL(/\/dailies/);
|
||||
});
|
||||
|
||||
// ── N5: "Map" nav link goes to /map ──────────────────────────────────────────
|
||||
|
||||
@@ -16,7 +16,7 @@ test.afterAll(() => {
|
||||
});
|
||||
|
||||
// ── P1: Post without photo ─────────────────────────────────────────────────────
|
||||
test('P1: post text-only entry → created on disk and visible on /tracker', async ({ page }) => {
|
||||
test('P1: post text-only entry → created on disk and visible on /dailies', async ({ page }) => {
|
||||
const tag = `p1-${Date.now()}`;
|
||||
const title = `UI Test ${tag}`;
|
||||
|
||||
@@ -39,12 +39,12 @@ test('P1: post text-only entry → created on disk and visible on /tracker', asy
|
||||
const photos = fs.readdirSync(entryDir).filter(f => /\.(jpg|jpeg|png|webp|heic)$/i.test(f));
|
||||
expect(photos.length, 'Text-only entry should have no photos').toBe(0);
|
||||
|
||||
await page.goto('/tracker');
|
||||
await page.goto('/trips/japan-korea-2026/dailies');
|
||||
await expect(page.locator('body')).toContainText(tag);
|
||||
});
|
||||
|
||||
// ── P2: Post with photo ────────────────────────────────────────────────────────
|
||||
test('P2: post entry with photo → photo saved in entry folder and visible on /tracker', async ({ page }) => {
|
||||
test('P2: post entry with photo → photo saved in entry folder and visible on /dailies', async ({ page }) => {
|
||||
const tag = `p2-${Date.now()}`;
|
||||
const title = `UI Test ${tag}`;
|
||||
|
||||
@@ -70,7 +70,7 @@ test('P2: post entry with photo → photo saved in entry folder and visible on /
|
||||
const photos = fs.readdirSync(entryDir).filter(f => /\.(jpg|jpeg|png|webp|heic)$/i.test(f));
|
||||
expect(photos.length, 'At least one photo should be saved').toBeGreaterThan(0);
|
||||
|
||||
await page.goto('/tracker');
|
||||
await page.goto('/trips/japan-korea-2026/dailies');
|
||||
await expect(page.locator('body')).toContainText(tag);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user