UNPKG

@tanstack/markdown

Version:

A tiny, fast, deterministic Markdown renderer for blogs and documentation.

486 lines (340 loc) 13.5 kB
--- name: 'production-pipelines' description: > Audit and ship a production Markdown pipeline with explicit trust boundaries, external syntax highlighting, parse-ahead caching, compatibility checks, deterministic output, and bundle budgets. Load before deploying blogs, docs, or untrusted-content rendering. metadata: type: lifecycle library: '@tanstack/markdown' library_version: '0.0.13' requires: - 'render-markdown' sources: - 'TanStack/markdown:docs/core-concepts/security.md' - 'TanStack/markdown:docs/core-concepts/document-model.md' - 'TanStack/markdown:docs/core-concepts/syntax-profile.md' - 'TanStack/markdown:docs/guides/react.md' - 'TanStack/markdown:docs/guides/syntax-highlighting.md' - 'TanStack/markdown:docs/guides/performance.md' - 'TanStack/markdown:docs/guides/testing.md' - 'TanStack/markdown:docs/comparison.md' - 'TanStack/markdown:src/utils.ts' - 'TanStack/markdown:tests/security.test.tsx' - 'TanStack/markdown:tests/bundle-size.test.ts' --- This skill builds on `render-markdown`. Read it first for the supported syntax, AST, parser options, and renderer contracts. # TanStack Markdown — Production Pipeline Checklist Run every section before deploying a blog, documentation site, or user-content renderer. ## Trust Boundary Checks ### Check: Classify every Markdown source Expected: ```ts import { renderHtml } from '@tanstack/markdown/html' export function renderUntrustedMarkdown( source: string, sanitize: (html: string) => string, ): string { const rendered = renderHtml(source) return sanitize(rendered) } ``` Fail condition: Untrusted input reaches `allowHtml`, an unaudited extension `renderHtml` hook, or an unaudited highlighter. Fix: Separate trusted and untrusted entry points, keep trusted callbacks disabled for untrusted content, and enforce application link, image, and final-HTML policy. ## Highlighting Checks ### Check: Return trusted code contents only Expected: ```ts import { renderHtml } from '@tanstack/markdown/html' import { createHighlighter } from '@tanstack/highlight/core' import { plaintext } from '@tanstack/highlight/languages/plaintext' import { ts } from '@tanstack/highlight/languages/ts' import { createTanStackMarkdownHighlighter } from '@tanstack/highlight/markdown' const highlighter = createHighlighter({ languages: [plaintext, ts] }) export const html = renderHtml('```ts {1}\nconst answer = 42\n```', { highlighter: createTanStackMarkdownHighlighter(highlighter), }) ``` Fail condition: The callback returns a complete `<pre><code>` tree, does not escape source code, or comes from an unreviewed transform. Fix: Return only escaped markup for the renderer-owned `<code>` contents, and run highlighting during ingestion, build, or server rendering. ## Compatibility Checks ### Check: Validate the actual content corpus Expected: ```bash MARKDOWN_CORPUS_DIRS=../site/src/blog:../site/docs pnpm run test:corpus pnpm run corpus:audit:tanstack pnpm run corpus:audit:external ``` Fail condition: Adoption relies only on CommonMark examples or a comparison table instead of the site's Markdown. Fix: Add downstream content directories, preserve practical regressions with focused fixtures, and require renderer and bundle accounting for new syntax. ### Check: Verify deterministic output Expected: ```ts import { renderHtml } from '@tanstack/markdown/html' import { parseMarkdown } from '@tanstack/markdown/parser' const source = '# Deterministic\n\n- one\n- two' const firstDocument = JSON.stringify(parseMarkdown(source)) const secondDocument = JSON.stringify(parseMarkdown(source)) const firstHtml = renderHtml(source) const secondHtml = renderHtml(source) if (firstDocument !== secondDocument || firstHtml !== secondHtml) { throw new Error('Markdown output is nondeterministic') } ``` Fail condition: Identical source and options produce different serialized AST or HTML. Fix: Remove time, randomness, environment state, and unstable ordering from extensions and render callbacks. ## Performance and Cache Checks ### Check: Parse once with final options Expected: ```ts import type { MarkdownDocument } from '@tanstack/markdown' import { renderHtml } from '@tanstack/markdown/html' import { parseMarkdown } from '@tanstack/markdown/parser' const cache = new Map<string, MarkdownDocument>() export function renderCachedArticle(key: string, source: string): string { const cacheKey = `markdown-0.0.13:${key}` let document = cache.get(cacheKey) if (!document) { document = parseMarkdown(source, { frontmatter: true, headingIds: true, }) cache.set(cacheKey, document) } return renderHtml(document) } ``` Fail condition: Stable content is reparsed per request, or parser options/extensions change after the AST is cached. Fix: Build the AST with final parse options, version persisted cache keys, invalidate stored ASTs when node contracts change, and use only narrow entry points. ### Check: Enforce bundle budgets Expected: ```bash pnpm run size pnpm test -- tests/bundle-size.test.ts ``` Fail condition: Any measured entry exceeds its checked gzip budget or starts bundling a highlighter. Fix: Inspect the bundle diff and justify any syntax or dependency cost before adjusting a budget. ## Release Checks ### Check: Run the complete package gate Expected: ```bash pnpm run verify ``` Fail condition: Tests, typechecking, build, docs validation, conformance accounting, sizes, benchmarks, or the npm dry run fail. Fix: Resolve every gate before publishing, including HTML/React/Octane parity; audit raw HTML, highlighter output, HTML hooks, and component replacements separately. ## Common Production Mistakes ### CRITICAL Enabling HTML for untrusted Markdown Wrong: ```ts import { renderHtml } from '@tanstack/markdown/html' export function renderComment(source: string): string { return renderHtml(source, { allowHtml: true }) } ``` Correct: ```ts import { renderHtml } from '@tanstack/markdown/html' export function renderComment(source: string): string { return renderHtml(source) } ``` `allowHtml` emits raw nodes and is not a sanitization step. Source: `docs/core-concepts/security.md` ### HIGH Returning highlighter containers Wrong: ```ts import { renderHtml } from '@tanstack/markdown/html' function escapeCode(code: string): string { return code.replace(/[&<>]/g, (character) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', })[character] ?? character) } console.log(renderHtml('```ts\nconst x = 1\n```', { highlighter: (code) => `<pre><code>${escapeCode(code)}</code></pre>`, })) ``` Correct: ```ts import { renderHtml } from '@tanstack/markdown/html' function escapeCode(code: string): string { return code.replace(/[&<>]/g, (character) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', })[character] ?? character) } console.log(renderHtml('```ts\nconst x = 1\n```', { highlighter: (code) => `<span class="token">${escapeCode(code)}</span>`, })) ``` The renderer owns `<pre><code>`; the callback supplies only the code element's trusted contents. Source: `docs/guides/syntax-highlighting.md` ### CRITICAL Trusting arbitrary highlighter output Wrong: ```ts import { renderHtml } from '@tanstack/markdown/html' const source = '```html\n<img src=x onerror=alert(1)>\n```' console.log(renderHtml(source, { highlighter: (code) => code })) ``` Correct: ```ts import { renderHtml } from '@tanstack/markdown/html' function escapeCode(code: string): string { return code.replace(/[&<>]/g, (character) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', })[character] ?? character) } const source = '```html\n<img src=x onerror=alert(1)>\n```' console.log(renderHtml(source, { highlighter: escapeCode })) ``` Highlighter output is inserted without further escaping in every renderer. Source: `docs/core-concepts/security.md` ### MEDIUM Bundling highlighting into static clients Wrong: ```tsx import { Markdown } from '@tanstack/markdown/react' import { tokenize } from '@tanstack/highlight' export function Article({ source }: { source: string }) { return <Markdown highlighter={(code) => String(tokenize(code))}>{source}</Markdown> } ``` Correct: ```ts import { renderHtml } from '@tanstack/markdown/html' import type { CodeHighlighter } from '@tanstack/markdown' export function renderStaticArticle( source: string, highlighter: CodeHighlighter, ): string { return renderHtml(source, { highlighter }) } ``` Tokenizer runtimes, grammars, and themes can outweigh Markdown parsing and should remain build-time or server-side for static content. Source: `docs/guides/performance.md` ### CRITICAL Treating defaults as a sanitizer Wrong: ```ts import { renderHtml } from '@tanstack/markdown/html' export function renderForEveryPolicy(source: string): string { return renderHtml(source) } ``` Correct: ```ts import { renderHtml } from '@tanstack/markdown/html' export function renderWithPolicy( source: string, sanitize: (html: string) => string, ): string { return sanitize(renderHtml(source)) } ``` Core escaping and protocol filtering do not enforce application-specific outbound-link, image, or final-HTML policy. Source: `docs/core-concepts/security.md` ### HIGH Assuming complete CommonMark behavior Wrong: ```ts import { renderHtml } from '@tanstack/markdown/html' export function renderArbitraryMarkdown(source: string): string { return renderHtml(source) } ``` Correct: ```ts import { renderHtml } from '@tanstack/markdown/html' export function renderControlledDocs(source: string): string { return renderHtml(source) } ``` The package implements a documented docs/blog profile, not complete CommonMark, GFM, MDX, or arbitrary plugin behavior. Source: `docs/core-concepts/syntax-profile.md` ### MEDIUM Reparsing unchanged content Wrong: ```ts import { renderHtml } from '@tanstack/markdown/html' export function renderRequest(source: string): string { return renderHtml(source) } ``` Correct: ```ts import type { MarkdownDocument } from '@tanstack/markdown' import { renderHtml } from '@tanstack/markdown/html' import { parseMarkdown } from '@tanstack/markdown/parser' export function compile(source: string): MarkdownDocument { return parseMarkdown(source) } export function renderRequest(document: MarkdownDocument): string { return renderHtml(document) } ``` String render inputs parse the complete document, while a cached `MarkdownDocument` skips that work. Source: `docs/core-concepts/document-model.md` ### HIGH Injecting HTML into React Wrong: ```tsx import { renderHtml } from '@tanstack/markdown/html' export function Article({ source }: { source: string }) { const html = renderHtml(source) return <article dangerouslySetInnerHTML={{ __html: html }} /> } ``` Correct: ```tsx import { Markdown } from '@tanstack/markdown/react' export function Article({ source }: { source: string }) { return <article><Markdown>{source}</Markdown></article> } ``` The HTML string adds a trusted insertion boundary and bypasses React component replacement. Source: `docs/guides/react.md` ### MEDIUM Expecting fence metadata to highlight Wrong: ```ts import { renderHtml } from '@tanstack/markdown/html' const source = '```ts {1}\nconst answer = 42\n```' console.log(renderHtml(source)) ``` Correct: ```ts import { renderHtml } from '@tanstack/markdown/html' function escapeCode(code: string): string { return code.replace(/[&<>]/g, (character) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', })[character] ?? character) } const source = '```ts {1}\nconst answer = 42\n```' console.log(renderHtml(source, { highlighter: escapeCode })) ``` Fence metadata enters the AST, but token markup requires an external highlighter. Source: `docs/core-concepts/syntax-profile.md` ## Tensions ### Compatibility breadth versus bundle budget Do not maximize conformance by default. Require target-corpus evidence, renderer coverage, and measured bundle cost before adding syntax. ### Rich trusted output versus untrusted-content safety Do not enable raw HTML, extension HTML, or highlighter markup globally to solve presentation needs. Scope each trusted callback to controlled content. ### Parse-ahead performance versus option timing Build cached ASTs with final parser options and document transforms. Keep required HTML render hooks active when rendering those cached documents. ## Pre-Deploy Summary - [ ] Every content source is classified as trusted or untrusted. - [ ] Raw HTML, extension HTML, and highlighter output have explicit owners. - [ ] Application link, image, and final-sanitization policies are enforced. - [ ] The downstream corpus passes deterministic AST and renderer checks. - [ ] Unsupported syntax is documented rather than silently assumed. - [ ] Stable content is parsed once with final options and versioned cache keys. - [ ] Highlighting runs at build time or on the server where possible. - [ ] Narrow entry points and individual extensions are used. - [ ] Bundle budgets and HTML/React/Octane parity tests pass. - [ ] `pnpm run verify` passes before release. ## Related Skills - `render-markdown` for the supported profile, AST, parser options, and core HTML rendering. - `docs-features` for docs metadata and code-fence behavior. - `custom-extensions` for parser hooks and trusted HTML extension boundaries. - `react-rendering` and `octane-rendering` for framework component policy and SSR parity.