docs(guides): add story-authoring guide; list it in the README index

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-29 23:16:22 +02:00
co-authored by Claude Opus 5
parent cfe070efec
commit e106da0206
2 changed files with 229 additions and 1 deletions
+1 -1
View File
@@ -27,7 +27,7 @@ Two git repos:
| `tests/` | Playwright suite — see [`docs/reference/testing.md`](docs/reference/testing.md) |
| `php/` | Local PHP ini overrides |
| `docs/` | All project documentation — start at [`docs/README.md`](docs/README.md) |
| `docs/guides/` | Operational how-tos (posting, GPX, trip switching, setup, deploy cycle) |
| `docs/guides/` | Operational how-tos (posting, story authoring, GPX, trip switching, setup, deploy cycle) |
| `docs/reference/` | Stable facts: architecture, design system, testing |
| `docs/solutions/` | Write-ups of bugs and workflow traps already hit, with YAML frontmatter (`module`, `tags`, `problem_type`) |
| `docs/working/` | Specs, plans, backlog, QA — work in flight |
+228
View File
@@ -0,0 +1,228 @@
# Writing a Story
A Story is a long-form, hand-crafted piece with an immersive layout — distinct from an Entry, which is a quick dated post from the road (see [`CONCEPTS.md`](../../CONCEPTS.md)). Stories get a Ken Burns hero, scroll-driven sections, galleries and pull quotes.
The admin editor gives you a **plain markdown textarea** for the body. There is no block picker — the layout vocabulary is a set of shortcodes you type by hand. This guide is that vocabulary; the story edit form also carries a condensed copy of it on its **Blocks** tab, so you don't need this file open while writing.
Admin lives at **`/admin`** (the plugin slug is `admin2`, but the route is `/admin`). A story's edit URL looks like `/admin/pages/edit/trips/<trip>/stories/<slug>`.
Stories live at:
```
user/pages/01.trips/<trip>/04.stories/<slug>/story.md
```
`04.stories/stories.md` is a `routable: false` container — its children are aggregated onto the trip page. There is no standalone `/stories` view; don't create one.
---
## 1. Create the page
1. Admin → **Pages** → add a page under the trip's **Stories** folder
2. Set page template to **story** — this loads [`user/themes/intotheeast/blueprints/story.yaml`](../../user/themes/intotheeast/blueprints/story.yaml). You get the story-specific **Content / Blocks / Location / Publishing** tabs plus the inherited **Options / Advanced / Security** tabs
3. Fill in Title and Start Date (both required)
---
## 2. Upload the images first
Upload every image the story needs via the **Images** field at the bottom of the Content tab, before writing the body.
> There is no separate *Media* tab — the uploader is a field on the Content tab, labelled **Images**. It arrives via inheritance: `story.yaml` declares `'@extends': {type: default, context: blueprints://pages}`, which merges in Grav's default page form. `story.yaml` and `trip.yaml` did not originally extend it, so neither form could upload anything, which is why the demo story images had to be placed on the filesystem. Both now inherit, matching [`entry.yaml`](../../user/themes/intotheeast/blueprints/entry.yaml). `home.yaml` is still standalone, deliberately — the home page has no per-page media.
Every shortcode refers to images by **bare filename** — the `story-blocks` plugin prefixes the page URL at render time ([`story-blocks.php:22`](../../user/plugins/story-blocks/story-blocks.php)), so you write:
```
image="photo-1.jpg" ✅
image="/images/photo-1.jpg" ❌ don't path it
```
The demo stories use a `hero.jpg` / `photo-1.jpg` / `photo-2.jpg` naming convention. Worth copying — it keeps the shortcodes readable.
---
## 3. Frontmatter fields
All of these come from the form tabs, so you rarely type them by hand. Listed here because the body shortcodes are *not* the whole story — the hero in particular is frontmatter, not a tag.
| Field | Tab | Notes |
|---|---|---|
| `title` | Content | Required |
| `date` | Content | Required. Start date |
| `end_date` | Content | Optional — leave blank for a single-day story |
| `hero_image` | Content | **The hero. Filename only**, from the Images field. Missing → grey placeholder, story still renders |
| `hero_alt` | Content | Falls back to the title if empty |
| `location_name`, `location_country` | Location | Shown in the hero meta line and the opener |
| `lat`, `lng` | Location | Decimal degrees — places the story marker on the trip map |
| `transport_mode` | Location | walking / bicycle / bus / train / car |
| `force_connect` | Location | Always draw a connector line from the previous marker |
| `published` | Options | Grav's standard toggle, from the inherited form |
| `featured` | Publishing | Show as a homepage highlight when not travelling |
The Ken Burns pan on the hero is automatic — no parameter for it.
If you hand-write frontmatter, use `date: '2026-09-03'`. Admin2 saves its own serialization (`29-07-2026 19:51`); both parse fine.
---
## 4. The body: six shortcodes
Defined in [`user/plugins/story-blocks/shortcodes/`](../../user/plugins/story-blocks/shortcodes/). Plain prose between them renders as a normal reading column — you don't need a shortcode to write paragraphs.
### Wrapping tags
**`scrolly-section`** — text panels scroll over a pinned, slowly panning image. The centrepiece block.
```
[scrolly-section image="hero.jpg" alt="Description of the image" caption="Optional caption"]
The first panel. Scrolls into view over the image.
---
The second panel. A markdown `---` starts a new panel.
[/scrolly-section]
```
Panels are split on the `<hr>` that `---` produces ([`story.html.twig:234`](../../user/themes/intotheeast/templates/story.html.twig)). `caption` is optional. Under `prefers-reduced-motion` all panels render active with no pinning.
**`pull-quote`** — large extracted quote, optionally over a background image.
```
[pull-quote image="photo-1.jpg" alt="Description of the image"]
The quote itself. Markdown works in here.
[/pull-quote]
```
Drop `image`/`alt` entirely for the plain no-image variant.
### Self-closing tags
Note the ` /]` — these take no content.
**`chapter-break`** — full-width section transition over a background image.
```
[chapter-break image="photo-1.jpg" title="After Dark" number="II" alt="Description" /]
```
`number` is optional; the demo stories use roman numerals.
**`snap-gallery`** — swipeable multi-image carousel with dots.
```
[snap-gallery images="hero.jpg,photo-1.jpg" captions="First caption,Second caption" alts="First alt,Second alt" /]
```
⚠️ See the comma gotcha below.
**`full-bleed`** — single image edge-to-edge, as a visual pause.
```
[full-bleed image="photo-2.jpg" alt="Description" caption="Optional" credit="Optional" /]
```
**`image-caption`** — photo at a chosen width with caption beneath.
```
[image-caption image="photo-2.jpg" alt="Description" caption="Optional" credit="Optional" width="column" /]
```
`width` accepts `column` (default), `full`, `bleed`. Anything else falls back to `column`.
`full-bleed` and `image-caption` are implemented but not yet used by any story — the demos only exercise the other four.
---
## Gotchas
### snap-gallery splits on commas — captions cannot contain them
`images`, `captions` and `alts` are each split on `,` and zipped by index. **A comma inside a caption shifts every caption after it.**
This is already live in the demo content. `04.stories/04.florence-without-a-map/story.md` has:
```
captions="The Arno at noon — greener than expected, the bridges older than you remember,Via dei Servi: …"
```
Two images, but three comma-separated pieces — so slide 1 gets "The Arno at noon — greener than expected", slide 2 gets " the bridges older than you remember", and the Via dei Servi text is silently dropped.
Use em dashes or semicolons in gallery captions. There is no escaping mechanism.
### Self-closing tags need the space before `/]`
`[chapter-break … /]` — not `[chapter-break …/]` or `[chapter-break …]`.
### A typo'd shortcode fails silently
An unrecognised tag name or a malformed parameter list renders as literal text or vanishes — no error, no warning. Preview the page after every block; there is no in-editor validation.
### `---` outside a scrolly-section is just a horizontal rule
The panel-splitting behaviour only applies *inside* `[scrolly-section]`.
---
## Worked example
The best reference to copy from is [`user/pages/01.trips/italy-2026-demo/04.stories/01.sorano-rock-and-time/story.md`](../../user/pages/01.trips/italy-2026-demo/04.stories/01.sorano-rock-and-time/story.md) — it combines `scrolly-section`, `chapter-break` and `pull-quote` in one story.
```markdown
---
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 clinging to pale tufa cliffs at dusk
published: true
---
Opening prose. Renders as a normal reading column.
[scrolly-section image="hero.jpg" alt="Sorano seen from the approach road" caption="Sorano — tufa cliff town"]
First panel over the pinned image.
---
Second panel.
[/scrolly-section]
More prose between blocks.
[chapter-break image="photo-1.jpg" title="After Dark" number="II" alt="Narrow medieval alley at dusk" /]
[pull-quote image="photo-1.jpg" alt="Stone alley lit by a single lantern"]
A town built on rock, carved from rock, returning slowly to rock.
[/pull-quote]
```
---
## Publish
1. Set **Published** on the Publishing tab
2. Optionally set **Featured highlight** to surface it on the homepage between trips
3. Push the content:
```bash
make content-push
```
That commits and pushes `user/` to Gitea, which triggers the production pull. See [`deploy-cycle.md`](deploy-cycle.md).
---
## The Blocks tab
The story edit form has a **Blocks** tab holding a paste-ready example of each shortcode plus the comma warning. It's built from `type: spacer` fields in `story.yaml`, whose `text` admin2 renders as HTML (confirmed against admin2 2.0.12 — see its CHANGELOG entry for issue #91).
If you edit those fields, note two things: the `text` values are YAML double-quoted scalars, so HTML attribute quotes must be escaped (`\"`) — and `<h4>` is flattened by admin2's CSS reset, which is why the headings use `<strong>` in a styled `<p>` instead. Long `<pre>` content needs `white-space:pre-wrap`, or it overflows underneath the Page Info sidebar.
---
## Why it's a raw textarea
The admin2 story editor is a plain markdown field by design-so-far, not by limitation. Background and the options considered: [`docs/research/story-editing.md`](../research/story-editing.md). Note that its conclusion that admin2 cannot host a custom editor is **out of date** — admin-next ships a plugin field-component surface (`admin-next/fields/{type}.js` + `onApiBlueprintResolved`) that the installed api 1.0.9 / admin2 2.0.12 support. Nobody has built it yet.