# Reference

Complete reference for marp-compose content authors.

## Content directory structure

```text
content/
  bookends/<slug>/index.md    intro and wrap-up slides; same format as topics
  breaks/<slug>/index.md      custom break slides (optional)
  schedules/<slug>/index.md   frontmatter + marker body
  topics/<slug>/index.md      slide content for one topic
  topics/<slug>/<assets>      images, .excalidraw files, etc.
decks/
  schedules/<slug>/           composeSchedule output
  topics/<slug>/              composeTopic output
reference/                    source material (PDFs, PPTs, etc.) — never composed
```

## Topic files

A topic is a Markdown file at `content/topics/<slug>/index.md`. Put any images and assets alongside it in the same directory.

### Title resolution

The schedule builder reads only the topic body, not its frontmatter. The title is taken from the first `# h1` heading in the body; if there is no h1, the slug is used as the fallback.

Duration and all other metadata come from the schedule marker (`{{ slug, 45m, type: model }}`), not from the topic file.

### Frontmatter

Always include `marp: true`. This enables standalone topic builds for validating a topic while you're building it.

You can include `theme:` for standalone topic builds, but it will be ignored when the topic is included in a schedule — the schedule's own `theme:` applies instead (or, if the schedule has none, the theme from `marp.config.js`). Frontmatter always takes precedence over `marp.config.js`, so a schedule `theme:` beats a config theme.

### Body conventions

- Write normal Marp markdown — `---` separates slides.
- The topic body is inlined verbatim into the composed deck. No wrapping separator is added.

## Bookend files

Bookends (`content/bookends/<slug>/index.md`) are intro and wrap-up slides — typically a title slide at the start of a session or a closing slide at the end. They follow the same format as topic files and are referenced in the schedule body the same way.

## Schedule files

A schedule is a Markdown file at `content/schedules/<slug>/index.md`.

### Frontmatter fields

| Field         | Type   | Required  | Description                                                                                                                                                                   |
|---------------|--------|-----------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `title`       | string | no        | Used if no `# h1` is found in the body; slug is the final fallback                                                                                                            |
| `description` | string | no        | Shown in the presenter HUD                                                                                                                                                    |
| `timings`     | string | see note  | Session durations (see syntax below). Required unless all sessions come from `{{ schedule }}` includes.                                                                       |
| `start_time`  | string | no        | Wall-clock start of the first session (`9:00`, `9am`, `09:00`). A single value applies to all days; multiple days use `;` separator. Not valid alongside time-range `timings`.|

### `timings` syntax

| Example                                             | Meaning                                                        |
|-----------------------------------------------------|----------------------------------------------------------------|
| `90m`                                               | One session of 90 minutes                                      |
| `2 x 90m`                                           | Two sessions of 90 minutes each                                |
| `90m, 60m, 90m`                                     | Three sessions with individual durations                       |
| `4 x 90m; 4 x 90m`                                  | Two days: four 90-minute sessions per day                      |
| `9:00-10:30, 11:00-12:30`                           | Two sessions by wall-clock time (30 min break implied)         |
| `9:00-10:30, 11:00-12:30; 9:00-10:30, 11:00-12:30`  | Two days with wall-clock times                                 |

Commas between sessions are optional — spaces work equally well. The `-` separator within time ranges and the `;` day separator are always required.

All entries within a single day must use the same syntax — mixing durations and time ranges within a day is a hard error.

When time ranges are used, the gap between consecutive sessions implies the break duration. A `{{ break }}` marker may omit its duration and the gap value is used. If a break marker specifies an explicit duration that differs from the implied gap, the build fails.

When a schedule is composed from `{{ schedule }}` includes and has no `timings` of its own, `timings` are inherited by joining each child's `timings` with `;`.

### Body structure

The body contains `---` slide separators, Markdown headings and content, and markers (see Marker syntax below).

Sessions within a day are delimited by `{{ break <type> }}` markers. Days are delimited by `***` on its own line — a standard Markdown thematic break that Marp does not treat as a slide separator. The final `***` at the end of a schedule is optional; marp-compose closes the last day automatically.

## Marker syntax

Markers are double-brace expressions in the schedule body. Commas between fields are optional — spaces work equally well. Multi-line commentary is supported and ignored:

```text
{{ topic code-is-cost, 40m
   Commentary here — ignored by the parser.
}}
```

### Marker reference

| Marker                    | Kind     | Description                                               |
|---------------------------|----------|-----------------------------------------------------------|
| `{{ slug }}`              | content  | Include topic by bare slug                                |
| `{{ topic slug }}`        | content  | Explicit `topic` keyword — identical to bare slug         |
| `{{ slug, 45m }}`         | content  | Include topic with duration override                      |
| `{{ topic slug, 45m }}`   | content  | Explicit with duration override                           |
| `{{ break coffee, 15m }}` | break    | Coffee break with duration. Optional in time-range mode.  |
| `{{ break lunch, 60m }}`  | break    | Lunch break with duration                                 |
| `{{ schedule slug }}`     | schedule | Replace with the body of the named child schedule         |
| `***`                     | day end  | Closes the day; use between days. `***` is optional.      |

### Key-value pairs

Additional key-value pairs can be passed after the duration and are passed through to the deck outline payload items:

```text
{{ topic why-habits-matter, 30m, type: model }}
{{ the-habit-loop, 45m, optional: true }}
```

## CLI

```text
marp-compose [-C <dir>] <slug> [options]
```

Pass a slug and `marp-compose` resolves it automatically — topics under `content/topics/` and schedules under `content/schedules/`.

### Options

| Flag               | Description                                                       |
|--------------------|-------------------------------------------------------------------|
| `-C <dir>`         | Change working directory before composing                         |
| `--preview`, `-p`  | Compose and open in live-preview mode (see below)                 |
| `--draft`          | Skip missing slugs; insert a placeholder slide instead of failing |
| `--help`, `-h`     | Show usage                                                        |

## Live preview

`--preview` (or `-p`) composes the deck, opens it in Chrome, and watches the source directory for changes. Edits to any topic, schedule, or `.excalidraw` file trigger an automatic recompose and browser reload.

```sh
marp-compose my-offsite --preview
```

If you are using `@goalwards/deck`, `deck preview` is the preferred alias:

```sh
deck preview my-offsite
```

`--draft` works in preview mode too — missing items get a placeholder slide instead of failing:

```sh
marp-compose my-offsite --preview --draft
```

Press `Ctrl-C` to stop. The preview server cleans up the Chrome profile on exit.

## Excalidraw stages

If a topic directory contains `.excalidraw` files, `marp-compose` automatically runs `excalidraw-stages` on them during every compose.

**With frames** — each Excalidraw frame (named with a numeric prefix, e.g. `01-problem`, `02-loop`) is rendered as a separate SVG into a `<source>__stages/` directory alongside the deck output. Reference them in order to build up a diagram across slides:

```markdown
![](my-diagram__stages/01-problem.svg)
![](my-diagram__stages/02-loop.svg)
```

**Without frames** — the entire file is exported as `my-diagram__stages/image.svg`.

In `--preview` mode, changes to any `.excalidraw` file also trigger a re-run of `excalidraw-stages` followed by a browser reload, so the SVGs stay in sync as you edit the diagram.

## Build stages

The `{{ build }}` directive expands a single slide into multiple slides — one per selected Excalidraw stage. Use it to reveal a diagram step by step within a topic.

### Syntax

```text
{{ build slug }}
{{ build slug range }}
{{ build slug range; alt text }}
```

`slug` is the base name of an `.excalidraw` file in the topic directory (without the extension). `marp-compose` resolves the staged SVGs from `<slug>__stages/` in the compiled output — run the Excalidraw stages step first if the SVGs are not yet generated.

**`alt text`** — optional image alt text, placed after a `;` separator. Applied to every generated image tag.

### Range syntax

The range selects which stages to include. Indices are 1-based; negative indices count from the end.

| Range form            | Meaning                                  |
|-----------------------|------------------------------------------|
| _(omitted)_ or `..`   | All stages                               |
| `n`                   | Exactly stage n                          |
| `n..m`                | Stages n through m (inclusive)           |
| `n..`                 | Stage n to the last                      |
| `..m`                 | First stage through m                    |
| `-n`                  | The nth-from-last stage                  |
| `-n..`                | From the nth-from-last stage to the last |

Out-of-range values — including indices larger than the stage count or a `start > end` range — are hard errors that abort the build.

### Speaker notes

Each generated slide gets a `<!-- build m of n -->` comment prepended at the very top, where `m` is the 1-based position within the selected range and `n` is the total selected. This comment appears first in the presenter HUD notes. The last slide in a sequence always has `m === n`.

### Constraints

- Only one `{{ build }}` directive is allowed per slide. Two directives on the same slide is a hard error.
- `{{ build }}` is only valid in topic and bookend files. Using it in schedule markdown is a hard error.
- The expanded slides are transient — `marp-compose` writes them to `index-built.md` in the deck output directory; the source `index.md` is never modified.

## Transitions

Transitions animate the movement from one slide to the next. Add a Marp transition comment to any slide to apply a named transition at that boundary:

```markdown
<!-- _transition: morph -->
```

The comment applies to the boundary _after_ the slide it appears on. To use a different transition when navigating backwards across the same boundary, add a second comment:

```markdown
<!-- _transition: morph -->
<!-- _transition-back: fade -->
```

### morph

The built-in `morph` transition smoothly animates named text elements and Excalidraw diagram elements that appear on both sides of a slide boundary.

**Text elements** — wrap any inline text with `[text]{morph}` or `[text]{morph=tag}`:

```markdown
# [Welcome back]{morph=title}

- the [most important thing]{morph} is consistency
```

- `{morph}` — the tag is derived automatically by slugifying the text (`most-important-thing`)
- `{morph=tag}` — explicit tag; use this when the text changes between slides

Any two elements on adjacent slides that share the same tag morph into each other. Elements without a matching partner on the other slide fade out (or in).

**Excalidraw diagram elements** — morphing between diagram elements happens automatically. When a topic uses `excalidraw-stages` to build up a diagram across slides, elements that appear in both stage N and stage N+1 will animate smoothly between their positions. No extra markup is needed.

### Custom transitions

You can supply your own transition or override a built-in by dropping a JS file in `transitions/` at your content root:

```text
content/
  transitions/
    my-transition.js     ← single-file transition
    morph/               ← multi-file override (files joined in sort order)
      morph.js
```

`marp-compose` checks `content-root/transitions/` before falling back to built-ins, so a `transitions/morph.js` in your content root replaces the built-in morph entirely.

## Presenting your deck

Once `marp-compose` succeeds, the output lives in `decks/schedules/<slug>/` (or `decks/topics/<slug>/`). That directory contains:

- `index.html` — the rendered slide deck (includes embedded metadata for the presenter HUD)
- `schedule.md` — the composed markdown (schedules only)

Hand this to `marp-present`:

```sh
marp-present <slug>          # present a schedule
marp-present topic <slug>    # present a standalone topic
```

See `docs/tutorial.md` and `docs/reference.md` in the marp-present repository for the full presenter guide.

## Schedule errors

These errors abort the build. The error text is shown in the terminal.

| Error message | Cause | Fix |
| --- | --- | --- |
| `timings declares N sessions for day D but body has M sessions` | The number of `{{ break <type> }}`-delimited sessions in the body doesn't match the session count in `timings`. | Count `{{ break }}` markers in the body and reconcile with `timings`. |
| `duration required: '<slug>' has no duration in schedule marker` | A content marker has no duration. | Add a duration: `{{ slug, 45m }}`. Use `--draft` to skip this check during authoring. |
| `session N on day D has Xm of content but only Ym declared` | The sum of topic durations in a session exceeds the declared session duration. | Shorten topics, remove one, or increase the session duration in `timings`. |
| `break '<type>' on day D declares Xm but timings imply Ym` | A `{{ break }}` marker specifies a duration that doesn't match the gap between time-range sessions. | Remove the duration from the break marker (it will be inferred), or adjust the time ranges. |
| `start_time is redundant when timings use time ranges — remove one` | `start_time` is set in frontmatter but `timings` already encodes start times as ranges. | Remove either `start_time` or switch `timings` to duration notation. |
| `cannot mix time ranges and durations in timings: "..."` | A single day's `timings` mixes `90m` durations and `9:00-10:30` ranges. | Use one form consistently within each day. |
| `invalid time range "...": end must be after start` | A time range ends before or at the same time it starts. | Swap the times or correct the values. |
| `time ranges overlap: session N ends at HH:MM but session M starts at HH:MM` | Consecutive time-range sessions overlap. | Adjust the ranges so sessions don't overlap. |
| `{{ build }} directives are not allowed in schedule markdown` | A `{{ build }}` directive appears in a schedule file rather than a topic file. | Move the `{{ build }}` into the topic's `index.md`. |
