# 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//stories/`. Stories live at: ``` user/pages/01.trips//04.stories//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 `
` 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 `

` is flattened by admin2's CSS reset, which is why the headings use `` in a styled `

` instead. Long `

` 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.