docs: restructure docs/ into guides/ reference/ working/ research/
This commit is contained in:
@@ -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
|
||||
@@ -0,0 +1,226 @@
|
||||
# Home Page & Content Flow Design Spec
|
||||
|
||||
**Goal:** Replace the redirect-based home page with a real home page showing the active trip's feed and map side by side, add a proper past-trips archive, enrich the trip page with a sticky sidebar index, and introduce story cards into all feeds.
|
||||
|
||||
**Architecture:** Pure Twig + CSS changes on top of the existing Grav stack. The home page is a new Grav page (`00.home/home.md`) with a new `home.html.twig` template. Feeds (home + dailies) are extended to merge journal entries and story entries into one chronological collection, with stories rendered as visually distinct cards. No new plugins, no build pipeline.
|
||||
|
||||
**Tech Stack:** Grav CMS (PHP/Twig), Vanilla CSS, Leaflet.js (already loaded in `dailies.html.twig`)
|
||||
|
||||
---
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- All changes in `user/` — commit with `git -C user`
|
||||
- No new Grav plugins
|
||||
- No JS framework — all interactivity is vanilla JS
|
||||
- No build pipeline — CSS shipped as plain files
|
||||
- Existing token names in `tokens.css` must not change
|
||||
- Theme directory: `user/themes/intotheeast/`
|
||||
- Active trip slug: `config.site.active_trip` (set in `user/config/site.yaml`)
|
||||
- `system.yaml` `home.alias` redirect must be removed — `/` becomes a real page
|
||||
- `config.site.active_trip` in `site.yaml` must always be set to a trip slug (even between trips, point it at the last trip) — the home page template has no fallback if this value is empty
|
||||
|
||||
---
|
||||
|
||||
## 1. URL Structure
|
||||
|
||||
| URL | Page file | Template |
|
||||
|---|---|---|
|
||||
| `/` | `user/pages/00.home/home.md` | `home.html.twig` (new) |
|
||||
| `/trips` | `user/pages/01.trips/trips.md` | `trips.html.twig` (update existing) |
|
||||
| `/trips/<slug>/` | existing | `trip.html.twig` (update existing) |
|
||||
| `/trips/<slug>/dailies` | existing | `dailies.html.twig` (update existing) |
|
||||
|
||||
**system.yaml change:** Remove `home: alias: /trips/japan-korea-2026/dailies`. Set `home: alias: /` (or remove the alias entirely so Grav serves the `00.home` page at `/`).
|
||||
|
||||
---
|
||||
|
||||
## 2. Home Page (`/`)
|
||||
|
||||
### Layout
|
||||
|
||||
Two-column CSS grid on desktop. Map left (~45%), entry feed right (~55%).
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ [Trip name] · 31 journal entries · 4 stories │
|
||||
├────────────────────────┬────────────────────────────────┤
|
||||
│ │ [story card] │
|
||||
│ Leaflet map │ [journal card] │
|
||||
│ (sticky, │ [journal card] │
|
||||
│ 45% width) │ [story card] │
|
||||
│ │ ... │
|
||||
└────────────────────────┴────────────────────────────────┘
|
||||
```
|
||||
|
||||
- Map is `position: sticky; top: 0; height: 100vh`
|
||||
- Entry feed is scrollable, sorted descending by date
|
||||
- Feed contains both journal entries and story entries merged (see §5)
|
||||
|
||||
### Data
|
||||
|
||||
```twig
|
||||
{% set slug = config.site.active_trip %}
|
||||
{% set trip = grav.pages.find('/trips/' ~ slug) %}
|
||||
{% set dailies = grav.pages.find('/trips/' ~ slug ~ '/dailies') %}
|
||||
{% set stories_page = grav.pages.find('/trips/' ~ slug ~ '/stories') %}
|
||||
{% set journal_entries = dailies ? dailies.children.published().order('date', 'desc') : [] %}
|
||||
{% set story_entries = stories_page ? stories_page.children.published() : [] %}
|
||||
{# merge and sort handled in template — see §5 #}
|
||||
```
|
||||
|
||||
### Map
|
||||
|
||||
Reuse the existing Leaflet setup from `dailies.html.twig` (`feed-map`). Markers come from journal entries with `lat`/`lng` in frontmatter. GPX route line loaded from trip page media if present (same pattern as `map.html.twig`). Clicking a marker scrolls to that entry card in the feed (use `data-entry-id` on cards + `scrollIntoView`).
|
||||
|
||||
### Mobile
|
||||
|
||||
Stack vertically: map on top at `height: 40vh`, feed below. No hamburger needed — simpler than the dedicated map page.
|
||||
|
||||
---
|
||||
|
||||
## 3. Past Trips Archive (`/trips`)
|
||||
|
||||
Update `trips.html.twig`. Show each trip as a card, sorted newest first.
|
||||
|
||||
Each card contains:
|
||||
- Trip title (links to `/trips/<slug>/`)
|
||||
- Date range: `date_start` – `date_end` from trip page frontmatter (show "Ongoing" if no `date_end`)
|
||||
- Entry count: journal entries + story entries counted separately
|
||||
|
||||
```twig
|
||||
{% set journal_count = grav.pages.find(trip.route ~ '/dailies').children.published()|length %}
|
||||
{% set story_count = grav.pages.find(trip.route ~ '/stories').children.published()|length %}
|
||||
```
|
||||
|
||||
Display: **31 journal entries · 4 stories**
|
||||
|
||||
The active trip appears as the first card. No special treatment needed beyond chronological ordering — it naturally sits at the top.
|
||||
|
||||
---
|
||||
|
||||
## 4. Trip Page (`/trips/<slug>/`)
|
||||
|
||||
Update `trip.html.twig`. Current state: shows title, dates, nav links, 3 recent entries. Target state:
|
||||
|
||||
### Header (update existing `.trip-hero`)
|
||||
```
|
||||
Japan & Korea 2026
|
||||
Jun 2026 – Aug 2026 · 31 journal entries · 4 stories
|
||||
```
|
||||
|
||||
Add entry counts below the date line (small, secondary text).
|
||||
|
||||
### Two-column layout
|
||||
|
||||
Add a right sidebar alongside the existing content:
|
||||
|
||||
```
|
||||
┌──────────────────────────────────┬──────────────────────┐
|
||||
│ [full chronological feed] │ Journal │
|
||||
│ (centered, existing max-width) │ Jun 19 Kyoto │
|
||||
│ │ Jun 18 Osaka │
|
||||
│ │ ... │
|
||||
│ │ │
|
||||
│ │ Stories │
|
||||
│ │ The night train │
|
||||
│ │ First ramen │
|
||||
└──────────────────────────────────┴──────────────────────┘
|
||||
```
|
||||
|
||||
- Right sidebar: `position: sticky; top: 1rem`
|
||||
- Two sections: **Journal** (list of entry titles as jump-links via `#entry-<slug>`) and **Stories** (same)
|
||||
- Each item in the sidebar is a jump-link to `#entry-<slug>` anchor on the feed card
|
||||
- Feed comes from `dailies.children` + `stories.children` merged (see §5)
|
||||
- On mobile: sidebar collapses to hidden (toggle-able or just hidden — defer this decision to implementation)
|
||||
|
||||
### Remove the current "Recent entries" section
|
||||
|
||||
The right-sidebar index replaces it. The full merged feed is the main content.
|
||||
|
||||
---
|
||||
|
||||
## 5. Story Cards in Feeds (home + trip page)
|
||||
|
||||
Feeds in both `home.html.twig` and `trip.html.twig` show a merged chronological list of journal entries and story entries.
|
||||
|
||||
### Merging collections in Twig
|
||||
|
||||
Grav doesn't natively merge two page collections and sort them. Use a Twig loop to build a combined array:
|
||||
|
||||
```twig
|
||||
{% set all_items = [] %}
|
||||
{% for e in journal_entries %}
|
||||
{% set all_items = all_items|merge([{'type': 'journal', 'page': e, 'date': e.date}]) %}
|
||||
{% endfor %}
|
||||
{% for s in story_entries %}
|
||||
{% set all_items = all_items|merge([{'type': 'story', 'page': s, 'date': s.date}]) %}
|
||||
{% endfor %}
|
||||
{# Sort descending by date #}
|
||||
{% set all_items = all_items|sort((a, b) => a.date < b.date ? 1 : -1) %}
|
||||
```
|
||||
|
||||
### Journal card (existing format, unchanged)
|
||||
|
||||
```html
|
||||
<article class="entry-card" id="entry-{{ item.page.slug }}" data-lat="{{ item.page.header.lat }}" data-lng="{{ item.page.header.lng }}">
|
||||
<!-- existing card markup -->
|
||||
</article>
|
||||
```
|
||||
|
||||
Add `id` and `data-lat`/`data-lng` attributes for sidebar jump-links and map sync.
|
||||
|
||||
### Story card (new)
|
||||
|
||||
```html
|
||||
<article class="entry-card entry-card--story" id="entry-{{ item.page.slug }}">
|
||||
<a class="entry-card-inner" href="{{ item.page.url }}">
|
||||
{% if hero %}
|
||||
<div class="entry-card-photo entry-card-photo--story">
|
||||
<img src="{{ hero.cropResize(720, 405).url }}" alt="{{ item.page.title }}" loading="lazy">
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="entry-card-body">
|
||||
<span class="story-badge">✦ Story</span>
|
||||
<h2 class="entry-title">{{ item.page.title }}</h2>
|
||||
</div>
|
||||
</a>
|
||||
</article>
|
||||
```
|
||||
|
||||
**Visual treatment:** `entry-card--story` gets a teal left border (3px, `var(--color-accent)`) and no excerpt text. The `✦ Story` badge is small-caps, accent color.
|
||||
|
||||
### Story page (full-screen)
|
||||
|
||||
Story pages (`/trips/<slug>/stories/<story-slug>`) use `stories.html.twig` (already exists). That template should:
|
||||
- Override `{% block nav %}` to render **only** a fixed escape link — not an empty block, not the global nav
|
||||
- Escape link: `← Back` fixed top-left, links to `page.parent.parent.url` (the trip page)
|
||||
|
||||
Implementation of the Snowfall-style scroll-snap interior is **deferred to Milestone 3** — this spec only covers the story card in the feed and the escape link on the story page.
|
||||
|
||||
---
|
||||
|
||||
## 6. Navigation
|
||||
|
||||
Update `base.html.twig` nav. Current: single "Journal" link pointing to active trip dailies. New:
|
||||
|
||||
- **Home** → `/`
|
||||
- **Past Trips** → `/trips`
|
||||
|
||||
The per-trip sub-nav (Journal / Map / Stats / Stories) stays on the trip page — it is not in the global nav.
|
||||
|
||||
---
|
||||
|
||||
## 7. Files Changed
|
||||
|
||||
| File | Change |
|
||||
|---|---|
|
||||
| `user/pages/00.home/home.md` | **Create** — new home page, `template: home` |
|
||||
| `user/themes/intotheeast/templates/home.html.twig` | **Create** — side-by-side map + feed |
|
||||
| `user/themes/intotheeast/templates/trips.html.twig` | **Update** — trip cards with counts |
|
||||
| `user/themes/intotheeast/templates/trip.html.twig` | **Update** — counts in header, two-column + sidebar |
|
||||
| `user/themes/intotheeast/templates/dailies.html.twig` | **Update** — merge stories into feed, story cards, add `id`/`data-` attrs |
|
||||
| `user/themes/intotheeast/templates/stories.html.twig` | **Update** — add escape link, remove global nav |
|
||||
| `user/themes/intotheeast/templates/partials/base.html.twig` | **Update** — new nav links |
|
||||
| `user/themes/intotheeast/css/style.css` | **Update** — home layout, story card styles, sidebar styles |
|
||||
| `user/config/system.yaml` | **Update** — remove `home.alias` redirect |
|
||||
@@ -0,0 +1,143 @@
|
||||
# Stats Redesign — Design Spec
|
||||
|
||||
*2026-06-19*
|
||||
|
||||
---
|
||||
|
||||
## Goal
|
||||
|
||||
Expand trip statistics with new data points already available in entry frontmatter, add smart distance labelling based on whether GPX files are present, and add a dedicated cycling stats panel derived from GPX track data.
|
||||
|
||||
---
|
||||
|
||||
## Data sources
|
||||
|
||||
| Source | Fields available |
|
||||
|---|---|
|
||||
| Entry frontmatter | `date`, `lat`, `lng`, `location_city`, `location_country`, `weather_temp_c` |
|
||||
| Trip page media | `.gpx` files (Komoot exports) |
|
||||
| GPX trackpoints | `lat`, `lon`, `<ele>` (meters), `<time>` (ISO 8601, 1s resolution) |
|
||||
| GPX track metadata | `<type>` (e.g. `racebike`, `hiking`) |
|
||||
|
||||
---
|
||||
|
||||
## Main stats block — changes
|
||||
|
||||
The existing 4-stat grid expands to 6 stats. Both `stats.html.twig` and the inline toggle in `trip.html.twig` get the same treatment.
|
||||
|
||||
### Stats
|
||||
|
||||
| Stat | Label | Source | Notes |
|
||||
|---|---|---|---|
|
||||
| Days on the road | `days on the road` | `(now - first entry date) / 86400` | Unchanged |
|
||||
| Entries posted | `entries posted` | `all_entries\|length` | Unchanged |
|
||||
| Countries visited | `countries visited` | Deduplicated `location_country` | Unchanged; country list shown below grid |
|
||||
| **Cities visited** | `cities visited` | Deduplicated `location_city` | New; same dedup logic as countries |
|
||||
| Distance | see below | see below | Label + icon vary by mode |
|
||||
| **Temperature range** | `°C range` | `min(weather_temp_c)` – `max(weather_temp_c)` | New; shown as e.g. `−2 → 28 °C` |
|
||||
|
||||
### Distance stat — two modes
|
||||
|
||||
**Mode A — GPX present** (any `.gpx` files exist on the trip page):
|
||||
- Value: sum of haversine distances between all consecutive trackpoints across all GPX files
|
||||
- Label: `km cycled`
|
||||
- Icon: cycling icon (or activity-specific icon — see Icon system below)
|
||||
|
||||
**Mode B — No GPX files:**
|
||||
- Value: sum of haversine distances between consecutive entry `lat`/`lng` points (current behaviour)
|
||||
- Label: `km roamed`
|
||||
- Icon: generic travel icon (compass / globe)
|
||||
|
||||
Edge case — trip has both GPX files and many geo-spread entries (e.g. a mixed cycling + backpacking trip): use Mode A (GPX total only). This may understate total travel distance. Accepted limitation; revisit when transport mode is implemented.
|
||||
|
||||
---
|
||||
|
||||
## Cycling panel
|
||||
|
||||
A separate expandable panel, independent of the main stats toggle. Only rendered when GPX files are present on the trip page.
|
||||
|
||||
### Button placement
|
||||
|
||||
Sits next to the existing Stats button in the filter bar area:
|
||||
|
||||
```
|
||||
[ All ] [ Journal ] [ Stories ] [ Stats ] [ Cycling ]
|
||||
```
|
||||
|
||||
The Cycling button is hidden entirely when no GPX files exist. Detection: server-side Twig filters `trip_page.media.all` for `.gpx` extension (same mechanism the map template already uses) and sets a boolean passed to the template.
|
||||
|
||||
### Stats shown
|
||||
|
||||
| Stat | Unit | How computed |
|
||||
|---|---|---|
|
||||
| Distance | km | Sum haversine between all trackpoints (same value as main stats Mode A) |
|
||||
| Elevation gain | m ↑ | Sum of positive `<ele>` differences (threshold: > 1 m per step to filter GPS noise) |
|
||||
| Elevation loss | m ↓ | Sum of negative `<ele>` differences (same threshold) |
|
||||
| Highest point | m | `max(<ele>)` across all files |
|
||||
| Lowest point | m | `min(<ele>)` across all files |
|
||||
| Moving time | h:mm | Total time excluding segments where computed speed < 1 km/h |
|
||||
| Average speed | km/h | Distance ÷ moving time |
|
||||
|
||||
Max speed is explicitly excluded — GPS noise at 1-second resolution produces unreliable spikes.
|
||||
|
||||
### Icon system
|
||||
|
||||
The GPX `<type>` tag on the track element drives the icon shown in both the main stats distance block and the cycling panel header:
|
||||
|
||||
| `<type>` value | Icon |
|
||||
|---|---|
|
||||
| `racebike` | Road bike |
|
||||
| `touringbicycle` | Touring bike |
|
||||
| `mtb` | Mountain bike |
|
||||
| `cycling` (generic) | Generic bike |
|
||||
| `hiking` | Hiking boot |
|
||||
| `hike` | Hiking boot |
|
||||
| Any unrecognised value | Generic bike (fallback) |
|
||||
|
||||
When multiple GPX files exist with different types, use the type from the first file. This is an acceptable heuristic for now.
|
||||
|
||||
---
|
||||
|
||||
## GPX parsing — algorithm
|
||||
|
||||
All parsing is client-side JavaScript. The template passes the list of GPX file URLs to a JS variable; JS fetches and processes them sequentially.
|
||||
|
||||
```
|
||||
for each GPX file URL:
|
||||
fetch(url) → text → DOMParser → XML document
|
||||
extract all <trkpt> elements → array of { lat, lon, ele, time }
|
||||
append to master trackpoint array
|
||||
|
||||
compute over master array:
|
||||
distance = sum haversine(p[i-1], p[i]) for i in 1..n
|
||||
ele_gain = sum max(0, ele[i] - ele[i-1] - 1) for i in 1..n (1m threshold)
|
||||
ele_loss = sum max(0, ele[i-1] - ele[i] - 1)
|
||||
highest = max(ele)
|
||||
lowest = min(ele)
|
||||
speed[i] = haversine(p[i-1], p[i]) / (time[i] - time[i-1]) in km/h
|
||||
moving_time = sum (time[i] - time[i-1]) where speed[i] >= 1 km/h
|
||||
avg_speed = distance / moving_time
|
||||
```
|
||||
|
||||
The 1 m elevation threshold filters out the flat-line noise visible in the Komoot files (many consecutive identical `<ele>` values).
|
||||
|
||||
---
|
||||
|
||||
## Template changes
|
||||
|
||||
| File | Change |
|
||||
|---|---|
|
||||
| `stats.html.twig` | Add cities stat, temp range stat; update distance stat with mode detection + label + icon |
|
||||
| `trip.html.twig` | Same stats changes; add Cycling button (hidden if no GPX); add cycling panel block with JS parsing |
|
||||
|
||||
The cycling panel JS and the distance mode detection JS share the same GPX fetch logic — extract into a single `parseGpxFiles(urls)` function called once, results used by both.
|
||||
|
||||
---
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Transport mode per entry (deferred — tracked separately)
|
||||
- Weather breakdown (dropped — depends on free-text consistency)
|
||||
- Max speed stat (dropped — GPS noise)
|
||||
- Lowest point shown in main stats (cycling panel only)
|
||||
- Per-file breakdown (one aggregate across all GPX files)
|
||||
@@ -0,0 +1,87 @@
|
||||
# Trip Page Filter Bar — Design Spec
|
||||
|
||||
**Date:** 2026-06-19
|
||||
**Status:** Approved
|
||||
|
||||
## Problem
|
||||
|
||||
The trip page (`trip.html.twig`) shows a map + combined feed, but the three nav links below the title (Journal · Stats · Stories) navigate *away* from the page, losing the map context. The links are also unstyled (`trip-nav` has no CSS). Stories links to a stub. Stats is a separate page. The user can only return via browser back.
|
||||
|
||||
## Goal
|
||||
|
||||
Make the trip page self-contained. Filtering, stats, and content switching all happen in place. Navigation away from the trip page only happens when the user clicks into an individual entry or story.
|
||||
|
||||
## Design
|
||||
|
||||
### Filter bar (replaces `.trip-nav`)
|
||||
|
||||
Three mutually exclusive pill buttons above the feed, plus a Stats toggle to the right:
|
||||
|
||||
```
|
||||
[ All content ] [ Journal ] [ Stories ] [ Stats ↕ ]
|
||||
```
|
||||
|
||||
- **Default state:** "All content" active
|
||||
- **Behavior:** selecting a filter hides non-matching cards via JS (`display: none` toggle); no page navigation
|
||||
- **Stats** sits right-aligned, visually separated from the filter group; it is a toggle, not a filter
|
||||
|
||||
### Content filtering
|
||||
|
||||
Each `<article>` card in `trip.html.twig` gets a `data-type` attribute:
|
||||
|
||||
- Journal entries: `data-type="journal"`
|
||||
- Story entries: `data-type="story"`
|
||||
|
||||
JS selects all `[data-type]` cards and shows/hides based on the active filter button. Three states:
|
||||
|
||||
| Active button | Visible cards |
|
||||
|---|---|
|
||||
| All content | all |
|
||||
| Journal | `data-type="journal"` only |
|
||||
| Stories | `data-type="story"` only |
|
||||
|
||||
Empty-feed edge case: if Stories is selected and no stories exist yet, show a brief inline message ("No stories yet for this trip.").
|
||||
|
||||
### Stats inline expansion
|
||||
|
||||
Clicking Stats expands a compact stats block between the filter bar and the first card. Clicking Stats again collapses it. Stats button gets an active/pressed visual state while expanded.
|
||||
|
||||
Stats block content (same data as the existing `/stats` page):
|
||||
- Days on the road
|
||||
- Entries posted
|
||||
- Countries visited
|
||||
- km traveled (approximate, straight-line haversine between GPS points)
|
||||
- Countries list
|
||||
|
||||
The computation logic is moved inline into `trip.html.twig` (copied from `stats.html.twig`). The separate `/stats` sub-page is left untouched — it still works as a standalone URL.
|
||||
|
||||
### Story card distinction
|
||||
|
||||
`.entry-card--story` gets a visible border:
|
||||
|
||||
```css
|
||||
border: 2px solid var(--color-accent);
|
||||
```
|
||||
|
||||
No other visual changes to story cards in this session. Full story card redesign (hero image treatment, sneak peek, elegant layout) is deferred to a separate session.
|
||||
|
||||
## What changes
|
||||
|
||||
| File | Change |
|
||||
|---|---|
|
||||
| `user/themes/intotheeast/templates/trip.html.twig` | Add `data-type` attributes; add stats computation + inline stats block HTML; replace nav links with filter bar HTML; add filter + stats JS |
|
||||
| `user/themes/intotheeast/css/style.css` | Add `.trip-nav` pill styles + active state; add `.trip-stats-block` styles; add story card border |
|
||||
|
||||
## What does NOT change
|
||||
|
||||
- `/dailies`, `/stats`, `/stories` sub-pages continue to exist as standalone URLs
|
||||
- `stats.html.twig` is untouched
|
||||
- `dailies.html.twig` is untouched
|
||||
- No blueprint or page content changes
|
||||
- Story detail page design is out of scope
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Photo galleries, lightbox, full in-feed entry expansion
|
||||
- Story detail page
|
||||
- Feed redesign (full pictures, per-entry photo carousel)
|
||||
@@ -0,0 +1,154 @@
|
||||
# Tuscany Demo Stories — Design Spec
|
||||
|
||||
**Date:** 2026-06-19
|
||||
**Goal:** Three demo stories for the Italy 2025 Tuscany trip that showcase distinct story-mode composition patterns. Content is illustrative; the purpose is to demonstrate what the format can do.
|
||||
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
Demo content lives in `user/docs/demo/trips/italy-2025/`. The `demo-load` Makefile target currently copies the Italy dailies and GPX files but does not copy a `04.stories/` folder. This spec adds three story files and wires them into `demo-load` / `demo-reset`.
|
||||
|
||||
Story pages must live at:
|
||||
```
|
||||
user/docs/demo/trips/italy-2025/04.stories/<n>.<slug>/story.md
|
||||
```
|
||||
|
||||
The `demo-load` target copies the entire `04.stories/` folder into:
|
||||
```
|
||||
user/pages/01.trips/italy-2025/04.stories/
|
||||
```
|
||||
|
||||
A `stories.md` listing page already exists at `user/docs/demo/trips/italy-2025/stories.md` and is already loaded by `demo-load`.
|
||||
|
||||
---
|
||||
|
||||
## Story 1 — "The Val d'Orcia at Dawn"
|
||||
|
||||
**Composition pattern:** Gallery-led. Multiple `[snap-gallery]` blocks; chapter breaks as pure visual section dividers; minimal prose; PullQuote without background image (text-only variant).
|
||||
|
||||
**Demonstrates:**
|
||||
- Two `[snap-gallery]` blocks in one story (landscape set + detail set)
|
||||
- `[chapter-break]` used as a pure scene-change divider (no thematic text, just atmosphere)
|
||||
- `[pull-quote]` without `image=` parameter → text-only frosted style
|
||||
|
||||
**Frontmatter:**
|
||||
```yaml
|
||||
title: The Val d'Orcia at Dawn
|
||||
date: 2025-09-05
|
||||
location_name: Val d'Orcia
|
||||
location_country: Italy
|
||||
lat: 43.078
|
||||
lng: 11.676
|
||||
hero_image: hero.jpg
|
||||
hero_alt: Cypress-lined dirt road at first light, Tuscany
|
||||
published: true
|
||||
```
|
||||
|
||||
**Structure:**
|
||||
1. Short intro prose (2–3 sentences)
|
||||
2. `[snap-gallery]` — 4 images: landscape wide shots (dawn light, rolling hills, cypress allée, dirt road)
|
||||
3. Brief prose bridge (2 sentences)
|
||||
4. `[chapter-break]` — title "The Hour Before Heat", no number
|
||||
5. More prose (2–3 sentences)
|
||||
6. `[snap-gallery]` — 4 images: close-up details (gravel texture, bike wheel, water bottle, shadow on road)
|
||||
7. `[pull-quote]` — **no image param** (text-only variant) — short reflective line
|
||||
8. One closing sentence
|
||||
|
||||
**Path:** `user/docs/demo/trips/italy-2025/04.stories/01.val-dorcia-dawn/story.md`
|
||||
|
||||
---
|
||||
|
||||
## Story 2 — "The Long Climb to Montalcino"
|
||||
|
||||
**Composition pattern:** Scrollytelling-led. Two `[scrolly-section]` blocks with different step counts, demonstrating the sticky-image format at different rhythms. PullQuote with background image between the two sections.
|
||||
|
||||
**Demonstrates:**
|
||||
- `[scrolly-section]` with 3 steps (tighter rhythm, effort/grind feeling)
|
||||
- `[scrolly-section]` with 5 steps (longer, more expansive — arrival and wandering)
|
||||
- `[pull-quote]` with `image=` parameter (frosted overlay on photo)
|
||||
- `[chapter-break]` with roman numeral, separating climb from descent/arrival
|
||||
|
||||
**Frontmatter:**
|
||||
```yaml
|
||||
title: The Long Climb to Montalcino
|
||||
date: 2025-09-05
|
||||
end_date: 2025-09-06
|
||||
location_name: Montalcino
|
||||
location_country: Italy
|
||||
lat: 43.058
|
||||
lng: 11.489
|
||||
hero_image: hero.jpg
|
||||
hero_alt: Hairpin road climbing through olive groves towards a hilltop town
|
||||
published: true
|
||||
```
|
||||
|
||||
**Structure:**
|
||||
1. Intro prose (3 sentences — sets the scene: hot afternoon, 14km climb)
|
||||
2. `[scrolly-section]` — image: the climb. **3 steps:** (1) first kilometer, legs fresh; (2) halfway, sun overhead, silence; (3) the last 500m, the town appears
|
||||
3. `[chapter-break]` — title "Montalcino", number "II"
|
||||
4. `[pull-quote image="vineyard.jpg"]` — line about the view from the top
|
||||
5. Prose (2–3 sentences — arrival, finding a bar, cold water)
|
||||
6. `[scrolly-section]` — image: the town/streets. **5 steps:** (1) the main piazza; (2) a wine shop; (3) a cat on a wall; (4) evening light on the fortress; (5) the descent begins
|
||||
7. Closing prose (1–2 sentences)
|
||||
|
||||
**Path:** `user/docs/demo/trips/italy-2025/04.stories/02.long-climb-montalcino/story.md`
|
||||
|
||||
---
|
||||
|
||||
## Story 3 — "One Evening in Siena"
|
||||
|
||||
**Composition pattern:** Mood/fragment piece. Short and impressionistic. Opens with a PullQuote (no image) — the quote anchors the story before any prose. Closes with a PullQuote with image. Single short ScrollySection. SnapGallery in the middle.
|
||||
|
||||
**Demonstrates:**
|
||||
- PullQuote **as opening element** (before any body prose) — unusual structure
|
||||
- `[scrolly-section]` with just 2 steps (the minimum — shows it works for very short sections)
|
||||
- `[snap-gallery]` as a mid-story element (not a closing flourish)
|
||||
- PullQuote with image **as closing element**
|
||||
- Overall: the format works for short, atmospheric pieces, not just long narratives
|
||||
|
||||
**Frontmatter:**
|
||||
```yaml
|
||||
title: One Evening in Siena
|
||||
date: 2025-09-05
|
||||
location_name: Siena
|
||||
location_country: Italy
|
||||
lat: 43.318
|
||||
lng: 11.330
|
||||
hero_image: hero.jpg
|
||||
hero_alt: The Piazza del Campo at dusk, terracotta rooftops fading to blue
|
||||
published: true
|
||||
```
|
||||
|
||||
**Structure:**
|
||||
1. `[pull-quote]` — **no image, opens the story** — a single observational sentence about Siena at dusk
|
||||
2. Intro prose (2 sentences — arrival on bike, the square)
|
||||
3. `[scrolly-section image="campo.jpg"]` — **2 steps:** (1) the square fills with people as the sun goes; (2) a busker, a couple arguing, pigeons
|
||||
4. `[snap-gallery]` — 3 images: campo at dusk, a doorway, someone eating gelato
|
||||
5. Prose (2 sentences — finding dinner, the relief of sitting down)
|
||||
6. `[pull-quote image="sunset.jpg"]` — **with image, closes the story** — a line about what cycling does to ordinary moments
|
||||
|
||||
**Path:** `user/docs/demo/trips/italy-2025/04.stories/03.one-evening-siena/story.md`
|
||||
|
||||
---
|
||||
|
||||
## Makefile Changes
|
||||
|
||||
`demo-load` — add after the existing Italy stories.md copy line:
|
||||
```makefile
|
||||
cp -r user/docs/demo/trips/italy-2025/04.stories user/pages/01.trips/italy-2025/ 2>/dev/null || true
|
||||
```
|
||||
|
||||
`demo-reset` — the existing `rm -rf user/pages/01.trips/italy-2025` already removes everything including stories, so no additional line needed.
|
||||
|
||||
---
|
||||
|
||||
## Deliverables
|
||||
|
||||
1. `user/docs/demo/trips/italy-2025/04.stories/01.val-dorcia-dawn/story.md`
|
||||
2. `user/docs/demo/trips/italy-2025/04.stories/02.long-climb-montalcino/story.md`
|
||||
3. `user/docs/demo/trips/italy-2025/04.stories/03.one-evening-siena/story.md`
|
||||
4. `Makefile` — one line added to `demo-load`
|
||||
5. Two commits: one for story files (user repo), one for Makefile (main repo)
|
||||
|
||||
No image files are committed. Shortcode image params reference filenames that won't resolve without real photos — this is consistent with the existing Japan demo story.
|
||||
@@ -0,0 +1,170 @@
|
||||
# Accessibility Audit Design — intotheeast
|
||||
|
||||
**Date:** 2026-06-20
|
||||
**Standard:** WCAG 2.1 Level AA
|
||||
**Scope:** All Twig templates in `user/themes/intotheeast/templates/`, CSS tokens, and inline JS
|
||||
|
||||
---
|
||||
|
||||
## 1. Audit Results
|
||||
|
||||
### Failures (must fix)
|
||||
|
||||
| ID | Criterion | Severity | Where | Issue |
|
||||
|----|-----------|----------|-------|-------|
|
||||
| F1 | 2.4.1 Bypass Blocks | High | Every page | No skip-to-main link — keyboard users must tab through site header on every page load |
|
||||
| F2 | 1.4.3 Contrast Minimum | High | `tokens.css` | `--color-ink-muted` (#7A7268) is 3.74:1 on `--color-paper` and 3.44:1 on `--color-canvas` — fails 4.5:1 AA for small text. Used for timestamps, location labels, weather spans, and stat labels |
|
||||
| F3 | 1.4.3 Contrast Minimum | High | `tokens.css` | `--color-accent` (#2A8C73) is 4.30:1 on paper and 3.95:1 on canvas — fails 4.5:1 AA. Used as link text color on journal permalinks, back-pill anchors, and feed-map link |
|
||||
| F4 | 4.1.2 Name/Role/Value | High | `trip.html.twig` | Filter buttons (`All content` / `Journal` / `Stories`) have no `aria-pressed` — the active filter is communicated only via CSS class, invisible to screen readers |
|
||||
| F5 | 4.1.2 Name/Role/Value | High | `trip.html.twig` | Stats and Cycling toggle buttons have no `aria-expanded` or `aria-controls` — collapsed/expanded state is invisible to screen readers |
|
||||
| F6 | 2.1.1 Keyboard | Medium | All three feed templates | Photo strip is scroll-snap only; no keyboard navigation. Slides cannot be advanced by keyboard users |
|
||||
| F7 | 4.1.2 Name/Role/Value | Medium | `gpx-manager.html.twig` | Each table row has a bare "Delete" button — a screen reader hears "Delete, Delete, Delete" with no way to distinguish which file is targeted |
|
||||
| F8 | 1.1.1 Non-text Content | Medium | `entry.html.twig` | When the lightbox is open, the enlarged `<img>` has `alt=""` — the displayed photo has no accessible description |
|
||||
|
||||
### Passes (no changes needed)
|
||||
|
||||
- `<html lang="en">`, `<header>`, `<main>`, `<footer>` landmark structure ✓
|
||||
- `<nav aria-label="Main navigation">` ✓
|
||||
- `aria-current="page"` on active nav link ✓
|
||||
- Global `:focus-visible` rule with `--color-accent` outline ✓
|
||||
- `prefers-reduced-motion` block covering all animations ✓
|
||||
- Lightbox: `role="dialog"`, `aria-modal="true"`, `aria-label="Photo viewer"`, labelled close/prev/next buttons ✓
|
||||
- `aria-hidden="true"` on photo-strip dots, story hero overlay, story scroll cue ✓
|
||||
- `<time datetime="…">` on all entry dates ✓
|
||||
- `--color-ink` (#EDE8DF): 14.53:1 on paper ✓
|
||||
- `--color-ink-2` (#B8B0A4): 8.26:1 on paper ✓
|
||||
- Story nav title `aria-hidden="true"` (decorative scroll-driven element) ✓
|
||||
- Back-to-top button `aria-label="Back to top"` ✓
|
||||
- Hero image `alt` with `hero_alt ?? page.title` fallback ✓
|
||||
|
||||
---
|
||||
|
||||
## 2. Fixes
|
||||
|
||||
### Task 1 — Skip link + main landmark id
|
||||
|
||||
**Files:** `user/themes/intotheeast/templates/partials/base.html.twig`, `user/themes/intotheeast/css/style.css`
|
||||
|
||||
Add a visually-hidden skip link as the first focusable element in the page, before the site header. On `:focus-visible` it snaps to the top-left corner of the viewport. Add `id="main-content"` to the existing `<main class="site-main">` element so the link has a valid target.
|
||||
|
||||
Skip-link CSS: off-screen at rest (e.g. `position: absolute; left: -10000px`), snaps to `top: 0; left: 0` on `:focus-visible`. Styled with accent color to match the site's existing focus ring aesthetic.
|
||||
|
||||
### Task 2 — Color token contrast fixes
|
||||
|
||||
**Files:** `user/themes/intotheeast/css/tokens.css`
|
||||
|
||||
Two token values fail WCAG 1.4.3. Replace both:
|
||||
|
||||
| Token | Current | Replacement | Ratio on paper | Ratio on canvas |
|
||||
|-------|---------|-------------|----------------|-----------------|
|
||||
| `--color-ink-muted` | #7A7268 | #90887E | 5.07:1 ✓ | 4.66:1 ✓ |
|
||||
| `--color-accent` | #2A8C73 | #2E9880 | 5.00:1 ✓ | 4.59:1 ✓ |
|
||||
| `--color-accent-hover` | #236655 | #287A68 | 3.58:1 ✓ (non-text) | — |
|
||||
|
||||
`--color-accent-hover` is used only for hover/active states, so the 3:1 non-text contrast criterion (1.4.11) applies rather than 4.5:1. #287A68 passes 3:1.
|
||||
|
||||
These are purely token changes — no template or layout changes required.
|
||||
|
||||
### Task 3 — ARIA states for filter and toggle buttons
|
||||
|
||||
**Files:** `user/themes/intotheeast/templates/trip.html.twig`
|
||||
|
||||
**Filter buttons (F4):**
|
||||
|
||||
In the template, add `aria-pressed="true"` to the initially-active `All content` button and `aria-pressed="false"` to the other two. In the existing filter JS block (the `trip-filter-btn` click handler), toggle `aria-pressed` alongside `is-active`:
|
||||
|
||||
```js
|
||||
document.querySelectorAll('.trip-filter-btn').forEach(function(btn) {
|
||||
btn.setAttribute('aria-pressed', btn === activeBtn ? 'true' : 'false');
|
||||
});
|
||||
```
|
||||
|
||||
**Stats/Cycling toggles (F5):**
|
||||
|
||||
Add `aria-expanded="false"` and `aria-controls="trip-stats-block"` to the Stats button. Add `aria-expanded="false"` and `aria-controls="trip-cycling-block"` to the Cycling button. Add the matching `id` attributes to the panels they control (`id="trip-stats-block"` already exists; add `id="trip-cycling-block"` to the cycling panel). In the toggle JS, set `aria-expanded="true"` when the panel is shown, `"false"` when hidden.
|
||||
|
||||
No new elements needed — only attribute additions to existing markup and existing JS handlers.
|
||||
|
||||
### Task 4 — Photo strip keyboard navigation
|
||||
|
||||
**Files:** `user/themes/intotheeast/templates/partials/base.html.twig` (the dot-sync JS IIFE)
|
||||
|
||||
For each photo strip with more than one slide, inject a `<button class="strip-prev" aria-label="Previous photo">` and `<button class="strip-next" aria-label="Next photo">` as siblings to the strip after the dots. The buttons are hidden when the strip has only one slide (`data-slides="1"`).
|
||||
|
||||
Clicking prev/next scrolls the strip by one slide width via `scrollBy`. The existing dot-sync scroll listener already updates dot state, so dots stay in sync automatically.
|
||||
|
||||
The strip container gains `role="region"` and `aria-label="Photo strip"` to group it as a named region for screen reader navigation.
|
||||
|
||||
CSS for the buttons: minimal, positioned relative to the strip, styled as teal chevrons matching the site palette. Hidden via `display:none` when `data-slides="1"`.
|
||||
|
||||
The strip container itself does NOT get `tabindex="0"` — the injected buttons are the keyboard entry points, which is cleaner than making a scroll container focusable.
|
||||
|
||||
### Task 5 — GPX delete button names + lightbox alt text
|
||||
|
||||
**Files:** `user/themes/intotheeast/templates/gpx-manager.html.twig`, `user/themes/intotheeast/templates/entry.html.twig`
|
||||
|
||||
**GPX delete buttons (F7):**
|
||||
|
||||
The delete buttons are built in the JS file-list renderer. Change the button label from `Delete` to `Delete ${f.filename}`:
|
||||
|
||||
```js
|
||||
td.innerHTML = `<button class="gpx-delete" data-filename="${f.filename}">Delete ${f.filename}</button>`;
|
||||
```
|
||||
|
||||
The filename already appears in the adjacent `<td>`, so this adds redundancy for screen readers while not disturbing the visual layout. Alternatively, use `aria-label="Delete ${f.filename}"` and keep the visible text as `Delete` — either approach satisfies 4.1.2.
|
||||
|
||||
Use `aria-label`: keeps visible text short (`Delete`), accessible name specific (`Delete 2026-03-25-tokyo.gpx`).
|
||||
|
||||
**Lightbox alt text (F8):**
|
||||
|
||||
The lightbox open function already copies `data-alt` from the thumbnail. The fix is ensuring `data-alt` is populated with the thumbnail's `alt` attribute (which is `entry.title` — the entry title) and that the full-size `<img>` inside the lightbox receives it on open.
|
||||
|
||||
In the existing lightbox open JS: when setting the `src` of the lightbox `<img>`, also set its `alt` from the triggering thumbnail's `alt` attribute.
|
||||
|
||||
### Task 6 — axe-core Playwright regression tests
|
||||
|
||||
**Files:** `tests/ui/accessibility.spec.js` (new), `package.json`
|
||||
|
||||
Add `@axe-core/playwright` as a devDependency. Create `tests/ui/accessibility.spec.js` that runs an axe accessibility scan on the following pages:
|
||||
|
||||
- `/` (home)
|
||||
- `/trips/japan-korea-2026` (trip page, with filter bar and stats)
|
||||
- `/trips/japan-korea-2026/dailies` (journal feed with map)
|
||||
- One entry page (use a known demo slug)
|
||||
- `/trips` (trips archive)
|
||||
|
||||
Configuration: fail on `critical` and `serious` violations only. Log `moderate` and `minor` findings as warnings without failing. This matches realistic ongoing CI practice — the fixes in Tasks 1–5 should bring the site to zero `critical`/`serious` violations.
|
||||
|
||||
Each test uses the existing `chromium` project from `playwright.config.js` with the existing auth setup.
|
||||
|
||||
---
|
||||
|
||||
## 3. What is NOT in scope
|
||||
|
||||
- WCAG AAA criteria (e.g. 1.4.6 Enhanced Contrast at 7:1)
|
||||
- Map marker keyboard navigation — MapLibre GL has built-in keyboard support for map pan/zoom; marker focus is a complex interaction pattern beyond the current scope. Deferred.
|
||||
- Story page shortcode heading hierarchy — enforcing heading structure in author-written content is a content authoring concern, not a template concern
|
||||
- `post-form.html.twig` — the form is admin-only and used by Mischa alone; functional accessibility for this page is inherently self-tested
|
||||
|
||||
---
|
||||
|
||||
## 4. Testing approach
|
||||
|
||||
- **Task 1–5:** Manual verification by loading the page, tabbing through with keyboard, and checking AT output with a screen reader or browser accessibility tree inspector
|
||||
- **Task 6:** Automated axe-core scan catches regressions after future template changes; run as part of `npx playwright test`
|
||||
- Playwright tests must load demo data (`make demo-load`) before running, consistent with existing test setup
|
||||
|
||||
---
|
||||
|
||||
## 5. File map
|
||||
|
||||
| File | Changed by |
|
||||
|------|-----------|
|
||||
| `user/themes/intotheeast/templates/partials/base.html.twig` | Tasks 1, 4 |
|
||||
| `user/themes/intotheeast/css/style.css` | Task 1 |
|
||||
| `user/themes/intotheeast/css/tokens.css` | Task 2 |
|
||||
| `user/themes/intotheeast/templates/trip.html.twig` | Task 3 |
|
||||
| `user/themes/intotheeast/templates/gpx-manager.html.twig` | Task 5 |
|
||||
| `user/themes/intotheeast/templates/entry.html.twig` | Task 5 |
|
||||
| `tests/ui/accessibility.spec.js` | Task 6 (new) |
|
||||
| `package.json` | Task 6 |
|
||||
@@ -0,0 +1,300 @@
|
||||
# Demo Data Redesign — italy-2026-demo
|
||||
|
||||
**Date:** 2026-06-20
|
||||
**Status:** Approved
|
||||
|
||||
## Goal
|
||||
|
||||
Replace the existing patchwork of demo content across three trips with a single, high-quality demo trip (`italy-2026-demo`) that:
|
||||
- Follows the real Tuscany cycling route (Campiglia Marittima loop, 8 days)
|
||||
- Has 12 journal entries with actual photos for realistic gallery/lightbox QA
|
||||
- Has 4 stories that collectively exercise every story shortcode type
|
||||
- Is cleanly managed by a single `make demo-load` / `make demo-reset` pair
|
||||
|
||||
---
|
||||
|
||||
## 1. Cleanup
|
||||
|
||||
### Remove
|
||||
|
||||
| Path | Action |
|
||||
|---|---|
|
||||
| `user/docs/demo/trips/japan-korea-2026/` | Delete entirely |
|
||||
| `user/docs/demo/trips/italy-2025/dailies/` | Delete all entries |
|
||||
| `user/docs/demo/trips/italy-2025/04.stories/` | Delete all stories |
|
||||
| `user/docs/demo/trips/italy-2026-demo/dailies/` | Replace with 12 new entries |
|
||||
| `user/docs/demo/trips/italy-2026-demo/04.stories/` | Replace with 4 new stories |
|
||||
|
||||
Italy 2025 keeps its GPX files and page-structure files (trip.md, map.md, stats.md) — only demo-generated entries and stories are removed.
|
||||
|
||||
### Update CLAUDE.md
|
||||
|
||||
Remove the Japan/Korea reference from the `demo-load` description. New description:
|
||||
> `make demo-load` — load demo content into `italy-2026-demo` trip (journal entries + stories + GPX)
|
||||
|
||||
---
|
||||
|
||||
## 2. GPX Files
|
||||
|
||||
Rename the 4 newly added files to match the existing naming convention. Keep existing day-5/6/8 names unchanged.
|
||||
|
||||
| New filename | Original |
|
||||
|---|---|
|
||||
| `day-1-campiglia-to-sugherella.gpx` | `2025-10-11_2627663255_TGE Tuscany...Day 1...gpx` |
|
||||
| `day-2-sugherella-to-orbetello.gpx` | `2025-10-12_2630489431_TGE Tuscany...Day 2.gpx` |
|
||||
| `day-3-orbetello-to-sorano.gpx` | `2025-10-13_2632495944_TGE Tuscany...Day 3.gpx` |
|
||||
| `day-4-sorano-to-val-dorcia.gpx` | `2025-10-14_2634086364_TGE Tuscany...Day 4.gpx` |
|
||||
|
||||
All 7 GPX files live at `user/docs/demo/trips/italy-2026-demo/`.
|
||||
|
||||
---
|
||||
|
||||
## 3. Journal Entries (12)
|
||||
|
||||
Each entry is a directory at `user/docs/demo/trips/italy-2026-demo/dailies/<slug>.entry/` containing:
|
||||
- `entry.md` — frontmatter + one prose paragraph
|
||||
- `01.jpg` … `N.jpg` — placeholder images (numbered for sort order)
|
||||
|
||||
Images are downloaded from `https://picsum.photos/seed/<seed>/1200/800` during `make demo-load`. Seeds are fixed so the same images load every time.
|
||||
|
||||
### Entry list
|
||||
|
||||
| # | Slug | Date/Time | Location | Weather | Photos | Seed prefix |
|
||||
|---|---|---|---|---|---|---|
|
||||
| 1 | `2026-09-01-0700-setting-off-from-campiglia` | 2026-09-01 07:00 | Campiglia Marittima, Italy | Sunny 27°C | 2 | `demo-d1` |
|
||||
| 2 | `2026-09-02-1130-maremma-in-full-sun` | 2026-09-02 11:30 | Maremma, Italy | Sunny 29°C | 3 | `demo-d2a` |
|
||||
| 3 | `2026-09-02-1900-the-lagoon-at-dusk` | 2026-09-02 19:00 | Orbetello, Italy | Partly cloudy 24°C | 3 | `demo-d2b` |
|
||||
| 4 | `2026-09-03-0800-orbetello-morning` | 2026-09-03 08:00 | Orbetello, Italy | Sunny 22°C | 2 | `demo-d3a` |
|
||||
| 5 | `2026-09-03-1700-tufa-and-towers` | 2026-09-03 17:00 | Sorano, Italy | Sunny 26°C | 2 | `demo-d3b` |
|
||||
| 6 | `2026-09-04-1500-the-long-climb-north` | 2026-09-04 15:00 | Val d'Orcia, Italy | Partly cloudy 23°C | 4 | `demo-d4` |
|
||||
| 7 | `2026-09-05-0830-before-the-heat-arrives` | 2026-09-05 08:30 | Pienza, Italy | Sunny 21°C | 2 | `demo-d5a` |
|
||||
| 8 | `2026-09-05-1800-into-siena` | 2026-09-05 18:00 | Siena, Italy | Sunny 25°C | 3 | `demo-d5b` |
|
||||
| 9 | `2026-09-06-2000-florence-by-nightfall` | 2026-09-06 20:00 | Florence, Italy | Cloudy 21°C | 3 | `demo-d6` |
|
||||
| 10 | `2026-09-07-1400-one-rest-day` | 2026-09-07 14:00 | Florence, Italy | Partly cloudy 22°C | 2 | `demo-d7` |
|
||||
| 11 | `2026-09-08-0730-dawn-on-the-cecina-coast` | 2026-09-08 07:30 | Cecina, Italy | Sunny 20°C | 1 | `demo-d8a` |
|
||||
| 12 | `2026-09-08-1630-home` | 2026-09-08 16:30 | Campiglia Marittima, Italy | Sunny 26°C | 2 | `demo-d8b` |
|
||||
|
||||
### Entry coordinates
|
||||
|
||||
| # | lat | lng | Notes |
|
||||
|---|---|---|---|
|
||||
| 1 | 43.024 | 10.603 | GPX Day 1 start |
|
||||
| 2 | 42.612 | 11.171 | GPX Day 2 midpoint |
|
||||
| 3 | 42.442 | 11.218 | GPX Day 2 end (Orbetello) |
|
||||
| 4 | 42.442 | 11.217 | GPX Day 3 start |
|
||||
| 5 | 42.683 | 11.715 | GPX Day 3 end (Sorano) |
|
||||
| 6 | 43.077 | 11.678 | GPX Day 4 end (Pienza/Val d'Orcia) |
|
||||
| 7 | 43.078 | 11.676 | GPX Day 5 start |
|
||||
| 8 | 43.318 | 11.335 | GPX Day 5 end (Siena) |
|
||||
| 9 | 43.767 | 11.253 | GPX Day 6 end (Florence) |
|
||||
| 10 | 43.769 | 11.255 | Florence (rest day, slight offset) |
|
||||
| 11 | 43.553 | 10.313 | GPX Day 8 start (Cecina coast) |
|
||||
| 12 | 43.017 | 10.587 | GPX Day 8 end (Campiglia) |
|
||||
|
||||
### Entry frontmatter pattern
|
||||
|
||||
```yaml
|
||||
---
|
||||
title: '<Title>'
|
||||
date: '2026-09-NN HH:MM'
|
||||
template: entry
|
||||
published: true
|
||||
hero_image: ''
|
||||
lat: '<lat>'
|
||||
lng: '<lng>'
|
||||
location_city: '<City>'
|
||||
location_country: 'Italy'
|
||||
weather_temp_c: <N>
|
||||
weather_desc: '<Desc>'
|
||||
---
|
||||
```
|
||||
|
||||
`hero_image` is left empty — the template auto-picks `media.images|first` as hero, which will be `01.jpg`.
|
||||
|
||||
---
|
||||
|
||||
## 4. Stories (4)
|
||||
|
||||
Each story is a directory at `user/docs/demo/trips/italy-2026-demo/04.stories/<slug>/` containing `story.md` + image files.
|
||||
|
||||
Stories are ordered chronologically and geographically along the route. Each story's **primary shortcode** is different, ensuring all 4 types get QA coverage.
|
||||
|
||||
### Story list
|
||||
|
||||
| # | Slug | Day | Location | Primary shortcode | Also uses |
|
||||
|---|---|---|---|---|---|
|
||||
| 1 | `01.sorano-rock-and-time` | 3 | Sorano | `scrolly-section` | `chapter-break`, `snap-gallery` |
|
||||
| 2 | `02.val-dorcia-at-dawn` | 5 | Pienza / Val d'Orcia | `snap-gallery` | `chapter-break`, `pull-quote` |
|
||||
| 3 | `03.one-evening-siena` | 5 | Siena | `pull-quote` (with image) | `chapter-break`, `scrolly-section` |
|
||||
| 4 | `04.florence-without-a-map` | 7 | Florence | `chapter-break` (structural) | `pull-quote`, `scrolly-section` |
|
||||
|
||||
### Story: 01.sorano-rock-and-time
|
||||
|
||||
**Structure:**
|
||||
1. Intro prose (1 paragraph)
|
||||
2. `[scrolly-section image="hero.jpg"]` — 3 scroll steps separated by `---`: approach to Sorano, the tufa cliffs close up, entering the gate
|
||||
3. Prose bridge (1 paragraph)
|
||||
4. `[chapter-break image="photo-1.jpg" title="After Dark" number="II"]`
|
||||
5. `[snap-gallery images="photo-1.jpg,photo-2.jpg"]` — alley + view from the walls
|
||||
6. Closing prose (1 paragraph)
|
||||
|
||||
**Images:** `hero.jpg` (town on tufa cliff), `photo-1.jpg` (narrow medieval alley), `photo-2.jpg` (view south over valley)
|
||||
|
||||
**Frontmatter:**
|
||||
```yaml
|
||||
title: Sorano: Rock and Time
|
||||
date: '2026-09-03'
|
||||
location_name: Sorano
|
||||
location_country: Italy
|
||||
lat: 42.683
|
||||
lng: 11.715
|
||||
hero_image: hero.jpg
|
||||
hero_alt: Medieval town of Sorano perched on tufa cliffs
|
||||
published: true
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Story: 02.val-dorcia-at-dawn
|
||||
|
||||
**Structure:**
|
||||
1. Intro prose (1 paragraph — leaving camp before sunrise)
|
||||
2. `[snap-gallery images="hero.jpg,photo-1.jpg,photo-2.jpg"]` — 3 landscape shots: valley at first light, cypress road, distant farmhouse
|
||||
3. Prose bridge (1 paragraph — route through the valley floor)
|
||||
4. `[chapter-break image="photo-1.jpg" title="The Hour Before Heat"]`
|
||||
5. `[pull-quote]` (text-only, no image) — a short reflection on cycling rhythms
|
||||
6. Closing prose (1 paragraph — reaching Pienza by noon)
|
||||
|
||||
**Images:** `hero.jpg` (wide valley, golden hour), `photo-1.jpg` (cypress-lined road), `photo-2.jpg` (farmhouse on hillside)
|
||||
|
||||
**Frontmatter:**
|
||||
```yaml
|
||||
title: Val d'Orcia at Dawn
|
||||
date: '2026-09-05'
|
||||
location_name: Val d'Orcia
|
||||
location_country: Italy
|
||||
lat: 43.078
|
||||
lng: 11.676
|
||||
hero_image: hero.jpg
|
||||
hero_alt: Wide Tuscan valley at dawn, long cypress shadows
|
||||
published: true
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Story: 03.one-evening-siena
|
||||
|
||||
**Structure:**
|
||||
1. `[pull-quote image="hero.jpg" alt="..."]` — image-backed opener quote about arriving by bike
|
||||
2. Intro prose (1–2 paragraphs — the Campo appearing at the end of a street)
|
||||
3. `[chapter-break image="photo-1.jpg" title="The Campo" number="I"]`
|
||||
4. `[scrolly-section image="hero.jpg"]` — 3 scroll steps: the square filling up at dusk, a busker, sitting down after 8 hours riding
|
||||
5. Closing prose (1 paragraph — finding dinner, the specific relief of sitting still)
|
||||
|
||||
**Images:** `hero.jpg` (Campo at golden hour from upper rim), `photo-1.jpg` (stone doorway / Siena street detail)
|
||||
|
||||
**Frontmatter:**
|
||||
```yaml
|
||||
title: One Evening in Siena
|
||||
date: '2026-09-05'
|
||||
location_name: Siena
|
||||
location_country: Italy
|
||||
lat: 43.318
|
||||
lng: 11.330
|
||||
hero_image: hero.jpg
|
||||
hero_alt: Piazza del Campo at dusk, terracotta rooftops fading to blue
|
||||
published: true
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Story: 04.florence-without-a-map
|
||||
|
||||
**Structure:**
|
||||
1. Intro prose (1 paragraph — rest day, no route, no GPS)
|
||||
2. `[chapter-break image="hero.jpg" title="Day Seven" number="VII"]`
|
||||
3. `[snap-gallery images="hero.jpg,photo-1.jpg"]` — Arno view + street scene
|
||||
4. `[pull-quote]` (text-only) — reflection: cycling makes you earn the places; today we got Florence for free
|
||||
5. `[scrolly-section image="photo-1.jpg"]` — 3 steps: Uffizi queue they didn't join, a leather market, crossing a bridge at midday light
|
||||
6. Closing prose (1 paragraph — tired feet, early bed, tomorrow the coast road home)
|
||||
|
||||
**Images:** `hero.jpg` (Arno river with Ponte Vecchio), `photo-1.jpg` (narrow Florence street with washing lines)
|
||||
|
||||
**Frontmatter:**
|
||||
```yaml
|
||||
title: Florence Without a Map
|
||||
date: '2026-09-07'
|
||||
location_name: Florence
|
||||
location_country: Italy
|
||||
lat: 43.769
|
||||
lng: 11.255
|
||||
hero_image: hero.jpg
|
||||
hero_alt: Arno river at midday with Ponte Vecchio
|
||||
published: true
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Makefile Changes
|
||||
|
||||
### demo-load (full replacement)
|
||||
|
||||
New behaviour:
|
||||
1. Create trip folder structure under `user/pages/01.trips/italy-2026-demo/`
|
||||
2. Copy page-level markdown files (trip.md, map.md, stats.md, stories.md)
|
||||
3. Copy all 4 stories (with their image files) to `04.stories/`
|
||||
4. Copy all 12 journal entries to `01.dailies/`
|
||||
5. Copy 7 GPX files to trip root
|
||||
6. Download placeholder images via `curl` into each entry and story folder (skip if file exists)
|
||||
7. `php bin/grav clearcache`
|
||||
|
||||
Image download pattern for entries (per entry `SLUG`, images `01.jpg`…`NN.jpg`):
|
||||
```bash
|
||||
[ -f ".../01.dailies/SLUG/01.jpg" ] || curl -sL "https://picsum.photos/seed/demo-dN-1/1200/800" -o ".../01.jpg"
|
||||
```
|
||||
|
||||
Seed naming convention: `{seed-prefix}-{image-number}` e.g. entry 1 (`demo-d1`) gets seeds `demo-d1-1` and `demo-d1-2`. Story images use prefix `demo-s{N}` e.g. `demo-s1-hero`, `demo-s1-1`, `demo-s1-2`.
|
||||
|
||||
Image sizes: entries `1200x800`, story images `1600x1000`.
|
||||
|
||||
### demo-reset (updated)
|
||||
|
||||
Remove `user/pages/01.trips/italy-2026-demo/` entirely and clear cache. Behaviour unchanged from current, just scoped correctly.
|
||||
|
||||
---
|
||||
|
||||
## 6. trip.md Updates
|
||||
|
||||
`user/docs/demo/trips/italy-2026-demo/trip.md`:
|
||||
|
||||
```yaml
|
||||
---
|
||||
title: 'Tuscany 2026'
|
||||
template: trip
|
||||
date: '2026-09-01'
|
||||
date_start: '2026-09-01'
|
||||
date_end: '2026-09-08'
|
||||
cover_image: ''
|
||||
---
|
||||
```
|
||||
|
||||
Title changed from "Italy 2026 (Demo)" to "Tuscany 2026" — cleaner for a realistic demo.
|
||||
|
||||
---
|
||||
|
||||
## 7. What Is Not Changing
|
||||
|
||||
- `user/pages/01.trips/italy-2025/` — real trip page stays; only the demo entries in `docs/demo/trips/italy-2025/dailies/` and `docs/demo/trips/italy-2025/04.stories/` are removed
|
||||
- `user/pages/01.trips/japan-korea-2026/` — active trip, untouched
|
||||
- GPX files already loaded on the italy-2025 page — untouched
|
||||
- `user/config/site.yaml` `active_trip` — untouched
|
||||
|
||||
---
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- Write stories first (they define the image filenames), then entries
|
||||
- Image seeds are fixed strings so `make demo-load` is idempotent
|
||||
- The `|| true` pattern on `cp` commands is already established in the Makefile — follow it
|
||||
- `stories.md` (the listing page) frontmatter is unchanged
|
||||
- No `hero_image` in entry frontmatter — let the template auto-select `01.jpg`
|
||||
@@ -0,0 +1,109 @@
|
||||
# Design Spec: Smart GPX-Marker Connector Logic
|
||||
|
||||
**Date:** 2026-06-20
|
||||
**Status:** Approved
|
||||
|
||||
---
|
||||
|
||||
## Problem
|
||||
|
||||
The map currently draws a straight connector line between every adjacent pair of journal/story entry markers in chronological order. When GPX track files are also present, this creates two overlapping representations of the same movement — the accurate GPX track line and a redundant straight-line connector. For segments with no GPX coverage (e.g. a train journey), the straight connector is useful and should remain.
|
||||
|
||||
---
|
||||
|
||||
## Goal
|
||||
|
||||
Suppress connector lines between adjacent markers only when a single GPX file demonstrably covers both endpoints. Keep connectors for gaps that have no GPX coverage. Provide a per-entry override (`force_connect`) for cases where the algorithm suppresses a connector the author wants to show.
|
||||
|
||||
---
|
||||
|
||||
## Behaviour Modes
|
||||
|
||||
### No GPX files present
|
||||
Existing behaviour unchanged. All adjacent markers are connected by a line in chronological order.
|
||||
|
||||
### GPX files present
|
||||
Auto-connectors off by default. For each adjacent pair of markers (M1 → M2):
|
||||
|
||||
1. If `force_connect: true` on M2 → draw connector (override wins)
|
||||
2. Otherwise run the spatial algorithm (see below)
|
||||
3. If the algorithm finds coverage → suppress connector
|
||||
4. If the algorithm finds no coverage → draw connector
|
||||
|
||||
---
|
||||
|
||||
## Spatial Algorithm
|
||||
|
||||
**Proximity threshold:** 10 km
|
||||
|
||||
For each adjacent pair (M1, M2):
|
||||
|
||||
1. For each loaded GPX file F:
|
||||
a. Pre-filter: if M1 or M2 lies outside F's bounding box expanded by 10 km → skip F cheaply
|
||||
b. Sample every 10th trackpoint in F; compute haversine distance to M1 and to M2
|
||||
c. If both M1 and M2 have at least one sampled point within 10 km → suppress connector for this pair; stop checking further files
|
||||
2. If no file F covered both M1 and M2 → draw connector
|
||||
|
||||
**Rationale for 10 km:** entries are often posted from a hotel, village, or café near (but not on) a trail. 10 km accommodates varied terrain — coastal routes, hilly detours — without false-positives across genuinely separate segments.
|
||||
|
||||
**Rationale for same-file requirement:** two markers each near *different* GPX files (e.g. an inland hike and a coastal walk) must not suppress the connector between them — that gap (e.g. a train journey) is exactly what should be shown.
|
||||
|
||||
---
|
||||
|
||||
## Fallback Behaviour
|
||||
|
||||
If any GPX file fails to load, treat it as absent for the algorithm. Connectors default to drawing rather than hiding — missing data never creates invisible gaps on the map.
|
||||
|
||||
---
|
||||
|
||||
## Data Model Changes
|
||||
|
||||
Two new fields added to both the **journal entry** and **story entry** Grav blueprints:
|
||||
|
||||
### `force_connect`
|
||||
- Type: boolean
|
||||
- Default: false / null (unset)
|
||||
- Meaning: "always draw a connector from the previous marker to this entry"
|
||||
- Only has visible effect when GPX files are present (when no GPX, auto-connectors are already on)
|
||||
- Editable via Admin2 on any entry
|
||||
|
||||
### `transport_mode`
|
||||
- Type: enum
|
||||
- Values: `walking`, `bicycle`, `bus`, `train`, `car`
|
||||
- Default: null (unset)
|
||||
- Meaning: how the author arrived at this location (attached to the arriving entry)
|
||||
- **Not visualised yet** — data capture only, for future use (distance-by-mode stats, map icons, filter)
|
||||
- Editable via Admin2 on any entry
|
||||
|
||||
Both fields are exposed in frontmatter. Adding them to the mobile post form is deferred (backlog: blueprint-to-form sync pass).
|
||||
|
||||
---
|
||||
|
||||
## Client-Side Implementation
|
||||
|
||||
### Entry JSON
|
||||
The Twig template that serialises entries into a JS variable (`TRACKER_ENTRIES` or equivalent) gains two new fields per entry: `force_connect` (bool) and `transport_mode` (string or null).
|
||||
|
||||
### Timing
|
||||
Connector drawing is deferred until all GPX files have settled (Promise.all on load events). GPX tracks appear immediately as each file loads. Connectors render once all files are resolved or rejected.
|
||||
|
||||
### Performance
|
||||
- Bounding box pre-filter eliminates most files for any given pair without distance math
|
||||
- Sampling every 10th trackpoint keeps the haversine checks cheap even for full-day GPX files (thousands of points → hundreds of checks per file per pair)
|
||||
|
||||
---
|
||||
|
||||
## Deferred / Out of Scope
|
||||
|
||||
- Visualising `transport_mode` on the map (icons, line styles by mode)
|
||||
- Distance-by-mode statistics
|
||||
- Adding `force_connect` / `transport_mode` to the mobile post form
|
||||
- Making the 10 km threshold configurable in `site.yaml`
|
||||
|
||||
---
|
||||
|
||||
## Affected Files (indicative)
|
||||
|
||||
- `user/themes/intotheeast/templates/map.html.twig` — entry JSON serialisation
|
||||
- `user/themes/intotheeast/js/map.js` (or equivalent) — connector drawing logic + algorithm
|
||||
- Blueprint file(s) for journal and story entries — add two new fields
|
||||
@@ -0,0 +1,306 @@
|
||||
# Inline Journal Feed Design Spec
|
||||
|
||||
*2026-06-20*
|
||||
|
||||
---
|
||||
|
||||
## Goal
|
||||
|
||||
Replace click-through journal entry cards with fully inline posts across the trip page, dailies page, and home page. Each journal entry renders its full content in the feed — title, meta, photo strip, and body text — without requiring navigation to the detail page.
|
||||
|
||||
---
|
||||
|
||||
## Scope
|
||||
|
||||
**In scope:**
|
||||
- Journal entry display in `trip.html.twig`, `dailies.html.twig`, `home.html.twig`
|
||||
- New `.journal-post` CSS component and photo strip styles
|
||||
- Dot-sync JS for the photo strip (one shared block in `base.html.twig`)
|
||||
- Map flash animation extended to `.journal-post.is-highlighted`
|
||||
- Test updates for T1, T2
|
||||
|
||||
**Out of scope:**
|
||||
- Story cards in the feed — remain as click-through `<a class="entry-card entry-card--story">`, unchanged
|
||||
- The journal entry detail page (`entry.html.twig`) — kept as-is; just not linked from the feed
|
||||
- The post form — photos are already uploaded correctly
|
||||
- Lightbox on the feed — only on the detail page
|
||||
|
||||
---
|
||||
|
||||
## Layout
|
||||
|
||||
Each journal entry in the feed renders as:
|
||||
|
||||
```
|
||||
Title (DM Serif Display, ~xl)
|
||||
DATE · 📍 City, Country · ☀️ Weather ← meta row; DATE is the permalink to detail page
|
||||
┌──────────────────────────────────────┐
|
||||
│ │
|
||||
│ Photo (full-width, 3:2 ratio) │ ← swipe left/right for 2–4 photos
|
||||
│ │
|
||||
└──────────────────────────────────────┘
|
||||
● ○ ○ ← dots; hidden when only 1 photo
|
||||
Body text paragraph(s)
|
||||
──────────────────────────────────────── ← border-bottom separator
|
||||
```
|
||||
|
||||
- **Title** sits above the photo, using `var(--font-display)` at `var(--text-xl)`
|
||||
- **Meta row** (date, location, weather) sits between title and photo; the date is a small `<a>` permalink to the detail page, styled in `var(--color-ink-muted)`. Location and weather are plain text spans
|
||||
- **Photo strip**: CSS scroll-snap, no JS library required for swipe
|
||||
- **Dots**: visible only when the entry has 2+ images; update via scroll listener
|
||||
- **Body**: full entry body text — not truncated, not excerpted
|
||||
- **Separator**: `border-bottom: 1px solid var(--color-border)` on the post root, matching the current entry card separator
|
||||
|
||||
---
|
||||
|
||||
## HTML Structure
|
||||
|
||||
```html
|
||||
<article class="journal-post" id="entry-{{ entry.slug }}"
|
||||
data-type="journal" data-lat="{{ entry.header.lat }}" data-lng="{{ entry.header.lng }}">
|
||||
|
||||
<header class="journal-post-header">
|
||||
<h2 class="journal-post-title">{{ entry.title }}</h2>
|
||||
<p class="journal-post-meta">
|
||||
<a class="journal-post-permalink" href="{{ entry.url }}">
|
||||
<time datetime="{{ entry.date|date('Y-m-d') }}">{{ entry.date|date('d M Y')|upper }}</time>
|
||||
</a>
|
||||
{% if entry.header.location_city or entry.header.location_country %}
|
||||
<span class="journal-post-location">
|
||||
· 📍
|
||||
{%- set _loc = [] -%}
|
||||
{%- if entry.header.location_city -%}{%- set _loc = _loc|merge([entry.header.location_city]) -%}{%- endif -%}
|
||||
{%- if entry.header.location_country -%}{%- set _loc = _loc|merge([entry.header.location_country]) -%}{%- endif -%}
|
||||
{{ _loc|join(', ') }}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if entry.header.weather_desc %}
|
||||
<span class="journal-post-weather">· {{ weather_icons[entry.header.weather_desc] ?? '' }} {{ entry.header.weather_desc }}</span>
|
||||
{% endif %}
|
||||
</p>
|
||||
</header>
|
||||
|
||||
{% set images = entry.media.images %}
|
||||
{% if images|length > 0 %}
|
||||
<div class="journal-photo-strip" data-slides="{{ images|length }}">
|
||||
{% for img in images %}
|
||||
<div class="journal-photo-slide">
|
||||
<img src="{{ img.cropResize(900, 600).url }}" alt="{{ entry.title }}" loading="lazy">
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% if images|length > 1 %}
|
||||
<div class="journal-photo-dots" aria-hidden="true">
|
||||
{% for img in images %}
|
||||
<span class="journal-photo-dot{% if loop.first %} is-active{% endif %}"></span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
<div class="journal-post-body">{{ entry.content|raw }}</div>
|
||||
|
||||
</article>
|
||||
```
|
||||
|
||||
**Key attribute notes:**
|
||||
- `id="entry-{{ entry.slug }}"` — required for map marker scroll targeting (`document.getElementById`)
|
||||
- `data-type="journal"` — required for the trip page filter bar (`querySelectorAll('[data-type]')`)
|
||||
- `data-lat` / `data-lng` — required for map marker rendering
|
||||
- The `<article>` root replaces the old `<a class="entry-card">` — the entry is no longer a clickable card
|
||||
|
||||
The `weather_icons` map (currently defined inline in `entry.html.twig`) must also be defined at the top of `trip.html.twig`, `dailies.html.twig`, and `home.html.twig` so the meta row can use it.
|
||||
|
||||
---
|
||||
|
||||
## Photo Strip: CSS
|
||||
|
||||
```css
|
||||
/* ── Journal post ──────────────────────────────────────────── */
|
||||
|
||||
.journal-post {
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
padding-bottom: var(--space-12);
|
||||
margin-bottom: var(--space-12);
|
||||
}
|
||||
|
||||
.journal-post-header {
|
||||
margin-bottom: var(--space-4);
|
||||
}
|
||||
|
||||
.journal-post-title {
|
||||
font-family: var(--font-display);
|
||||
font-size: var(--text-xl);
|
||||
font-weight: 400;
|
||||
line-height: var(--leading-snug);
|
||||
color: var(--color-ink);
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
|
||||
.journal-post-meta {
|
||||
font-size: var(--text-xs);
|
||||
color: var(--color-ink-muted);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.journal-post-permalink {
|
||||
color: var(--color-ink-muted);
|
||||
text-decoration: none;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.07em;
|
||||
}
|
||||
|
||||
.journal-post-permalink:hover { color: var(--color-accent); }
|
||||
|
||||
.journal-post-location,
|
||||
.journal-post-weather {
|
||||
color: var(--color-ink-muted);
|
||||
}
|
||||
|
||||
/* Photo strip */
|
||||
|
||||
.journal-photo-strip {
|
||||
display: flex;
|
||||
overflow-x: scroll;
|
||||
scroll-snap-type: x mandatory;
|
||||
scrollbar-width: none;
|
||||
border-radius: var(--radius-md);
|
||||
margin-bottom: var(--space-3);
|
||||
}
|
||||
|
||||
.journal-photo-strip::-webkit-scrollbar { display: none; }
|
||||
|
||||
.journal-photo-slide {
|
||||
flex: 0 0 100%;
|
||||
scroll-snap-align: start;
|
||||
aspect-ratio: 3 / 2;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.journal-photo-slide img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Dot indicators */
|
||||
|
||||
.journal-photo-dots {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: var(--space-2);
|
||||
margin-bottom: var(--space-4);
|
||||
}
|
||||
|
||||
.journal-photo-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 9999px;
|
||||
background: var(--color-border);
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.journal-photo-dot.is-active {
|
||||
background: var(--color-ink-muted);
|
||||
}
|
||||
|
||||
/* Body */
|
||||
|
||||
.journal-post-body {
|
||||
font-size: var(--text-base);
|
||||
line-height: var(--leading-normal);
|
||||
color: var(--color-ink-2);
|
||||
}
|
||||
|
||||
.journal-post-body p { margin-bottom: var(--space-4); }
|
||||
.journal-post-body p:last-child { margin-bottom: 0; }
|
||||
|
||||
/* Map flash — extends existing keyframe */
|
||||
|
||||
.journal-post.is-highlighted {
|
||||
animation: card-highlight 0.7s ease-out forwards;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Photo Strip: JS
|
||||
|
||||
One shared script block added to `base.html.twig`, just before `</body>`. It is a no-op on pages with no strips.
|
||||
|
||||
```html
|
||||
<script>
|
||||
(function () {
|
||||
document.querySelectorAll('.journal-photo-strip').forEach(function (strip) {
|
||||
var dots = strip.nextElementSibling;
|
||||
if (!dots || !dots.classList.contains('journal-photo-dots')) return;
|
||||
var dotEls = Array.from(dots.querySelectorAll('.journal-photo-dot'));
|
||||
strip.addEventListener('scroll', function () {
|
||||
var idx = Math.round(strip.scrollLeft / strip.offsetWidth);
|
||||
dotEls.forEach(function (d, i) { d.classList.toggle('is-active', i === idx); });
|
||||
}, { passive: true });
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## CSS Cleanup
|
||||
|
||||
The following selectors are used exclusively by the old journal entry card and can be removed from `style.css` once the new `.journal-post` component is in place. Story cards in the feed (`entry-card--story`) do **not** use them:
|
||||
|
||||
- `.entry-card-textmeta` and children (`.entry-date-plain`, `.entry-location-plain`)
|
||||
- `.entry-card-photo-overlay` and children (`.entry-date-overlay`, `.entry-location-overlay`)
|
||||
- `.entry-excerpt`
|
||||
- `.entry-read-more`
|
||||
- `.entry-card .entry-title` — the title rule scoped to `.entry-card`; replace with `.journal-post-title`
|
||||
- `.entry-card:hover .entry-card-photo img` — photo zoom on hover; journal posts have no hover interaction
|
||||
- `.entry-card:hover .entry-title` — title tint on hover; same reason
|
||||
- `.entry-card.is-highlighted` — replaced by `.journal-post.is-highlighted`
|
||||
|
||||
**Keep** the following — they are still used by story cards (`entry-card--story`) or elsewhere:
|
||||
- `.entry-card` base styles — story cards still use this class
|
||||
- `.entry-card-photo` and `.entry-card-photo img` — story cards use `.entry-card-photo--story`
|
||||
- `.entry-card:hover` background lift (in the shared three-card selector) — story cards still hover
|
||||
- All single-entry-page styles (`.entry-hero`, `.entry-header`, `.entry-body`, etc.)
|
||||
|
||||
---
|
||||
|
||||
## Test Updates
|
||||
|
||||
**T1** (`tests/ui/dailies.spec.js`):
|
||||
```js
|
||||
// OLD
|
||||
await expect(page.locator('.entry-card').first()).toBeVisible();
|
||||
// NEW
|
||||
await expect(page.locator('.journal-post').first()).toBeVisible();
|
||||
```
|
||||
|
||||
**T2** (`tests/ui/dailies.spec.js`):
|
||||
```js
|
||||
// OLD — used href on the <a> root
|
||||
const newerCard = page.locator(`.entry-card[href*="${NEWER_SLUG}"]`);
|
||||
const olderCard = page.locator(`.entry-card[href*="${OLDER_SLUG}"]`);
|
||||
// ...
|
||||
findIndex(c => c === el)
|
||||
|
||||
// NEW — use id attribute (journal posts are <article>, not <a>)
|
||||
const newerCard = page.locator(`#entry-${NEWER_SLUG}`);
|
||||
const olderCard = page.locator(`#entry-${OLDER_SLUG}`);
|
||||
// ...
|
||||
findIndex(c => c.id === el.id)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Swipe velocity / momentum — native browser scroll-snap handles this
|
||||
- Lightbox on the feed photo strip — photos are not tappable in the feed; the detail page retains the lightbox
|
||||
- Lazy-load placeholder shimmer
|
||||
- Image ordering UI — photos appear in filesystem order (same as the detail page gallery)
|
||||
@@ -0,0 +1,168 @@
|
||||
# Pixelfed Import & Demo Reorganisation — Design Spec
|
||||
|
||||
**Date:** 2026-06-20
|
||||
**Status:** Approved
|
||||
|
||||
## Overview
|
||||
|
||||
Import 36 Pixelfed posts from `gram.social/m038` (exported as `pixelfed-statuses.json`) into three new permanent trips. Simultaneously reorganise the demo system: move the Italy demo trip to a clearly-labelled 2026 demo slug and retire the Japan demo entries.
|
||||
|
||||
---
|
||||
|
||||
## Scope
|
||||
|
||||
### What this covers
|
||||
|
||||
1. Demo system reorganisation (Italy 2025 demo → `italy-2026-demo`, Japan demo retired)
|
||||
2. Three new real trip page trees
|
||||
3. A one-time Python import script that routes posts to the correct trip, downloads photos, and writes Grav entry folders
|
||||
4. Updated Makefile `demo-load` / `demo-reset` targets
|
||||
|
||||
### What this does not cover
|
||||
|
||||
- Generating proper titles (done interactively with Claude after import)
|
||||
- Adding GPX routes to real trips
|
||||
- Lat/lng geocoding (location data from the export has city/country only, no coordinates)
|
||||
|
||||
---
|
||||
|
||||
## Part 1: Demo System Reorganisation
|
||||
|
||||
### 1a. Italy demo moves to `italy-2026-demo`
|
||||
|
||||
Copy `user/docs/demo/trips/italy-2025/` → `user/docs/demo/trips/italy-2026-demo/`.
|
||||
|
||||
Update the trip page frontmatter inside the new demo source:
|
||||
```yaml
|
||||
title: 'Italy 2026 (Demo)'
|
||||
slug: italy-2026-demo
|
||||
```
|
||||
|
||||
Create the real page tree at `user/pages/01.trips/italy-2026-demo/` with the standard four subfolders.
|
||||
|
||||
Update Makefile:
|
||||
- `demo-load`: replace all `italy-2025` references with `italy-2026-demo`
|
||||
- `demo-reset`: replace `rm -rf user/pages/01.trips/italy-2025` with `rm -rf user/pages/01.trips/italy-2026-demo`
|
||||
|
||||
`italy-2025` is never touched by demo commands after this change.
|
||||
|
||||
### 1b. Japan demo retired
|
||||
|
||||
Remove all `japan-korea-2026` blocks from `demo-load` and `demo-reset`.
|
||||
|
||||
The source files in `user/docs/demo/trips/japan-korea-2026/` stay on disk as a backup but are not loaded by any make target. The `japan-korea-2026` trip structure and any real content committed there remains untouched.
|
||||
|
||||
### 1c. Italy 2025 demo stories removed
|
||||
|
||||
The 3 Tuscany demo stories currently at `user/pages/01.trips/italy-2025/04.stories/` (if present on disk) are deleted — they are moving to the demo trip. The `04.stories/` folder itself is kept with its `stories.md` index page.
|
||||
|
||||
---
|
||||
|
||||
## Part 2: Real Trip Page Trees
|
||||
|
||||
Three trips get the standard structure: `trip.md`, `01.dailies/dailies.md`, `02.map/map.md`, `03.stats/stats.md`, `04.stories/stories.md`.
|
||||
|
||||
| Slug | Title | Action |
|
||||
|---|---|---|
|
||||
| `central-asia-2023` | Central Asia 2023 | Create new |
|
||||
| `us-canada-mex-2024` | Northern America 2024 | Create new |
|
||||
| `italy-2025` | Cycling Tuscany 2025 | Exists — update title only |
|
||||
|
||||
All three are committed to the `user/` git repo as permanent content.
|
||||
|
||||
---
|
||||
|
||||
## Part 3: Pixelfed Import Script
|
||||
|
||||
### Location
|
||||
|
||||
`scripts/pixelfed-import.py`
|
||||
|
||||
Invoked via: `make pixelfed-import`
|
||||
|
||||
### Input
|
||||
|
||||
`/home/mischa/Nextcloud/Downloads/pixelfed/pixelfed-statuses.json` (36 posts, Pixelfed v1 export format)
|
||||
|
||||
### Trip routing by year
|
||||
|
||||
| `created_at` year | Target trip |
|
||||
|---|---|
|
||||
| 2023 | `central-asia-2023` |
|
||||
| 2024 | `us-canada-mex-2024` |
|
||||
| 2025 | `italy-2025` |
|
||||
|
||||
Posts are numbered per trip (1-indexed), reset for each trip.
|
||||
|
||||
### Output folder structure
|
||||
|
||||
For each post, one folder inside `user/pages/01.trips/{trip}/01.dailies/`:
|
||||
|
||||
```
|
||||
{YYYY-MM-DD}-pixelfed-{N}.entry/
|
||||
entry.md
|
||||
photo-1.jpg
|
||||
photo-2.jpg
|
||||
...
|
||||
```
|
||||
|
||||
Where `N` is the per-trip sequence number and `YYYY-MM-DD` comes from `created_at`.
|
||||
|
||||
### Field mapping
|
||||
|
||||
| Pixelfed field | Grav frontmatter field | Notes |
|
||||
|---|---|---|
|
||||
| `created_at` | `date` | ISO 8601 → `Y-m-d H:i` |
|
||||
| *(generated)* | `title` | `"Pixelfed Import {N}"` |
|
||||
| `place.name` | `location_city` | Empty string if `place` is null |
|
||||
| `place.country` | `location_country` | Empty string if `place` is null |
|
||||
| *(none)* | `lat`, `lng` | Always empty — no coordinate data in export |
|
||||
| *(none)* | `weather_temp_c`, `weather_desc` | Always empty |
|
||||
| first downloaded photo filename | `hero_image` | e.g. `photo-1.jpg` |
|
||||
| `content_text` | body | Already HTML-stripped in export |
|
||||
|
||||
Fixed frontmatter values: `template: entry`, `published: true`.
|
||||
|
||||
### Photo download
|
||||
|
||||
For each item in `media_attachments`:
|
||||
- Download from `url` field
|
||||
- Save as `photo-{index}.jpg` (1-indexed) regardless of original filename
|
||||
- Use the extension from the `mime` field (`image/png` → `.png`, `image/jpeg` → `.jpg`)
|
||||
- Set `hero_image` in frontmatter to the filename of the first downloaded photo
|
||||
|
||||
### Error handling
|
||||
|
||||
- If a photo download fails, log a warning and continue (do not abort the post)
|
||||
- If the output folder already exists, skip that post (idempotent re-runs)
|
||||
|
||||
### Make target
|
||||
|
||||
```makefile
|
||||
pixelfed-import:
|
||||
python3 scripts/pixelfed-import.py
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## File Map
|
||||
|
||||
| File | Change |
|
||||
|---|---|
|
||||
| `user/docs/demo/trips/italy-2026-demo/` | New — copy of italy-2025 demo source with updated title/slug |
|
||||
| `user/pages/01.trips/italy-2026-demo/` | New — demo trip page tree |
|
||||
| `user/pages/01.trips/italy-2025/trip.md` | Update title to "Cycling Tuscany 2025" |
|
||||
| `user/pages/01.trips/italy-2025/04.stories/` | Remove 3 demo story subfolders |
|
||||
| `user/pages/01.trips/central-asia-2023/` | New — real trip page tree |
|
||||
| `user/pages/01.trips/us-canada-mex-2024/` | New — real trip page tree |
|
||||
| `Makefile` | Update demo-load / demo-reset targets |
|
||||
| `scripts/pixelfed-import.py` | New — one-time import script |
|
||||
|
||||
---
|
||||
|
||||
## Constraints
|
||||
|
||||
- Never read `.env` directly
|
||||
- All CSS uses design tokens — script produces no CSS
|
||||
- Import script writes to `user/pages/` only; caller runs `make content-push` afterwards to commit and sync
|
||||
- The `italy-2025` trip must never appear in `demo-load` or `demo-reset` after this change
|
||||
@@ -0,0 +1,165 @@
|
||||
# UI/UX Alignment — Design Spec
|
||||
|
||||
*2026-06-20*
|
||||
|
||||
---
|
||||
|
||||
## Goal
|
||||
|
||||
Unify three disconnected micro-interaction patterns across the site:
|
||||
|
||||
1. **Back navigation** — inconsistent style and position across story and entry pages
|
||||
2. **Card hover** — inconsistent lift behaviour and structural divergence across the three card types
|
||||
3. **Map flash** — no visual feedback after the feed scrolls to a marker-targeted card
|
||||
|
||||
---
|
||||
|
||||
## 1. Back pill system
|
||||
|
||||
### Canonical pill component
|
||||
|
||||
The site is dark-themed (`--color-paper: #1A1814`, `--color-ink: #EDE8DF` cream). Two visual variants of a single pill component, chosen by what is behind it:
|
||||
|
||||
**Surface pill** (sits on the dark paper/canvas background):
|
||||
```css
|
||||
background: var(--color-canvas);
|
||||
border: 1px solid var(--color-border);
|
||||
color: var(--color-ink);
|
||||
border-radius: 9999px;
|
||||
padding: 0.4rem 0.9rem;
|
||||
font-size: var(--text-sm);
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
transition: border-color 0.15s, color 0.15s;
|
||||
```
|
||||
Hover: `border-color: var(--color-accent); color: var(--color-accent)`
|
||||
|
||||
**Overlay pill** (sits on top of a hero photo):
|
||||
```css
|
||||
background: rgba(0,0,0,0.6);
|
||||
backdrop-filter: blur(4px);
|
||||
color: var(--color-ink);
|
||||
border-radius: 9999px;
|
||||
padding: var(--space-2) var(--space-4);
|
||||
font-size: var(--text-sm);
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
```
|
||||
Hover: `color: var(--color-accent)`
|
||||
|
||||
The `.story-totop` button already matches the surface pill tokens (`--color-canvas` bg, `--color-border` border, `--color-ink` text) — it becomes part of this system without visual changes.
|
||||
|
||||
### Pill inventory
|
||||
|
||||
| Element | Page | Variant | Position | Notes |
|
||||
|---|---|---|---|---|
|
||||
| `.story-escape` | story | overlay | `fixed`, top-left | Overlays hero; keep as-is |
|
||||
| `← Back` in story body | story | surface | `static`, below-hero body section | Apply surface pill class |
|
||||
| Entry top back | entry | surface | `fixed`, `top: calc(var(--site-header-height) + var(--space-3))`, left | New element |
|
||||
| Entry footer back | entry | surface | `static`, in `.entry-footer` | Replaces current teal text link |
|
||||
| `.story-totop` | story | surface | `fixed`, bottom-right | Existing; bring into token system |
|
||||
|
||||
### Shared behaviour
|
||||
|
||||
All back pills use the same `onclick` pattern already present on `.story-escape`:
|
||||
```js
|
||||
onclick="if(history.length > 1){ history.back(); return false; }"
|
||||
```
|
||||
Fallback `href` is always `page.parent().url`.
|
||||
|
||||
---
|
||||
|
||||
## 2. Card hover unification
|
||||
|
||||
### Structural fix — entry card markup
|
||||
|
||||
Entry cards currently use a two-level structure (`<article>` wrapping `<a class="entry-card-inner">`), which causes the hover target to differ from trip and story cards. This diverges for no functional reason — `id` and `data-*` attributes are valid on `<a>` elements.
|
||||
|
||||
**Before:**
|
||||
```html
|
||||
<article class="entry-card" id="entry-{{ entry.slug }}"
|
||||
data-type="journal" data-lat="..." data-lng="...">
|
||||
<a class="entry-card-inner" href="{{ entry.url }}">
|
||||
...
|
||||
</a>
|
||||
</article>
|
||||
```
|
||||
|
||||
**After:**
|
||||
```html
|
||||
<a class="entry-card" id="entry-{{ entry.slug }}"
|
||||
data-type="journal" data-lat="..." data-lng="..."
|
||||
href="{{ entry.url }}">
|
||||
...
|
||||
</a>
|
||||
```
|
||||
|
||||
The class `.entry-card-inner` is eliminated. All CSS rules previously on `.entry-card-inner` move to `.entry-card`. The map's `document.getElementById('entry-' + slug)` continues to work unchanged.
|
||||
|
||||
The story variant card in the trip feed (`entry-card--story`) follows the same structural change.
|
||||
|
||||
### Hover pattern
|
||||
|
||||
All three card root elements get a uniform background lift:
|
||||
|
||||
```css
|
||||
.trip-card:hover,
|
||||
.entry-card:hover,
|
||||
.story-card:hover {
|
||||
background: var(--color-surface-raised);
|
||||
}
|
||||
```
|
||||
|
||||
Existing per-card effects are additive on top of the lift:
|
||||
- **Entry card**: photo zoom (`transform: scale(1.04)`) + title tint (`color: var(--color-accent)`) — keep
|
||||
- **Story card**: shadow (`box-shadow: var(--shadow-md)`) — keep
|
||||
- **Trip card**: border accent (`border-color: var(--color-accent)`) — keep
|
||||
|
||||
Transition values align across all three cards: `transition: background 0.15s, border-color 0.15s`.
|
||||
|
||||
---
|
||||
|
||||
## 3. Map flash
|
||||
|
||||
### Problem
|
||||
|
||||
After clicking a marker on the trip page mini-map, `scrollIntoView({ behavior: 'smooth', block: 'center' })` scrolls the feed but provides no visual confirmation of which card arrived.
|
||||
|
||||
### Solution
|
||||
|
||||
A 700ms keyframe animation adds a faint teal wash to the targeted card, delayed 350ms after the click to let the scroll complete first.
|
||||
|
||||
**CSS:**
|
||||
```css
|
||||
@keyframes card-highlight {
|
||||
0% { background-color: color-mix(in srgb, var(--color-accent) 12%, transparent); }
|
||||
100% { background-color: transparent; }
|
||||
}
|
||||
|
||||
.entry-card.is-highlighted {
|
||||
animation: card-highlight 0.7s ease-out forwards;
|
||||
}
|
||||
```
|
||||
|
||||
**JS (in `trip.html.twig`, marker click handler):**
|
||||
```js
|
||||
el.addEventListener('click', function () {
|
||||
var card = document.getElementById('entry-' + entry.slug);
|
||||
if (!card) return;
|
||||
card.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
setTimeout(function () {
|
||||
card.classList.add('is-highlighted');
|
||||
setTimeout(function () { card.classList.remove('is-highlighted'); }, 700);
|
||||
}, 350);
|
||||
});
|
||||
```
|
||||
|
||||
The `is-highlighted` class is removed after the animation so it can re-trigger on repeated clicks of the same marker.
|
||||
|
||||
---
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Semantics/accessibility audit of feed list containers and landmark roles (logged as backlog)
|
||||
- `<article>` element on full entry/story pages (logged as backlog)
|
||||
- `.story-totop` behaviour changes — visual tokens only
|
||||
@@ -0,0 +1,183 @@
|
||||
# Documentation Restructure — Design Spec
|
||||
|
||||
**Date:** 2026-06-21
|
||||
**Scope:** Full restructure of `docs/` from organic flat layout to type-first hierarchy serving two personas: Mischa and Claude.
|
||||
|
||||
---
|
||||
|
||||
## Problem
|
||||
|
||||
The `docs/` folder grew organically: milestone specs, design specs, plans, QA docs, research, and how-tos sit at the same level with no clear navigation. There is no operational how-to for posting, GPX management, or trip switching. CLAUDE.md contains setup and architecture detail that inflates its size and makes it harder to scan.
|
||||
|
||||
---
|
||||
|
||||
## Goals
|
||||
|
||||
1. Two personas find what they need without searching: **Mischa** (poster, PM, designer, dev) and **Claude** (AI assistant).
|
||||
2. `guides/` is written for Mischa now but extensible to external users later (future: publish as a Grav CMS travel setup).
|
||||
3. CLAUDE.md stays lean — inline context only, no content duplicated from `docs/`.
|
||||
|
||||
---
|
||||
|
||||
## Folder Structure
|
||||
|
||||
```
|
||||
docs/
|
||||
guides/ ← operational how-tos; Mischa-facing; extensible to external users
|
||||
reference/ ← stable facts: design system, architecture
|
||||
working/ ← active project docs: specs, plans, QA, backlog, milestones
|
||||
specs/
|
||||
plans/
|
||||
qa/
|
||||
milestones/
|
||||
research/ ← raw discovery input
|
||||
README.md ← navigation index
|
||||
```
|
||||
|
||||
### Persona mapping
|
||||
|
||||
| Persona | Primary sections |
|
||||
|---|---|
|
||||
| Mischa (operational) | `guides/` for how-tos; `working/` for PM status |
|
||||
| Mischa (design/dev) | `reference/` for design system + architecture |
|
||||
| Claude | `working/specs/` + `working/plans/` for active context; `reference/` for stable facts; `CLAUDE.md` for always-loaded project rules |
|
||||
|
||||
---
|
||||
|
||||
## File Migration
|
||||
|
||||
### guides/ — new and extracted content
|
||||
|
||||
| File | Source |
|
||||
|---|---|
|
||||
| `guides/posting.md` | new — end-to-end posting flow |
|
||||
| `guides/gpx-manager.md` | new — GPX upload/delete/slug/Komoot workaround |
|
||||
| `guides/trip-switching.md` | new — 3-file checklist, expanded |
|
||||
| `guides/local-setup.md` | extracted from CLAUDE.md §2 |
|
||||
|
||||
### reference/ — moved and new
|
||||
|
||||
| File | Source |
|
||||
|---|---|
|
||||
| `reference/design-system.md` | moved from `docs/design/design-spec.md` |
|
||||
| `reference/architecture.md` | new — stack, plugin roles, template hierarchy, post-submission data flow |
|
||||
|
||||
### working/ — moved and renamed
|
||||
|
||||
| New path | Current path |
|
||||
|---|---|
|
||||
| `working/specs/*` (13 files) | `docs/superpowers/specs/*` |
|
||||
| `working/plans/*` (14 files) | `docs/superpowers/plans/*` |
|
||||
| `working/milestones/milestone-1.md` | `docs/milestone-1-spec.md` |
|
||||
| `working/milestones/milestone-2.md` | `docs/milestone-2-spec.md` |
|
||||
| `working/milestones/milestone-3.md` | `docs/milestone-3-spec.md` |
|
||||
| `working/milestones/milestone-4.md` | `docs/milestone-4-spec.md` |
|
||||
| `working/backlog.md` | `docs/backlog.md` |
|
||||
| `working/production-todo.md` | `docs/production-todo.md` |
|
||||
| `working/pm-analysis.md` | `docs/pm-analysis.md` |
|
||||
| `working/qa/test-plan.md` | `docs/qa-test-plan.md` |
|
||||
| `working/qa/results.md` | `docs/qa-results.md` |
|
||||
| `working/bugs-and-fixes.md` | `docs/bugs-and-fixes.md` |
|
||||
| `working/summary.md` | `docs/summary.md` |
|
||||
|
||||
### research/ — moved and renamed
|
||||
|
||||
| New path | Current path |
|
||||
|---|---|
|
||||
| `research/polarsteps.md` | `docs/research-polarsteps.md` |
|
||||
| `research/findpenguins.md` | `docs/research-findpenguins.md` |
|
||||
| `research/story-editing.md` | `docs/research-story-editing.md` |
|
||||
|
||||
---
|
||||
|
||||
## New Content
|
||||
|
||||
### guides/posting.md
|
||||
|
||||
Covers: opening `/post`, all form fields (title, body, location, weather fetch, lat/lng, photos), what happens on submit (form plugin → add-page-by-form → cache-on-save), how to verify the entry appeared, common failure modes (cache not cleared, entry in wrong trip folder).
|
||||
|
||||
### guides/gpx-manager.md
|
||||
|
||||
Covers: logging in, upload flow, slug rules (spaces/special chars → hyphens, lowercase), how slugification works (client-side Blob trick), delete flow, how to bypass the UI (drop file into `user/pages/01.trips/<slug>/` + `make content-push`), Komoot manual export workaround (no API integration yet).
|
||||
|
||||
### guides/trip-switching.md
|
||||
|
||||
Covers: the 3-file checklist — `user/config/site.yaml` (`active_trip`), `user/pages/02.post/post-form.md` (`pageconfig.parent`) — why both must match, what breaks silently if they don't (entries post to wrong folder), and the new trip page tree to create under `user/pages/01.trips/<new-slug>/`.
|
||||
|
||||
### guides/local-setup.md
|
||||
|
||||
Covers: first-time setup after clone (`mkdir -p user/plugins user/data`, `make setup`), fix-perms after 500 errors, Grav 2.0 upgrade process (update Dockerfile URL + `make setup`), required system.yaml settings (`accounts.type: flex`, `pages.type: flex`), admin user API permissions, disabling old `admin` plugin, language URL prefix fix.
|
||||
|
||||
### reference/architecture.md
|
||||
|
||||
Covers:
|
||||
- **Stack**: Grav 2.0.0-rc.10 + Admin2, Docker image, PHP session config
|
||||
- **Plugin roles**: form (built-in) → add-page-by-form (third-party) → cache-on-save (custom); what each does in the post-submission pipeline
|
||||
- **Template hierarchy**: `base.html.twig` extended by all page templates; key templates: `trip.html.twig`, `entry.html.twig`, `map.html.twig`, `stats.html.twig`, `gpx-manager.html.twig`
|
||||
- **Data flow for a post**: form submit → page created in dailies folder → cache cleared → entry visible in feed
|
||||
- **GPX pipeline**: files on trip page media → picked up by `map.html.twig` via `trip_page.media.all` → rendered by MapLibre
|
||||
|
||||
### docs/README.md
|
||||
|
||||
```markdown
|
||||
# docs/
|
||||
|
||||
## If you're Mischa
|
||||
- **Doing something?** → guides/
|
||||
- **Checking project status?** → working/backlog.md, working/production-todo.md
|
||||
- **Design or architecture decisions?** → reference/
|
||||
|
||||
## If you're Claude
|
||||
- **Project rules + always-needed context** → CLAUDE.md (root)
|
||||
- **Active specs and plans** → working/specs/, working/plans/
|
||||
- **Stable facts** → reference/
|
||||
- **Raw research** → research/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## CLAUDE.md Changes
|
||||
|
||||
**Remove** (moves to `docs/`):
|
||||
- §2 local development setup → `docs/guides/local-setup.md`; replace with one-line pointer
|
||||
- Architecture/plugin detail → `docs/reference/architecture.md`; replace with one-line pointer
|
||||
|
||||
**Add** (superpowers skill path overrides):
|
||||
|
||||
```markdown
|
||||
### Superpowers skill paths
|
||||
Specs: `docs/working/specs/YYYY-MM-DD-<topic>-design.md`
|
||||
Plans: `docs/working/plans/YYYY-MM-DD-<topic>.md`
|
||||
```
|
||||
|
||||
The brainstorming and writing-plans skills default to `docs/superpowers/`; these lines override that default so generated files land in the right place automatically.
|
||||
|
||||
**Keep inline** (always-loaded context Claude needs without following a link):
|
||||
- §0 project specifics (folder layout, stack versions, trip entity architecture, active trip, GPX pipeline, env rules, remote operations, content sync, gitignore)
|
||||
- §1 environment modes (dev vs. prod settings, cache-on-save behaviour)
|
||||
- Language URL prefix gotcha
|
||||
- Grav 2.0 config requirements (flex accounts/pages, admin user permissions)
|
||||
|
||||
---
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- End-user documentation (blog readers) — deferred until external publish decision
|
||||
- `user/docs/` folder — separate git repo; not restructured here
|
||||
- Memory files (`~/.claude/projects/*/memory/`) — not part of `docs/`; maintained separately
|
||||
- Design/UX wireframes — stay in existing spec files, not reorganized further
|
||||
|
||||
---
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
1. `docs/` contains exactly four subdirectories: `guides/`, `reference/`, `working/`, `research/`
|
||||
2. All 32+ existing files are moved to their new paths; no files remain at `docs/` root except `README.md`
|
||||
3. `docs/superpowers/` no longer exists; content is under `working/`
|
||||
4. Four new guides exist and cover their stated scope
|
||||
5. `reference/architecture.md` exists and covers stack, plugin roles, template hierarchy, and post data flow
|
||||
6. `docs/README.md` exists with persona-based navigation
|
||||
7. CLAUDE.md no longer contains §2 local setup block; contains pointer to `docs/guides/local-setup.md`
|
||||
8. All internal cross-references in moved files updated to new paths
|
||||
9. Memory files that reference `docs/superpowers/` paths updated to `docs/working/`
|
||||
10. CLAUDE.md contains superpowers skill path overrides pointing to `docs/working/specs/` and `docs/working/plans/`
|
||||
@@ -0,0 +1,102 @@
|
||||
# Entry Enrichment — Design Spec
|
||||
|
||||
**Date:** 2026-06-21
|
||||
**Status:** Approved
|
||||
|
||||
## Overview
|
||||
|
||||
Enrich all real trip journal entries with `location_city`, `location_country`, `lat`, `lng`, `weather_temp_c`, and `weather_desc` using an in-chat review workflow. One Markdown review doc per trip; Claude infers values, user corrects, Claude applies to YAML.
|
||||
|
||||
---
|
||||
|
||||
## Scope
|
||||
|
||||
### What this covers
|
||||
|
||||
- Filling in `location_city` and `location_country` where blank
|
||||
- Geocoding to `lat`/`lng` for all entries with a city
|
||||
- Approximate seasonal `weather_temp_c` and `weather_desc` for each entry date + location
|
||||
- Three real trips: `central-asia-2023`, `us-canada-mex-2024`, `italy-2025`
|
||||
|
||||
### What this does not cover
|
||||
|
||||
- Adding new journal entries (content creation)
|
||||
- Adding GPX tracks to central-asia or us-canada-mex
|
||||
- Historical weather API lookups (values are seasonal approximations, not exact)
|
||||
|
||||
---
|
||||
|
||||
## Review Document Format
|
||||
|
||||
One file per trip at `docs/enrichment/{trip-slug}.md`.
|
||||
|
||||
### Table columns
|
||||
|
||||
| Column | Source | Notes |
|
||||
|---|---|---|
|
||||
| Entry | folder name | e.g. `2023-09-05-pixelfed-8.entry` |
|
||||
| Date | `date` frontmatter | `YYYY-MM-DD` |
|
||||
| Title | `title` frontmatter | Read-only reference |
|
||||
| City | inferred from title+body | Edit to correct |
|
||||
| Country | inferred from title+body | Edit to correct |
|
||||
| Lat | extracted from Map Link | Do not edit directly; update Map Link instead |
|
||||
| Lng | extracted from Map Link | Do not edit directly; update Map Link instead |
|
||||
| Map Link | OSM link `https://www.openstreetmap.org/#map=15/{lat}/{lng}` | Replace with corrected OSM or Google Maps link |
|
||||
| Temp °C | seasonal approximation | Edit directly if wrong |
|
||||
| Weather | seasonal approximation | Edit directly if wrong (e.g. `sunny`, `cloudy`, `rainy`) |
|
||||
|
||||
### Coordinate extraction rules
|
||||
|
||||
When reading back a reviewed doc, extract lat/lng from Map Link using these URL patterns:
|
||||
|
||||
- **OSM:** `openstreetmap.org/#map={zoom}/{lat}/{lng}` → use the two numbers after `#map=N/`
|
||||
- **Google Maps:** `maps.google.com/.../@{lat},{lng},{zoom}z` or `maps.app.goo.gl/...` (follow redirect, then parse)
|
||||
|
||||
If a cell has no Map Link (blank city), lat/lng are left empty.
|
||||
|
||||
---
|
||||
|
||||
## Inference Rules
|
||||
|
||||
1. Read `title` first — most locations are explicit ("Poutine and French Echoes in Old Montreal").
|
||||
2. Fall back to body text if title is ambiguous.
|
||||
3. If neither title nor body reveals a location, leave City/Country blank and note it for manual fill.
|
||||
4. City = the specific city or town; Country = the country.
|
||||
|
||||
---
|
||||
|
||||
## Weather Approximation
|
||||
|
||||
Fill `weather_temp_c` with a single integer (the approximate daytime high in °C for that city + month). Fill `weather_desc` with one word: `sunny`, `cloudy`, `partly cloudy`, `rainy`, `cold`, or `hot`. Based on known climate patterns — not historical API data.
|
||||
|
||||
---
|
||||
|
||||
## Application Step
|
||||
|
||||
After user approves a reviewed doc, Claude:
|
||||
|
||||
1. Re-reads the table row by row
|
||||
2. Extracts coordinates from Map Link (or leaves blank if no link)
|
||||
3. Updates the corresponding `entry.md` frontmatter fields in-place
|
||||
4. Reports a summary of changes made
|
||||
|
||||
No scripts are written — changes are applied directly via Edit tool.
|
||||
|
||||
---
|
||||
|
||||
## Order of Execution
|
||||
|
||||
1. `central-asia-2023` — 22 entries (generate doc → review → apply)
|
||||
2. `us-canada-mex-2024` — 12 entries (generate doc → review → apply)
|
||||
3. `italy-2025` — 2 entries (generate doc → review → apply)
|
||||
|
||||
---
|
||||
|
||||
## File Map
|
||||
|
||||
| File | Change |
|
||||
|---|---|
|
||||
| `docs/enrichment/central-asia-2023.md` | New — review table, 22 rows |
|
||||
| `docs/enrichment/us-canada-mex-2024.md` | New — review table, 12 rows |
|
||||
| `docs/enrichment/italy-2025.md` | New — review table, 2 rows |
|
||||
| `user/pages/01.trips/*/01.dailies/*/entry.md` | Update 6 frontmatter fields per entry |
|
||||
@@ -0,0 +1,205 @@
|
||||
# Homepage Redesign Spec
|
||||
|
||||
**Date:** 2026-06-21
|
||||
**Goal:** Make the homepage context-aware: a persistent two-column map+feed layout that switches its right column between an active-trip feed and a curated highlights grid depending on whether Mischa is currently travelling.
|
||||
|
||||
---
|
||||
|
||||
## 1. Mode switch
|
||||
|
||||
A `travelling` toggle in `user/config/site.yaml` controls which mode the homepage renders. It is exposed in Admin2's Site Configuration panel via a new site config blueprint.
|
||||
|
||||
| `travelling` | Homepage mode |
|
||||
|---|---|
|
||||
| `true` | Active trip — map + live feed |
|
||||
| `false` | Between trips — map + highlights grid |
|
||||
|
||||
The `active_trip` value changes format: it now stores the full page route (`/trips/italy-2026-demo`) instead of the bare slug (`italy-2026-demo`), because it will be managed via a `type: pages` dropdown in Admin2 rather than a free-text field.
|
||||
|
||||
---
|
||||
|
||||
## 2. Data model changes
|
||||
|
||||
### 2a. New file: `user/blueprints/config/site.yaml`
|
||||
|
||||
Exposes site config fields in Admin2:
|
||||
|
||||
```yaml
|
||||
active_trip:
|
||||
type: pages
|
||||
label: Active Trip
|
||||
start_route: '/trips'
|
||||
show_root: false
|
||||
show_slug: true
|
||||
|
||||
travelling:
|
||||
type: toggle
|
||||
label: Currently Travelling
|
||||
highlight: 1
|
||||
default: false
|
||||
```
|
||||
|
||||
### 2b. `user/config/site.yaml` — value format update
|
||||
|
||||
```yaml
|
||||
# Before
|
||||
active_trip: italy-2026-demo
|
||||
|
||||
# After
|
||||
active_trip: /trips/italy-2026-demo
|
||||
travelling: false
|
||||
```
|
||||
|
||||
### 2c. Trip page blueprint (`user/themes/intotheeast/blueprints/trip.yaml`)
|
||||
|
||||
Add one field:
|
||||
|
||||
```yaml
|
||||
tagline:
|
||||
type: text
|
||||
label: Tagline
|
||||
help: Short description shown on homepage highlight cards (e.g. "6 weeks from Venice to Sicily by train")
|
||||
```
|
||||
|
||||
### 2d. Entry blueprint (`user/themes/intotheeast/blueprints/entry.yaml`)
|
||||
|
||||
Add one field:
|
||||
|
||||
```yaml
|
||||
featured:
|
||||
type: toggle
|
||||
label: Featured highlight
|
||||
help: Show this entry as a homepage highlight when not travelling
|
||||
default: false
|
||||
```
|
||||
|
||||
### 2e. Story blueprint (`user/themes/intotheeast/blueprints/story.yaml`)
|
||||
|
||||
Add the same `featured` toggle (identical definition). Stories are not auto-included — they opt in the same way as journal entries.
|
||||
|
||||
---
|
||||
|
||||
## 3. Layout
|
||||
|
||||
The two-column structure is always present regardless of mode.
|
||||
|
||||
```
|
||||
┌────────────────────────┬────────────────────────────────┐
|
||||
│ │ │
|
||||
│ MapLibre map │ Right column │
|
||||
│ (sticky, │ (switches by mode) │
|
||||
│ always visible) │ │
|
||||
│ │ │
|
||||
└────────────────────────┴────────────────────────────────┘
|
||||
```
|
||||
|
||||
- Map: left column, ~45% width, `position: sticky; top: 0; height: 100vh`
|
||||
- Right column: ~55% width, scrollable
|
||||
- Mobile: map stacks on top at `40vh`, right column scrolls below
|
||||
|
||||
---
|
||||
|
||||
## 4. Active trip mode (`travelling: true`)
|
||||
|
||||
### Right column
|
||||
|
||||
Chronological feed, newest first. Merges journal entries and story cards from the active trip's `/dailies` and `/stories` sub-pages. This is the existing feed behaviour — no changes to card markup or order logic.
|
||||
|
||||
Trip title and entry counts shown above the feed.
|
||||
|
||||
### Map
|
||||
|
||||
- Marker per journal entry with `lat`/`lng` in frontmatter
|
||||
- Journey line connecting markers in order
|
||||
- GPX route files loaded from the active trip page media (same pattern as `map.html.twig`, including the smart connector-suppression logic from the GPX connector spec)
|
||||
- Clicking a marker scrolls to that entry card in the feed
|
||||
|
||||
### Template change (`home.html.twig`)
|
||||
|
||||
The slug-based path construction is replaced with direct route usage:
|
||||
|
||||
```twig
|
||||
{# Before #}
|
||||
{% set slug = config.site.active_trip %}
|
||||
{% set trip = grav.pages.find('/trips/' ~ slug) %}
|
||||
{% set dailies_page = grav.pages.find('/trips/' ~ slug ~ '/dailies') %}
|
||||
{% set stories_page = grav.pages.find('/trips/' ~ slug ~ '/stories') %}
|
||||
|
||||
{# After #}
|
||||
{% set trip_route = config.site.active_trip %}
|
||||
{% set trip = grav.pages.find(trip_route) %}
|
||||
{% set dailies_page = grav.pages.find(trip_route ~ '/dailies') %}
|
||||
{% set stories_page = grav.pages.find(trip_route ~ '/stories') %}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Between-trips mode (`travelling: false`)
|
||||
|
||||
### Highlight selection logic
|
||||
|
||||
1. Collect all published trip pages from `/trips`
|
||||
2. For each trip, collect all published children from `/dailies` and `/stories` where `featured: true`
|
||||
3. From each trip's candidates, pick one at random (`random()`)
|
||||
4. Gather the per-trip picks into a pool; if more than 6 trips have candidates, randomly discard down to 6
|
||||
5. Shuffle the final pool so cards appear in random order (not grouped by trip)
|
||||
|
||||
### Right column
|
||||
|
||||
A grid of highlight cards. Below the grid, a "Explore all past trips →" CTA linking to `/trips`.
|
||||
|
||||
**Grid layout:** 3 columns on desktop, 2 on tablet, 1 on mobile.
|
||||
|
||||
### Highlight card anatomy
|
||||
|
||||
```
|
||||
┌──────────────────────────┐
|
||||
│ [hero image] │
|
||||
├──────────────────────────┤
|
||||
│ ✦ Story / ◎ Journal │ ← type badge
|
||||
│ Entry title │ ← links to entry page
|
||||
│ Italy 2025 │ ← trip title
|
||||
│ "tagline from trip" │ ← trip tagline
|
||||
│ → View trip │ ← links to trip page
|
||||
└──────────────────────────┘
|
||||
```
|
||||
|
||||
- Hero image: `entry.media.images|first` if no `hero_image` frontmatter field; cropped to 16:9
|
||||
- Type badge: `✦ Story` (accent colour) or `◎ Journal` (muted)
|
||||
- Entry title: full clickable link to the entry URL
|
||||
- Trip title + tagline: small secondary text; trip title links to the trip page
|
||||
- "→ View trip": explicit CTA link to the trip page
|
||||
|
||||
Cards with no hero image still render but without an image block.
|
||||
|
||||
### Map
|
||||
|
||||
- Marker per highlighted entry that has `lat`/`lng` in frontmatter
|
||||
- No journey line between markers (entries are from different trips)
|
||||
- No GPX data loaded
|
||||
- Map fits bounds across all markers; falls back to a world-level zoom if no entries have coordinates
|
||||
- Clicking a marker scrolls to that highlight card
|
||||
|
||||
---
|
||||
|
||||
## 6. Files changed
|
||||
|
||||
| File | Change |
|
||||
|---|---|
|
||||
| `user/blueprints/config/site.yaml` | **Create** — exposes `active_trip` (pages) + `travelling` (toggle) in Admin2 |
|
||||
| `user/config/site.yaml` | **Update** — `active_trip` value to full route; add `travelling: false` |
|
||||
| `user/themes/intotheeast/blueprints/trip.yaml` | **Update** — add `tagline` text field |
|
||||
| `user/themes/intotheeast/blueprints/entry.yaml` | **Update** — add `featured` toggle |
|
||||
| `user/themes/intotheeast/blueprints/story.yaml` | **Update** — add `featured` toggle |
|
||||
| `user/themes/intotheeast/templates/home.html.twig` | **Update** — mode branch, route-based lookup, highlights logic, GPX loading |
|
||||
| `user/themes/intotheeast/css/style.css` | **Update** — highlight card styles, grid layout |
|
||||
|
||||
No new plugins. No build pipeline. All changes in `user/`.
|
||||
|
||||
---
|
||||
|
||||
## 7. Constraints
|
||||
|
||||
- `post-form.md` (`pageconfig.parent`) remains manually synced with `active_trip` — this is unchanged behaviour documented in CLAUDE.md
|
||||
- The `type: pages` field in Admin2 is confirmed present in the bundle but untested in a user site config blueprint; if it does not render, fall back to `type: select` with static trip slug options (one-minute fix, no other code changes needed)
|
||||
- Random selection uses Twig's `random()` — order varies per page load; this is intentional
|
||||
Reference in New Issue
Block a user