The docs preset composes the built-in callout, comment-component, component-transform, and heading-collection extensions.
import { parseMarkdown } from '@tanstack/markdown/parser'
import { renderHtml } from '@tanstack/markdown/html'
import { docsMarkdownExtensions } from '@tanstack/markdown/extensions/docs'
const extensions = docsMarkdownExtensions()
const document = parseMarkdown(source, { extensions })
const html = renderHtml(document, {
extensions,
headingAnchors: true,
})The preset is a separate 2.3 KB gzip entry and is not imported by the parser or renderers.
> [!TIP] Cache the parsed document
> Rendering an AST avoids parsing the same article again.This produces a CalloutNode and renders markdown-alert, markdown-alert-title, and markdown-alert-content classes.
Keep blank lines inside a callout quoted with >. An unquoted blank line ends the callout. Custom titles are plain text; put Markdown links in the body.
Heading collection adds table-of-contents data to document.headings:
const document = parseMarkdown('# Guide\n\n## Install', {
extensions: docsMarkdownExtensions(),
})
document.headings
// [
// { id: 'guide', text: 'Guide', level: 1 },
// { id: 'install', text: 'Install', level: 2 },
// ]Disable collection with docsMarkdownExtensions({ collectHeadings: false }), or configure component names that should not contribute headings.
Single components use one HTML comment:
<!-- ::separator tone="subtle" -->Block components use matching start and end comments:
<!-- ::start:tabs -->
## React
React content
## Solid
Solid content
<!-- ::end:tabs -->Names are normalized to lowercase. Quoted, unquoted, and boolean-style attributes are supported.
The preset transforms these tabs variants:
| Variant | Input model | Output metadata |
|---|---|---|
| default | Sections divided by the shallowest heading | tab names, slugs, and md-tab-panel children |
| files | Fenced code blocks with file= or title= | file metadata and one panel per file |
| package-manager | Shared package... lines or framework: package... lines | package groups and install mode |
| bundler | Vite and Rsbuild heading sections | available bundlers and panel content |
The transforms emit custom element names and JSON data-* properties. Your application owns the components and behavior attached to that contract.
Package-manager lines without a framework prefix apply to every framework. Each line is a separate command, so this block supplies three commands to the application's package-manager component:
<!-- ::start:tabs variant="package-manager" mode="local-install" -->
@tanstack/intent@latest list
@tanstack/intent@latest validate
@tanstack/intent@latest review
<!-- ::end:tabs -->A line is a framework line when it starts with a name made of letters, digits, _, or - followed by a colon, so both react:package and react: package select the react group. A colon that starts a URL or path does not begin a framework prefix: https://example.com/package.tgz, file:../local-package, and git+ssh:// specifiers are shared commands. A framework line can still install a protocol package, as in react: file:../local-package.
The data-package-manager-meta JSON keeps shared commands under the empty string key in packagesByFramework. This fallback group comes first. Named framework groups include shared lines in source order, so renderers can select a named group or fall back to the shared group. Framework discovery should ignore the empty key.
Use a fenced text block inside the component when command arguments contain literal Markdown characters such as <package>#<skill> or *.
framework blocks split top-level framework headings into md-framework-panel elements. Nested headings receive a framework label, while top-level selector headings are omitted from collected table-of-contents data.
Use the individual extension entry points when the complete preset is more than your site needs.