π§±βMarkdown API
How do I read, edit, or validate a deck file from my own code instead of by hand?
@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:
@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
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.
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:
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:
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.
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
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
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
`title`, `author`, `date`, `theme`, `template`, `aspectRatio`, `transition`, `class`, `keyboard`, `mouse`, `touch`, `routing`, `favicon`, `maxDuration`for a deck and
`layout`, `class`, `id`, `transition`, `background`, `notes`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
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 page.
Runtime
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 for what {.step} compiles to.
The Vite plugin
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, 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:
<deckDir>/layouts/<name>.{tsx,jsx,mdx,ts,js}β yours, shadows everything- The deck templateβs layouts
- The active themeβs layouts
- 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
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 for the file itself.