Extensions are small synchronous hooks for profile-specific syntax. They are intentionally narrower than a general content-processing pipeline.
import type { MarkdownExtension } from '@tanstack/markdown'
const extension: MarkdownExtension = {
name: 'example',
parseBlock(context) {
return undefined
},
transformInline(nodes) {
return nodes
},
transformDocument(document) {
return document
},
renderHtml(node, context) {
return undefined
},
}parseBlock runs before built-in block parsing at the current source line. Return undefined when the extension does not own that line. When returning a node, call context.consume(count) with the number of source lines consumed.
The context includes:
Nested parsing shares the parser depth budget and heading slugger.
transformInline receives built-in inline nodes after parsing. Return the replacement array. Keep transforms deterministic and avoid repeated full-array scans for every node.
transformDocument runs after all blocks and footnotes are built. Return a new MarkdownDocument, mutate and return nothing, or leave the document unchanged. The built-in heading collector uses this phase.
renderHtml runs before built-in HTML node rendering. Return a string to claim the node or undefined to continue with the standard renderer. The context can render nested block and inline nodes.
This hook is HTML-specific. A custom node intended for both outputs should use a ComponentNode and map its tag through React components, or maintain an explicit React rendering layer. Returned HTML is trusted and is not sanitized.
Extensions run in array order. Put more specific parsers before more general parsers, and use stable extension arrays for parsing and source-string rendering:
const extensions = [callouts, customComponents]
const document = parseMarkdown(source, { extensions })
const html = renderHtml(document, { extensions })Document transforms are already represented in a pre-parsed AST. HTML render hooks still need to be present when that AST is rendered.
An extension is appropriate when syntax is broadly useful to docs, has a deterministic block boundary, and does not justify cost in the core entry. Use a larger processing ecosystem when the job requires async plugins, arbitrary tree pipelines, compiler integration, or MDX evaluation.