parseMarkdown returns a MarkdownDocument: a plain, serializable object with block nodes under children.
import { parseMarkdown } from '@tanstack/markdown/parser'
export default function render(output: HTMLElement) {
output.innerHTML = `
<style>
.ast-explorer { display: grid; gap: 12px; }
.ast-explorer textarea,
.ast-explorer pre {
box-sizing: border-box;
width: 100%;
margin: 0;
border: 1px solid color-mix(in srgb, currentColor 22%, transparent);
border-radius: 8px;
background: var(--notebook-background);
color: var(--notebook-foreground);
font: 13px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace;
}
.ast-explorer textarea { min-height: 90px; padding: 12px; resize: vertical; }
.ast-explorer pre { max-height: 300px; overflow: auto; padding: 12px; }
</style>
<main class="ast-explorer">
<textarea aria-label="Markdown source"># Hello **world**</textarea>
<pre aria-label="Parsed Markdown document"></pre>
</main>
`
const source = output.querySelector<HTMLTextAreaElement>('textarea')
const ast = output.querySelector<HTMLElement>('pre')
if (!source || !ast) throw new Error('AST explorer controls not found')
function update() {
ast.textContent = JSON.stringify(parseMarkdown(source.value), null, 2)
}
source.addEventListener('input', update)
update()
}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.
All 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.