parseMarkdown returns a MarkdownDocument: a plain, serializable object with block nodes under children.
import { parseMarkdown } from '@tanstack/markdown/parser'
const document = parseMarkdown('# Hello **world**')
// {
// type: 'root',
// children: [
// {
// type: 'heading',
// depth: 1,
// id: 'hello-world',
// children: [
// { type: 'text', value: 'Hello ' },
// { type: 'strong', children: [{ type: 'text', value: 'world' }] },
// ],
// },
// ],
// }Block nodes describe document structure: headings, paragraphs, code, lists, blockquotes, tables, footnotes, thematic breaks, HTML, callouts, and extension components.
Inline nodes describe text flow: text, inline code, emphasis, strong text, strike, links, images, breaks, footnote references, and inline HTML.
Every node has a discriminating type, so TypeScript narrows the union naturally:
for (const block of document.children) {
if (block.type === 'heading') {
console.log(block.depth, block.id)
}
}The complete node contracts are in the Types Reference.
Frontmatter is stored as the original string in document.frontmatter. TanStack Markdown does not choose a YAML parser or schema for the application.
The heading collection extension adds document.headings, a compact array of IDs, text, levels, and optional framework labels.
Both renderers accept a source string or a MarkdownDocument:
import { parseMarkdown } from '@tanstack/markdown/parser'
import { renderHtml } from '@tanstack/markdown/html'
const document = parseMarkdown(source)
await cache.set(key, JSON.stringify(document))
const html = renderHtml(document)Parsing once is useful for build pipelines, content indexes, multiple render targets, and high-traffic SSR paths. Rendering a pre-parsed AST is also the fastest measured path in the package.
The AST is public and typed, but the package is still pre-1.0. Pin versions when persisting documents across deployments, and regenerate cached AST data when upgrading across a release that changes node contracts.