πŸ§±β€‚Markdown API

How do I read, edit, or validate a deck file from my own code instead of by hand?

10 min readView as markdown

@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 fromUse it forRuns in
@getnarro/markdownParsing, editing, validating, compilingNode β€” no React, no DOM
@getnarro/markdown/viteThe build pluginNode, at build time
@getnarro/markdown/runtimeRendering a compiled deckThe browser
@getnarro/markdown/layoutsThe built-in layout components and registryThe 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:

TypeImport 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));
FunctionReturns
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:

MistakeWhat happened without validation
layout: sidebar-left β€” no such layoutThe plugin emitted a throw into the generated module, so the build succeeded and the deck failed in the browser
::rihgt:: β€” a mistyped slotContent attached to a prop no layout reads, and never rendered
aspectRation: "16:9" β€” a mistyped deck keyPreserved 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:

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";
OptionTypeDefaultFor
deckPathstringrequiredAbsolute path to the deck .md
themeResolvedThemeβ€”The resolved theme, whose layouts shadow the built-ins
themeResolver(name: string) => ResolvedTheme | undefinedβ€”How to resolve the deck’s own theme: key
templatePathstringβ€”The deck template, overriding both the deck’s template: key and the file found beside it
devbooleantrueDevelopment mode
cssResolveDirstringthe deck’s directoryWhere @import "tailwindcss" resolves from β€” set it when the deck directory has no node_modules
availableThemesreadonly string[]β€”Theme names the host can resolve, so a deck naming a missing one fails the build. Omit to skip the check
strictbooleanfalseTreat 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’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

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.

IntroductionWhat is Narro, and should I be writing markdown or React?
InstallationWhat do I install, and what does the project look like afterwards?
First DeckWhat does a working React deck look like, end to end?
Markdown ModeHow do I split a slide, style one word, or reveal a line β€” without leaving the markdown file?
Writing AI PromptsWhat do I tell an AI so the deck it writes actually builds?
Rules for AI AgentsWhat do I paste into my repo so an agent stops writing decks that build wrong?
React APIWhich component or hook do I import, and what does it take?
CLIWhich command do I run, and what are its flags?
Markdown APIHow do I read, edit, or validate a deck file from my own code instead of by hand?
Component ReferenceWhat props does this component take, and which package do I import it from?
AnimationHow do I reveal a list one line at a time, or move between slides with something other than a cut?
NavigationHow does the audience move through the deck, and how do I present it?
Canvas & PositioningHow do I put something at an exact position instead of in the flow?
Images & MediaHow do I use an image as a background, tint it, or embed a video?
ArchitectureWhich package owns what, and why is the seam where it is?
Transform ModeHow do I zoom and pan across one big canvas instead of cutting between slides?
Import & ExportHow do I get this deck out as PPTX, PDF, or one file I can email?
AI IntegrationHow do I wire an AI assistant up to Narro so it can write and build decks?
ThemingHow do I change the colours, fonts, and overall look of a deck?
Deck TemplatesHow do I define one house style with named layouts my slides can reference, like a PowerPoint master?
TroubleshootingSomething is wrong with my deck. What is it, and how do I fix it?
LimitationsWhat can't Narro do, and what do I do instead?