# Markdown API

> Reading, editing, validating, and compiling a deck from code — the four @getnarro/markdown entry points

`@getnarro/markdown` is a library, not only a file format. The CLI's `check`,
`dev`, and `build`, the MCP server's slide-editing tools, and this
documentation's own generators are all built on the exports below — none of them
parses a deck by hand.

Reach for it when you are writing something that produces or edits decks: a
generator, an importer, a lint rule, a CMS integration, an agent tool.

## Four entry points

The specifier matters, and the split is by *where the code runs*, not by topic.
The root is Node-safe — it pulls in no React — which is why `narroMarkdown`,
`DeckShell`, and the layout components are not there. Reaching for one of those
from `@getnarro/markdown` fails at resolution.

The exception worth knowing: the shared **types** (`SlideLayoutProps`,
`SlideLayout`, `DeckFrontmatter`, `SlideFrontmatter`) are re-exported from both
the root and `/runtime`, because a layout component needs them and a Node-side
tool inspecting a deck does too. Either import works.

| Import from | Use it for | Runs in |
| --- | --- | --- |
| `@getnarro/markdown` | Parsing, editing, validating, compiling | Node — no React, no DOM |
| `@getnarro/markdown/vite` | The build plugin | Node, at build time |
| `@getnarro/markdown/runtime` | Rendering a compiled deck | The browser |
| `@getnarro/markdown/layouts` | The built-in layout components and registry | The browser |

Every **runtime** export, read out of the built modules rather than transcribed:

<!-- generated:markdown-exports -->
- `@getnarro/markdown` — `BUILTIN_THEME`, `CompileError`, `FrontmatterError`, `TEMPLATE_EXTENSIONS`, `TemplateError`, `applyTemplateToSlide`, `buildDeckModule`, `builtinLayoutNames`, `builtinLayoutSlots`, `closest`, `compileAndValidate`, `compileDeck`, `compileMdx`, `compileSlide`, `deckFrontmatterJsonSchema`, `deckFrontmatterKeys`, `defineTemplate`, `discoverTemplatePath`, `emptySlideFrontmatter`, `extractNotes`, `formatDiagnostics`, `interpolate`, `isBuiltinLayout`, `isReservedKey`, `loadTemplateFile`, `parseDeckDocument`, `parseDeckFrontmatter`, `parseSlideFrontmatter`, `parseTemplate`, `parseYamlObject`, `resolveDeckTemplate`, `serializeDeckDocument`, `slideFrontmatterJsonSchema`, `slideFrontmatterKeys`, `slideFrontmatterOf`, `slideId`, `slideTitle`, `slotsForBuiltinLayout`, `splitDeck`, `splitSlots`, `templateCss`, `templateJsonSchema`, `templateLayout`, `templateLayoutNames`, `templateLayoutRuntime`, `templateLayoutSlots`, `templateSchema`, `templateSlotMap`, `tokensCss`, `validateCompiledDeck`, `validateDeck`, `validateTemplate`
- `@getnarro/markdown/vite` — `narroMarkdown`
- `@getnarro/markdown/runtime` — `DeckMetaContext`, `DeckShell`, `DeckSlide`, `RsStep`, `SlideChrome`, `createTemplateLayout`, `deckMetaFrom`, `defaultMdxComponents`, `renderDeck`, `useDeckMeta`
- `@getnarro/markdown/layouts` — `CoverLayout`, `DefaultLayout`, `EndLayout`, `ImageFullLayout`, `ImageRightLayout`, `QuoteLayout`, `SectionLayout`, `ThreeColumnLayout`, `TwoColumnLayout`, `builtinLayoutNames`, `builtinLayouts`
<!-- /generated:markdown-exports -->

Type-only exports are erased at runtime, so they do not appear in that list. The
ones you are most likely to reach for, and where each lives — the shipped
`.d.ts` for each entry point is the complete answer:

| Type | Import from |
| --- | --- |
| `ParsedDeck`, `ParsedSlideSource` | `@getnarro/markdown` |
| `DeckDocument`, `RawSlide` | `@getnarro/markdown` |
| `CompiledDeck`, `CompiledSlide`, `CompiledSlot`, `CompileOptions` | `@getnarro/markdown` |
| `Diagnostic`, `DiagnosticSeverity`, `ValidateOptions` | `@getnarro/markdown` |
| `Slot`, `JsonSchema` | `@getnarro/markdown` |
| `BuiltinLayoutName` | `@getnarro/markdown` or `/layouts` |
| `DeckFrontmatter`, `SlideFrontmatter`, `SlideLayout`, `SlideLayoutProps` | `@getnarro/markdown` or `/runtime` |
| `DeckShellProps`, `DeckSlideProps`, `DeckData`, `DeckSlideData`, `MdxComponents` | `@getnarro/markdown/runtime` |
| `NarroMarkdownOptions`, `ResolvedTheme` | `@getnarro/markdown/vite` |

## Reading a deck: two functions, and they are not interchangeable

This is the distinction to get right before anything else.

```ts
import { parseDeckDocument, splitDeck } from "@getnarro/markdown";

splitDeck(source);          // for rendering — frontmatter parsed into objects
parseDeckDocument(source);  // for editing — frontmatter kept as its original text
```

`splitDeck` returns a `ParsedDeck`: deck frontmatter as a typed object, the
shared MDX preamble, and one `ParsedSlideSource` per slide with its parsed
frontmatter and 1-based `startLine`. It is the right shape for compiling or
inspecting, and it **loses the original YAML text** — reserialising from it
would reformat every slide in the file.

`parseDeckDocument` returns a `DeckDocument`: the same fence-aware split, but
frontmatter and body as verbatim strings. It round-trips:

```ts
serializeDeckDocument(parseDeckDocument(source)) === source;   // true
```

That guarantee is what makes programmatic editing safe. A tool can splice one
slide and write the file back without touching the other forty, which is the
difference between an edit a human will accept in review and a whole-file
reformat.

One caveat: line endings are normalised to `\n` on parse, so a CRLF file
round-trips to LF rather than byte for byte. Everything else — spacing, YAML
key order, comments, trailing whitespace — is preserved.

Both are fence-aware: a `---` inside a fenced code block never splits a slide.

## Editing a deck

`DeckDocument.slides` is a plain array of `{ frontmatterText, body }`, so
insertion, deletion, and reordering are array operations:

```ts
import {
  parseDeckDocument,
  serializeDeckDocument,
  slideId,
  slideTitle,
} from "@getnarro/markdown";

const doc = parseDeckDocument(await readFile("deck.md", "utf8"));

// Where does the slide about revenue live?
const index = doc.slides.findIndex((slide) => slideTitle(slide)?.includes("Revenue"));

// Insert after it.
doc.slides.splice(index + 1, 0, {
  frontmatterText: "layout: section\nid: outlook",
  body: "\n## Outlook\n",
});

await writeFile("deck.md", serializeDeckDocument(doc));
```

| Function | Returns |
| --- | --- |
| `slideFrontmatterOf(slide)` | The slide's frontmatter parsed into a `SlideFrontmatter`, without disturbing the stored text |
| `slideId(slide, index)` | The slide's `id`, or `slide-<index + 1>` when it has none |
| `slideTitle(slide)` | The first heading in the body, with any trailing `{.attribute}` block stripped — or `undefined` |

`slideId` taking the index is deliberate: a deck where only some slides set `id`
still needs a stable key for every slide, and the caller always knows the
position. Address slides by `id` where one exists — an index shifts the moment a
slide is inserted above it, which is why `narro new` and the templates set `id`
on slides worth returning to.

## Validating

`validateDeck` is what `narro check` runs. It returns diagnostics rather than
throwing, so an author who mistyped three layout names learns all three in one
pass.

```ts
import { formatDiagnostics, splitDeck, validateDeck } from "@getnarro/markdown";
import { themes } from "@getnarro/marketplace";

const diagnostics = validateDeck(splitDeck(source), {
  extraLayouts: ["hero", "compare"],          // layouts the host can resolve
  availableThemes: themes.map((t) => t.id),   // omit to skip the theme check
  knownDeckKeys: deckFrontmatterKeys,         // omit to skip the key check
});

if (diagnostics.length > 0) console.log(formatDiagnostics(diagnostics, "deck.md"));
```

Each `Diagnostic` carries `severity` (`"error"` or `"warning"`), an optional
zero-based `slide`, an optional 1-based `line`, and a `message`.

Three things it catches, none of which is a parse error and none of which used
to stop a build:

| Mistake | What happened without validation |
| --- | --- |
| `layout: sidebar-left` — no such layout | The plugin emitted a `throw` into the generated module, so the build succeeded and the deck failed in the browser |
| `::rihgt::` — a mistyped slot | Content attached to a prop no layout reads, and never rendered |
| `aspectRation: "16:9"` — a mistyped deck key | Preserved as an unknown extra and ignored |

Every option is opt-in because each needs knowledge the library does not have:
which layouts the host can resolve, which themes are installed. Omitting one
skips that check rather than guessing.

`closest(value, candidates)` is exported separately — it is the "did you mean?"
matcher, and it is worth reusing so your tool's suggestions match the CLI's.

Severity is chosen deliberately: an unrecognised **deck** key is a warning, not
an error, because a theme is allowed to define its own keys. Pass `strict` to
the Vite plugin to promote warnings.

## Compiling

```ts
import { compileDeck } from "@getnarro/markdown";

const compiled = await compileDeck(parsed, { path: "/abs/deck.md", dev: true });
```

`compileDeck` turns a `ParsedDeck` into a `CompiledDeck`: per slide, its
`index`, `id`, resolved `layout` name, `frontmatter`, extracted `notes`, and one
`CompiledSlot` per slot — each carrying generated ESM module `code` rather than
a React element, so the caller decides how to load it.

`compileSlide` and `compileMdx` are the same machinery at slide and fragment
granularity. `dev: true` emits `jsx-dev-runtime` calls with source positions.

Failures throw `CompileError`, which carries `slideIndex` so a caller can point
at the slide rather than the file.

`buildDeckModule` assembles the ESM module the Vite plugin serves; you need it
only if you are replacing that plugin.

## Frontmatter

```ts
import {
  deckFrontmatterKeys,
  parseDeckFrontmatter,
  parseSlideFrontmatter,
  slideFrontmatterKeys,
} from "@getnarro/markdown";
```

Both parsers preserve unrecognised keys on `extra` rather than dropping or
rejecting them — deck-level for the theme to read, slide-level forwarded to the
layout component as props. `emptySlideFrontmatter()` gives you the zero value,
and `parseYamlObject` is the underlying reader.

The recognised keys are
<!-- generated:deck-frontmatter -->`title`, `author`, `date`, `theme`, `template`, `aspectRatio`, `transition`, `class`, `keyboard`, `mouse`, `touch`, `routing`, `favicon`, `maxDuration`<!-- /generated:deck-frontmatter -->
for a deck and
<!-- generated:slide-frontmatter -->`layout`, `class`, `id`, `transition`, `background`, `notes`<!-- /generated:slide-frontmatter -->
for a slide.

`deckFrontmatterJsonSchema()` and `slideFrontmatterJsonSchema()` return the same
contract as JSON Schema, built from the same zod objects the parser validates
against — so the published schema cannot describe a key the parser rejects. They
are served at:

- <https://getnarro.com/schema/deck-frontmatter.schema.json>
- <https://getnarro.com/schema/slide-frontmatter.schema.json>

Both set `additionalProperties: true`, because forbidding unknown keys would
contradict the `extra` behaviour above.

## Layouts

```ts
import { builtinLayoutNames, slotsForBuiltinLayout } from "@getnarro/markdown";
import { builtinLayouts } from "@getnarro/markdown/layouts";
```

`builtinLayoutSlots` is the source of truth for both. Ask it rather than
hardcoding a table — this page's own layout table was hand-maintained once and
was wrong about three of nine rows.

`slotsForBuiltinLayout(name)` returns the slot names or `undefined` if the name
is not built in, which is also how you test for a built-in without a second
lookup. `isBuiltinLayout` is the type guard.

The full table of names and slots is on the
[markdown mode](/docs/markdown-mode#layouts) page.

## Runtime

```tsx
import { DeckShell, DeckSlide, renderDeck } from "@getnarro/markdown/runtime";
import type { SlideLayoutProps } from "@getnarro/markdown/runtime";
```

`renderDeck(deck)` is the one-call path: it renders a `DeckShell` wrapping one
`DeckSlide` per slide, and is what the generated deck module calls.

`DeckShell` configures a core `Presentation` from deck frontmatter — `keyboard`,
`mouse`, `touch`, and `routing` all default to `true`, `aspectRatio` to `"16:9"`.
`DeckSlide` renders one slide: it takes the resolved `layout` component, the
per-slot compiled components (`default` is required), the frontmatter, and
optional `components` for user-provided MDX scope.

`SlideLayoutProps` is what a layout component receives: `children` is the
default slot, `frontmatter` is the resolved slide frontmatter including
forwarded `extra` keys, and every named slot arrives as a `ReactNode` prop
under its own name.

`RsStep` and `defaultMdxComponents` are the fragment plumbing — see
[markdown mode](/docs/markdown-mode#fragments) for what `{.step}` compiles to.

## The Vite plugin

```ts
import { narroMarkdown } from "@getnarro/markdown/vite";
```

| Option | Type | Default | For |
| --- | --- | --- | --- |
| `deckPath` | `string` | required | Absolute path to the deck `.md` |
| `theme` | `ResolvedTheme` | — | The resolved theme, whose layouts shadow the built-ins |
| `themeResolver` | `(name: string) => ResolvedTheme \| undefined` | — | How to resolve the deck's own `theme:` key |
| `templatePath` | `string` | — | The [deck template](/docs/deck-templates), overriding both the deck's `template:` key and the file found beside it |
| `dev` | `boolean` | `true` | Development mode |
| `cssResolveDir` | `string` | the deck's directory | Where `@import "tailwindcss"` resolves from — set it when the deck directory has no `node_modules` |
| `availableThemes` | `readonly string[]` | — | Theme names the host can resolve, so a deck naming a missing one fails the build. Omit to skip the check |
| `strict` | `boolean` | `false` | Treat validation warnings as errors |

`narroMarkdown` is also the entry point's default export, so
`import narroMarkdown from "@getnarro/markdown/vite"` works too.

The plugin exposes two virtual modules: `virtual:narro/deck` (the compiled deck)
and `virtual:narro/deck.css` (its stylesheet).

`strict` is off by default for the reason given above — an unrecognised deck key
is usually a theme's, not a typo. Turn it on in CI, where a typo should not
merge.

## Layout resolution order

The plugin resolves a `layout:` name in this order, first match winning:

1. `<deckDir>/layouts/<name>.{tsx,jsx,mdx,ts,js}` — yours, shadows everything
2. The [deck template](/docs/deck-templates)'s layouts
3. The active theme's layouts
4. The built-ins

Shadowing is by name, so defining `layouts/two-column.tsx` replaces the built-in
of that name for the whole deck without renaming any slide.

A template layout resolves to its own `base` plus its region wrappers, and that
base is looked up in steps 1 and 4 only — layouts on a master do not chain, so
there is no cycle to detect.

## Deck templates

```ts
import {
  type DeckTemplate,
  applyTemplateToSlide,
  loadTemplateFile,
  resolveDeckTemplate,
  templateSlotMap,
  tokensCss,
  validateTemplate,
} from "@getnarro/markdown";
```

`resolveDeckTemplate(deckPath, frontmatter)` is the whole entry point: it honours
the deck's `template:` key, falls back to discovery beside the deck, and returns
the parsed template with the path it came from. `validateTemplate` reports the
names in it that do not resolve, `templateSlotMap` produces the `layoutSlots`
`validateDeck` takes, and `applyTemplateToSlide` produces what a slide inherits —
classes, background, frontmatter defaults, and the resolved placeholders.

The build calls them in that order; so does `narro check`. See
[deck templates](/docs/deck-templates) for the file itself.