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:
@@ -27,7 +27,7 @@ Two git repos:
|
|||||||
| `tests/` | Playwright suite — see [`docs/reference/testing.md`](docs/reference/testing.md) |
|
| `tests/` | Playwright suite — see [`docs/reference/testing.md`](docs/reference/testing.md) |
|
||||||
| `php/` | Local PHP ini overrides |
|
| `php/` | Local PHP ini overrides |
|
||||||
| `docs/` | All project documentation — start at [`docs/README.md`](docs/README.md) |
|
| `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/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/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 |
|
| `docs/working/` | Specs, plans, backlog, QA — work in flight |
|
||||||
|
|||||||
@@ -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.
|
||||||
Reference in New Issue
Block a user