Markdown Rendering for SPAs
Technical architecture guide

Markdown should be the authoring format—not the browser runtime.

Build a small, secure, and complete documentation system by compiling Markdown into semantic HTML, retaining only the runtime features your SPA actually needs.

Static-firstPre-render Markdown and syntax highlighting during generation.
AST only when justifiedUse structure for transforms, provenance, linting, and multi-output workflows.
Security is separateParsing, sanitization, URL policy, and DOM insertion are distinct controls.
No section matched that search. Try a parser, security, AST, highlighting, or performance term.
01 · Decision

Recommended architecture

Use one public document contract with two execution modes: generation-time compilation for normal SPA pages and an optional browser compiler for dynamic Markdown.

Primary recommendation

Dynamic Markdown

Use markdown-it behind a stable adapter with a small semantic document model.

  • Strong extension model.
  • Useful token metadata.
  • Lower abstraction cost than a full AST stack.

Advanced compiler

Adopt unified, remark, and rehype when tree processing becomes a platform capability.

  • Linting and rewrites.
  • Multiple output formats.
  • Large plugin ecosystem.
Parsermarkdown-it for runtime rendering. Generation-time compiler for normal documents.
Document modelA thin token-derived projection for outline, links, code blocks, diagrams, diagnostics, search, and provenance.
HighlightingPrism with an explicit language allowlist at runtime. Pre-highlight static pages.
SecurityDisable raw HTML, sanitize final HTML, enforce URL policy, and use Trusted Types where available.
DiagramsLazy-load Mermaid only when a Mermaid fence is present. Keep strict security configuration.
02 · System design

The rendering pipeline

A trustworthy renderer separates parsing, document analysis, HTML generation, sanitization, and progressive enhancement.

Markdown sourceHuman-readable authoring format.
Normalize inputLine endings, size limits, encoding.
Parse blocks + inlineLists, fences, links, emphasis, references.
Tokens or ASTStructured representation with metadata.
Analyze + renderOutline, search, provenance, semantic HTML.
SanitizeHTML allowlist, URL policy, Trusted Types.
Progressive SPATOC, search, copy, theme, lazy Mermaid.
Mermaid source is preserved as an offline fallback.
flowchart LR
  A[Markdown source] --> B[Normalize]
  B --> C[Block parser]
  C --> D[Inline parser]
  D --> E[Tokens or AST]
  E --> F[Analyze document]
  E --> G[Render HTML]
  F --> H[Outline and search]
  F --> I[Links and provenance]
  G --> J[Sanitize]
  J --> K[Trusted DOM insertion]
  K --> L[Progressive enhancements]
!

Parsing is not sanitization

A CommonMark-compliant parser can still generate dangerous HTML when raw HTML, custom plugins, or unsafe URLs are allowed.

03 · Trade-offs

From custom code to content frameworks

The smallest parser is not automatically the smallest system. Conformance, extension behavior, security, and maintenance dominate the real cost.

ApproachStrengthConstraintBest useFit
Custom regexMinimal initial code.Breaks on nesting, references, escaping, and pathological input.Trusted, intentionally constrained snippets.Avoid
Custom state machineTotal control over syntax and metadata.Large conformance and test burden.Deliberately small house dialect.Niche
MarkedFast, low-level, browser friendly.Sanitization is external. Less structural than AST pipelines.Simple Markdown viewers.Good
markdown-itExtensible rule engine, useful tokens, safe defaults.Token stream rather than canonical mdast.Dynamic SPA rendering.Best default
micromarkPrecise low-level tokenizer and extension model.Infrastructure, not a complete application renderer.Custom AST pipelines.Foundation
unified stackRich mdast/hast transformation ecosystem.More modules and plugin-order complexity.Content platforms and multi-output compilers.Advanced
markdown-wasmCompact, fast, CommonMark-oriented conversion.Less flexible for custom semantics and transforms.Read-only conversion.Lean
Markdoc / MDXTyped components and application-native content.New dialect or framework coupling.Dedicated content products.Platform choice
04 · Structure

When an AST is worth the weight

Use an AST when the system needs to understand, transform, validate, or republish the document—not merely render it.

Token model is sufficient when

  • The primary output is HTML.
  • You need headings, links, code blocks, diagrams, and source line ranges.
  • Transforms are local and renderer-oriented.
  • Bundle size and implementation simplicity matter more than plugin breadth.

Full AST is justified when

  • You rewrite or serialize Markdown.
  • You need exact source positions and diagnostics.
  • You produce multiple output formats.
  • You need linting, typed directives, cross-document transforms, or a plugin ecosystem.

Stable public contract

Hide the parser implementation behind one document model. This allows a future move from markdown-it tokens to mdast without rewriting the SPA.

document-model.ts
export interface CompiledDocument {
  html: string | TrustedHTML;
  outline: OutlineItem[];
  codeBlocks: CodeBlockInfo[];
  diagrams: DiagramInfo[];
  links: LinkInfo[];
  textIndex: string;
  diagnostics: Diagnostic[];
  provenance: ProvenanceRecord[];
}
05 · Code rendering

Syntax highlighting choices

Treat highlighting as a separate compiler stage. Prefer explicit language labels and load only the grammars your documents use.

Prism

Small core. Explicit languages. Predictable token classes.

Runtime default

Highlight.js

Broad support and automatic detection, with possible false matches.

Good alternative

Shiki

VS Code-quality grammars and themes. Higher browser cost.

Premium or build-time

Tree-sitter

Structural and incremental parsing. Excessive for display-only code.

Editor features

Static compiler path
const document = compileMarkdown(source, {
  rawHtml: false,
  gfm: true,
  highlight: "build-time",
  languages: ["ts", "json", "bash", "html", "css"]
});

await emitSpaHtml(document);
Runtime language registry
const allowedLanguages = new Set([
  "text", "html", "css", "js", "ts",
  "json", "bash", "yaml", "sql",
  "markdown", "http", "graphql", "hcl"
]);
06 · Product contract

Define “complete” as a dialect

A lightweight renderer stays coherent by documenting what it supports, how extensions are encoded, and what content is prohibited.

Core syntax

  • CommonMark blocks and inline syntax.
  • GFM tables, task lists, strikethrough, and autolinks.
  • Reference links, fenced code, images, and nested lists.

Documentation extensions

  • Stable heading IDs and table of contents.
  • Callouts, footnotes, code titles, and line emphasis.
  • Mermaid fences, provenance records, and collapsible sections.

Excluded by default

  • Raw HTML, scripts, event handlers, arbitrary CSS, and iframes.
  • MDX or content-defined JavaScript execution.
  • Document-controlled security configuration.
Controlled fence metadata
```ts title="policy-engine.ts" lines="3,7-11" copy
export function evaluatePolicy(input: PolicyInput) {
  return policySet.evaluate(input);
}
```
07 · Trust boundary

Layered security model

No single library provides the whole control plane. Each stage constrains a different class of risk.

1

Parser configuration

Disable raw HTML and cap document, fence, and nesting sizes.
Prevent
2

Renderer policy

Generate only semantic elements and formally parsed extension metadata.
Constrain
3

HTML sanitizer

Apply a conservative allowlist after all transforms and plugins have run.
Clean
4

URL policy

Allow relative URLs, fragments, HTTPS, and approved schemes. Reject scriptable protocols.
Validate
5

DOM insertion

Use Trusted Types and restrictive Content Security Policy where supported.
Enforce
6

Special renderers

Keep Mermaid strict. Do not let content override runtime security settings.
Isolate
Trusted DOM insertion
const policy = window.trustedTypes?.createPolicy("spa-markdown", {
  createHTML: value => DOMPurify.sanitize(value)
});

container.innerHTML = policy
  ? policy.createHTML(renderedHtml)
  : DOMPurify.sanitize(renderedHtml);
08 · Runtime discipline

Performance by execution mode

The strongest optimization is architectural: do not ship parsers, highlighters, sanitizers, or diagram engines to pages that do not need them.

09 · Delivery

Implementation path

Start with a narrow contract and strong tests. Add AST infrastructure only after concrete content-processing requirements appear.

1

Dialect and security contract

Define supported CommonMark and GFM behavior, extension syntax, URL rules, limits, and prohibited constructs.

1–2 days
2

Compiler adapter and document model

Wrap markdown-it. Produce semantic HTML plus outline, links, code blocks, diagrams, search text, diagnostics, and provenance.

4–8 days
3

Highlighting and SPA enhancements

Add build-time Prism, runtime copy controls, table wrappers, heading anchors, search, TOC, and lazy Mermaid.

4–7 days
4

Security and conformance

Add DOMPurify, URL policy, Trusted Types, CommonMark/GFM fixtures, fuzz cases, accessibility checks, and performance budgets.

4–7 days

Expected initial production effort

Approximately three to five engineer weeks for a complete, tested, and secure implementation. A focused internal v1 can be delivered in roughly seven to twelve engineering days.

10 · Codebase shape

Proposed module structure

Keep parser-specific concerns behind an adapter. Keep security, analysis, rendering, and progressive enhancement independently testable.

markdown-runtime/
markdown-runtime/
├── compiler/
│   ├── compile.ts
│   ├── normalize.ts
│   ├── parser.ts
│   ├── renderer.ts
│   └── document-model.ts
├── analysis/
│   ├── outline.ts
│   ├── links.ts
│   ├── code-blocks.ts
│   ├── search-index.ts
│   ├── diagnostics.ts
│   └── provenance.ts
├── security/
│   ├── sanitize.ts
│   ├── url-policy.ts
│   ├── trusted-types.ts
│   └── limits.ts
├── enhancements/
│   ├── code-highlight.ts
│   ├── code-controls.ts
│   ├── mermaid.ts
│   ├── footnotes.ts
│   └── callouts.ts
├── static/
│   ├── prehighlight.ts
│   ├── emit-html.ts
│   └── emit-manifest.ts
└── tests/
    ├── commonmark/
    ├── gfm/
    ├── security/
    ├── snapshots/
    ├── accessibility/
    └── performance/
11 · Provenance

Primary references

The guide prioritizes standards and official project documentation. Links are grouped by the claim families they support.

CommonMark and GitHub Flavored MarkdownGrammar behavior, block/inline parsing, GFM extensions.
Open standard
markdown-itToken model, renderer rules, browser support, and extension design.
Open docs
Unified, remark, rehype, and mdastAST processing, transformation pipelines, and positional metadata.
Open docs
Prism, Highlight.js, Shiki, and Tree-sitterHighlighting trade-offs, language grammars, browser costs, and structural parsing.
Open Prism
DOMPurify and Trusted TypesHTML sanitization and safer DOM insertion boundaries.
Open DOMPurify
Mermaid security configurationStrict security mode and controlled diagram rendering.
Open docs