A document engine for technical publishing

TanStackMarkdown

alpha

Markdown with an exit strategy.

Parse documents or accumulated AI output into a plain, serializable tree. Inspect it, cache it, index it, or render it as HTML, React, or Octane—without carrying a content framework or incremental parser state through your app.

3.0 ThousandTotal Downloads845Weekly Downloads3GitHub Stars
Read the syntax profile
4.9 KB parser0 runtime dependencies1 public AST
article.md3 blocks / live
---
title: Ship the docs
---

# One source

Parse it **once**. Render it where the product needs it.

- cache the tree
- index the text
- choose a renderer
root · 3 blocks
frontmatter · string
heading · depth: 1
paragraph · 3 inline nodes
list · 3 items
The durable layer

The AST is the product.

Parsing does not trap your content inside a renderer. Edit the source and inspect the real serializable tree, deterministic HTML, or React output below.

1 parse
every output
plain JSON
cache and index
0 deps
runtime core
Parsing locally
article.mdeditable source
One document, three views
{
  "type": "root",
  "frontmatter": "title: Durable content",
  "children": [
    {
      "type": "heading",
      "depth": 1,
      "id": "one-source-many-destinations",
      "children": [
        {
          "type": "text",
          "value": "One source, many destinations"
        }
      ]
    },
    {
      "type": "paragraph",
      "children": [
        {
          "type": "text",
          "value": "Parse the document "
        },
        {
          "type": "strong",
          "children": [
            {
              "type": "text",
              "value": "once"
            }
          ]
        },
        {
          "type": "text",
          "value": ", then keep the tree."
        }
      ]
    },
    {
      "type": "list",
      "ordered": false,
      "items": [
        {
          "type": "listItem",
          "children": [
            {
              "type": "paragraph",
              "children": [
                {
                  "type": "text",
                  "value": "cache it at the edge"
                }
              ]
            }
          ]
        },
        {
          "type": "listItem",
          "children": [
            {
              "type": "paragraph",
              "children": [
                {
                  "type": "text",
                  "value": "index its text"
                }
              ]
            }
          ]
        },
        {
          "type": "listItem",
          "children": [
            {
              "type": "paragraph",
              "children": [
                {
                  "type": "text",
                  "value": "render it with React or HTML"
                }
              ]
            }
          ]
        }
      ]
    },
    {
      "type": "blockquote",
      "children": [
        {
          "type": "paragraph",
          "children": [
            {
              "type": "text",
              "value": "The renderer can change. The document does not."
            }
          ]
        }
      ]
    }
  ]
}
21 typed nodes236 source charactersJSON serializable
Accumulated AI responses

Stream the text. Keep the parser stateless.

Append each chunk and pass the complete string back through TanStack Markdown. The optional streaming profile reparses synchronously, so there is no incremental state to coordinate, recover, or discard.

+0.2 KBadded to the React path
Every prefixdeterministic React / HTML parity
Same defaultsHTML escaped, executable URLs removed
import { streamingMarkdownExtension } from '@tanstack/markdown/extensions/streaming'

const extensions = [streamingMarkdownExtension()]

<Markdown extensions={extensions} frontmatter={false} headingIds={false}>
  {responseSoFar}
</Markdown>
Complete
Accumulated source
# Streaming response

The model can send **ordinary Markdown** as it thinks.

- completed blocks stay stable
- unfinished markers stay out of the way
- React and HTML stay in sync

```ts
const text = responseSoFar
return render(text)
```

[Unsafe links stay text-only](javascript:alert("nope"))
React output

Streaming response

The model can send ordinary Markdown as it thinks.

  • completed blocks stay stable
  • unfinished markers stay out of the way
  • React and HTML stay in sync
const text = responseSoFar
return render(text)

Unsafe links stay text-only

Complete input, reparsed294 / 294 chars
A deliberate profile

It does less Markdown on purpose.

Technical docs need a known vocabulary, not an open-ended compiler platform. New syntax has to justify its bytes, its ambiguity, and its long-term maintenance cost.

287 / 652

Current CommonMark example accounting. Full conformance is explicitly not the goal; the supported docs profile is.

Inside the profile
  • Headings and emphasis
  • Lists and task lists
  • Tables and footnotes
  • Fenced code with metadata
  • Links, images, and references
  • Frontmatter
Outside the contract
  • Every CommonMark edge case
  • Arbitrary async plugin chains
  • MDX or JSX evaluation
  • A bundled highlighter
  • A general HTML sanitizer
Boundary behavior

Unsafe surprises are opt-in.

Raw HTML starts escaped, executable URL schemes are stripped, and text, attributes, and code are encoded at render time. The parser is also tested against malformed and adversarial input as part of its normal release gates.

Untrusted input
<script>alert("not today")</script>

[Run code](javascript:alert("nope"))

**Trusted Markdown still renders.**
Deterministic HTML
<p>&lt;script&gt;alert(&quot;not today&quot;)&lt;/script&gt;</p>
<p>Run code</p>
<p><strong>Trusted Markdown still renders.</strong></p>
HTML escapedExecutable URL removedMarkdown preserved
Size ledger

A parser should not outweigh the page.

Split entry points keep the parser, renderers, framework adapters, and docs extensions independent. Import the layer the page needs.

Gzip sizes from the project benchmark report. Package scopes and feature sets are not equivalent.

TanStack parser4.9 KB
marked12.5 KB
unified stack36.8 KB
markdown-it52.7 KB
6.7 KB
HTML renderer
6.7 KB
React adapter
6.7 KB
Octane adapter
2.4 KB
docs preset
Content, then color

Syntax highlighting stays outside the parser.

Code fences carry language and metadata. An explicit highlighter can render them later, so the core never silently imports a grammar engine. The same boundary works with your own highlighter or TanStack Highlight.

callouts · 0.4 KBdocs preset · 2.4 KBheading collection
Companion, not dependency
TanStackHighlight

Tiny, synchronous highlighting for the code fences your document model already understands.

Explore Highlight
Open source, in public

A small document contract deserves careful stewardship.

Syntax fixtures, renderer parity, deterministic corpus checks, size budgets, and resilience limits ship with the library—not as an afterthought.

Partners

Gold
Lovable
CodeRabbit
Cloudflare
Railway
Netlify
Silver
OpenRouter
AG Grid
WorkOS
SerpApi
Clerk
Bronze
Electric
Prisma
Sentry
Unkey
Markdown You?
We're looking for TanStack Markdown Partners to join our mission! Partner with us to push the boundaries of TanStack Markdown and build amazing things together.
Let's chat

GitHub Sponsors

Keep the source. Own the model.

Build your document pipeline on plain data.

Start with the parser