Structured slides and validation

This section is for people comfortable with code. You can make and present slides without it.

Existing decks keep their --- frontmatter syntax. Code fences protect literal --- and :::notes examples. A deck can also start directly with Markdown. Legacy slide metadata is recognized by a known field at the start of the block (such as layout:, section: or image:). For arbitrary metadata, or to remove that ambiguity, use an explicit metadata block at the start of a slide:

Example
---
:::meta
layout: split
id: deployment-options
:::
# Deployment options

:::slot left
## Directory bundle
- Separate media files
:::

:::slot right
## Single HTML file
- Easy to transfer
:::

:::notes
Explain the tradeoff.
:::

:::meta belongs to the slide, while the optional initial deck configuration continues to use YAML between --- lines. Put a blank line before a directive when combining it with ordinary Markdown. Directives can nest: for example, a callout inside columns inside a slot. +++ separates columns only at that column block's top level. Slots, notes and metadata are top-level slide blocks.

Unassigned Markdown is the body region. split accepts left and right regions with the shared body above them; without named regions it retains the legacy first-block-left behavior. An explicit :::slot body is also supported, but cannot be combined with unassigned content. Duplicate or unclosed regions are errors. Layout templates declare the other regions they accept.

Set id: for a durable link such as #deployment-options. Numeric links such as #3 remain supported. Without an authored ID, the parser assigns positional IDs (slide-1, etc.); these are not stable across reordering.

Run mdeck check talk.md to validate YAML, dimensions, IDs, metadata and local asset references. --strict also fails on warnings. Builds reject source errors; unknown layouts warn and retain the generic fallback for compatibility.

Source model for integrations#

parseSlides(source) returns the original source, deckConfig, configSource, slides and diagnostics. Each slide has its resolved meta, authoredMeta (before section inheritance), id, content, regions, directive blocks, and source, metaSource, bodySource ranges. Ranges are half-open UTF-16 offsets into the original string, including its whitespace and line endings.

Regions contain Markdown content. Explicit regions also carry their original raw body and contiguous source range. The implicit body has ranges because notes and slots may interrupt it. This is a source-preserving document model, not a normalized Markdown serializer.

serializeDeck(deck) returns the original source unchanged. Use replaceRegion(deck, slideId, regionName, markdown) for targeted explicit-region edits, or applySourceEdits(source, edits) for general changes. Edits specify start, end, text and optionally expected text; overlaps and stale expected text are rejected. Reparse the returned source after editing. Callers saving to disk must also compare against the current file to detect external changes.

Editing helpers#

src/core/editDeck.js builds on the source model to change a deck without disturbing anything else in the file. Every function takes the parsed deck, never mutates it, and returns the new source (or { source, index } for structural changes); reparse the result before the next edit. Untouched bytes stay identical, including comments, blank lines and the file's line endings.

Function Effect
setRegion(deck, slideId, name, markdown) Replaces an explicit :::slot body, the implicit body (discontiguous ranges collapse into the first one), or appends a new slot
removeRegion(deck, slideId, name) Removes a slot block
setSlideMeta(deck, slideId, patch) Patches top-level keys of the slide's YAML; undefined deletes a key; works for :::meta and the legacy --- form, inserting or removing the block as needed
setSlideNotes(deck, slideId, markdown) Edits the :::notes block, the notes: key, or appends a block
setDeckConfig(deck, patch) Patches the frontmatter, inserting it when absent
insertSlide(deck, index, markdown) Inserts a slide, keeping delimiters and blank lines tidy
removeSlide(deck, slideId) / moveSlide(deck, slideId, toIndex) Structural changes that return the index to select next
replaceSlideSource(deck, slideId, text) Replaces one slide's Markdown verbatim

Metadata patches rewrite only the block of each changed key (patchYamlMapping in source.js), so comments on other keys survive. Comments inside an edited nested block such as props: are lost.

Editor API#

mdeck edit serves a small JSON API from the Vite dev server for the browser editor. It binds to the loopback interface, refuses requests whose Origin differs from its own host, and only ever writes the deck file and folders under the deck's extensions/ directory.

Route Purpose
GET /__mdeck/deck { path, name, source, hash, registry, warnings }
POST /__mdeck/source { source, base } writes when base equals the hash of the file on disk; otherwise 409 { source, hash }
GET /__mdeck/extension/:kind/:id The extension's manifest and text files
PUT /__mdeck/extension/:kind/:id { files: { name: text | null } } writes into extensions/<id>/ after validating the manifest against the files that will exist; built-ins are refused
DELETE /__mdeck/extension/:kind/:id Removes a deck-local extension folder

Two custom websocket events keep the editor in step with the disk: mdeck:deck-changed { hash } when another program saved the deck, and mdeck:extensions-changed { file } when an extension file changed.

The preview iframe runs the normal deck runtime with ?view=deck&editor=1. The editor posts { deckSource: { source, selection, config, overrides } } to it; config merges over the deck settings (for example to preview a palette) and overrides carries unsaved theme and palette manifests. The runtime answers with { deckEditorReady }, { deckRendered: { slideCount, diagnostics, error } } and the usual { deckStateChanged } messages.