Markdoc
Markdoc: Stripe's Markdown-based authoring framework for structured documentation, custom tags, validation, and renderers
You are an expert in using Markdoc for structured document generation and documentation authoring.
## Key Points
- Define strict schemas for all custom tags with `matches` constraints on attributes so that validation catches typos and invalid values at build time rather than producing broken output silently.
- Use Markdoc variables for product names, version numbers, and feature flags rather than hardcoding them — this keeps documentation accurate across releases by changing a single config value.
## Quick Example
```
{% /tabItem %}
{% tabItem label="yarn" %}
```skilldb get document-generation-services-skills/MarkdocFull skill: 298 linesMarkdoc — Document Generation
You are an expert in using Markdoc for structured document generation and documentation authoring.
Core Philosophy
Overview
Markdoc is an open-source documentation authoring framework created by Stripe. It extends Markdown with a custom tag syntax, variable interpolation, conditional content, and a validation system that catches errors at build time. Unlike MDX which gives authors full JavaScript power, Markdoc constrains what authors can do through a declarative schema — making it safer for multi-author documentation sites where you want to enforce consistency without risking arbitrary code execution. Markdoc can render to React, HTML strings, or any custom target.
Setup & Configuration
npm install @markdoc/markdoc
# For React rendering
npm install @markdoc/markdoc react react-dom
# For Next.js integration
npm install @markdoc/next.js
// markdoc.config.ts — schema definition
import { Config, Schema, Tag } from "@markdoc/markdoc";
// Define custom tags that authors can use in Markdown
const callout: Schema = {
render: "Callout",
description: "Display a callout box with a type-based style",
attributes: {
type: {
type: String,
default: "info",
matches: ["info", "warning", "error", "success"],
description: "Controls the visual style of the callout",
},
title: {
type: String,
required: false,
description: "Optional title displayed at the top",
},
},
children: ["paragraph", "list", "code"],
};
const tabs: Schema = {
render: "Tabs",
attributes: {},
children: ["tag"], // Only allows nested tags (TabItem)
};
const tabItem: Schema = {
render: "TabItem",
attributes: {
label: { type: String, required: true },
},
children: ["paragraph", "list", "code", "fence", "tag"],
};
// Define custom nodes to override default Markdown behavior
const heading: Schema = {
render: "Heading",
attributes: {
level: { type: Number, required: true },
id: { type: String },
},
transform(node, config) {
const children = node.transformChildren(config);
const text = children
.filter((c): c is string => typeof c === "string")
.join(" ");
const id = text.toLowerCase().replace(/\s+/g, "-").replace(/[^\w-]/g, "");
return new Tag("Heading", { level: node.attributes.level, id }, children);
},
};
// Variables available to all documents
const variables: Record<string, unknown> = {
product: { name: "MyApp", version: "3.2.0" },
flags: { betaFeatures: true },
};
// Functions available in Markdoc expressions
const functions: Config["functions"] = {
upper: {
transform(parameters: Record<string, unknown>) {
const value = parameters[0];
return typeof value === "string" ? value.toUpperCase() : value;
},
},
};
export const markdocConfig: Config = {
tags: { callout, tabs, tabItem },
nodes: { heading },
variables,
functions,
};
Core Patterns
Parsing, validating, and rendering documents
import Markdoc, { RenderableTreeNode } from "@markdoc/markdoc";
import { markdocConfig } from "./markdoc.config";
interface ProcessedDocument {
content: RenderableTreeNode;
errors: string[];
frontmatter: Record<string, unknown>;
}
function processDocument(source: string): ProcessedDocument {
// 1. Parse: Markdown string -> AST
const ast = Markdoc.parse(source);
// 2. Extract frontmatter (YAML between --- fences)
const frontmatter = ast.attributes.frontmatter
? (Markdoc.parse(ast.attributes.frontmatter) as any) // Use a YAML parser in production
: {};
// 3. Validate: check the AST against the schema
const errors = Markdoc.validate(ast, markdocConfig);
const errorMessages = errors.map(
(e) => `Line ${e.lines?.[0] ?? "?"}: ${e.error.message}`
);
if (errors.some((e) => e.error.level === "critical")) {
return { content: "", errors: errorMessages, frontmatter };
}
// 4. Transform: AST -> renderable tree
const content = Markdoc.transform(ast, markdocConfig);
return { content, errors: errorMessages, frontmatter };
}
// Render to HTML string (for server-side or static generation)
function renderToHtml(source: string): string {
const { content, errors } = processDocument(source);
if (errors.length > 0) {
console.warn("Markdoc validation warnings:", errors);
}
return Markdoc.renderers.html(content);
}
React rendering with custom components
// components/Callout.tsx
import React from "react";
interface CalloutProps {
type: "info" | "warning" | "error" | "success";
title?: string;
children: React.ReactNode;
}
const styles: Record<string, { bg: string; border: string; icon: string }> = {
info: { bg: "#eff6ff", border: "#3b82f6", icon: "i" },
warning: { bg: "#fffbeb", border: "#f59e0b", icon: "!" },
error: { bg: "#fef2f2", border: "#ef4444", icon: "x" },
success: { bg: "#f0fdf4", border: "#22c55e", icon: "+" },
};
export function Callout({ type, title, children }: CalloutProps) {
const s = styles[type];
return (
<div style={{ background: s.bg, borderLeft: `4px solid ${s.border}`, padding: "12px 16px", margin: "16px 0", borderRadius: 4 }}>
{title && <strong style={{ display: "block", marginBottom: 4 }}>{title}</strong>}
{children}
</div>
);
}
// render.tsx — React renderer
import React from "react";
import Markdoc from "@markdoc/markdoc";
import { Callout } from "./components/Callout";
import { Tabs, TabItem } from "./components/Tabs";
import { Heading } from "./components/Heading";
const components = { Callout, Tabs, TabItem, Heading };
interface DocPageProps {
source: string;
}
export function DocPage({ source }: DocPageProps) {
const { content, errors } = processDocument(source);
return (
<article>
{errors.length > 0 && (
<pre style={{ color: "red" }}>{errors.join("\n")}</pre>
)}
{Markdoc.renderers.react(content, React, { components })}
</article>
);
}
Markdoc content with custom tags and variables
---
title: Getting Started
---
# Welcome to {% $product.name %}
Current version: **{% $product.version %}**
{% callout type="info" title="Prerequisites" %}
Make sure you have Node.js 18+ installed before proceeding.
{% /callout %}
{% tabs %}
{% tabItem label="npm" %}
```bash
npm install myapp
{% /tabItem %} {% tabItem label="yarn" %}
yarn add myapp
{% /tabItem %} {% /tabs %}
{% if $flags.betaFeatures %}
Beta Features
These features are currently in preview. {% /if %}
### Next.js integration
```typescript
// next.config.mjs
import withMarkdoc from "@markdoc/next.js";
export default withMarkdoc({ schemaPath: "./markdoc" })({
pageExtensions: ["md", "mdoc", "tsx", "ts"],
});
# File structure for Next.js + Markdoc
pages/
docs/
getting-started.md # Markdoc files become pages
configuration.md
markdoc/
tags/ # Tag schemas auto-discovered
callout.markdoc.ts
tabs.markdoc.ts
nodes/ # Node overrides auto-discovered
heading.markdoc.ts
functions/ # Custom functions
upper.markdoc.ts
Best Practices
- Define strict schemas for all custom tags with
matchesconstraints on attributes so that validation catches typos and invalid values at build time rather than producing broken output silently. - Use Markdoc variables for product names, version numbers, and feature flags rather than hardcoding them — this keeps documentation accurate across releases by changing a single config value.
- Prefer Markdoc over MDX when multiple non-developer authors contribute documentation, because the constrained tag syntax prevents introducing arbitrary JavaScript that could break the build or create security issues.
Common Pitfalls
- Forgetting the closing tag syntax (
{% /callout %}) — Markdoc tags are not self-closing by default, and an unclosed tag will swallow all subsequent content into the tag's children without a clear error. - Not running
Markdoc.validate()beforetransform()— skipping validation means malformed documents render with missing content or wrong structure instead of surfacing actionable error messages during the build.
Anti-Patterns
Using the service without understanding its pricing model. Cloud services bill differently — per request, per GB, per seat. Deploying without modeling expected costs leads to surprise invoices.
Hardcoding configuration instead of using environment variables. API keys, endpoints, and feature flags change between environments. Hardcoded values break deployments and leak secrets.
Ignoring the service's rate limits and quotas. Every external API has throughput limits. Failing to implement backoff, queuing, or caching results in dropped requests under load.
Treating the service as always available. External services go down. Without circuit breakers, fallbacks, or graceful degradation, a third-party outage becomes your outage.
Coupling your architecture to a single provider's API. Building directly against provider-specific interfaces makes migration painful. Wrap external services in thin adapter layers.
Install this skill directly: skilldb add document-generation-services-skills
Related Skills
Docraptor
"DocRaptor: HTML-to-PDF API, Prince XML engine, CSS print styles, headers/footers, page breaks, async documents"
Docusaurus
Docusaurus: React-based static site generator for documentation sites, versioned docs, MDX support, search integration, i18n
Jspdf
jsPDF: client-side and server-side PDF generation in JavaScript, tables, images, custom fonts, autotable plugin
Latex Node
LaTeX with Node.js: compile LaTeX documents programmatically, template-based PDF generation, mathematical typesetting, academic papers
PDF Lib
"pdf-lib: create and modify PDFs in JavaScript, form filling, page manipulation, embedding images/fonts, digital signatures"
Puppeteer
"Puppeteer: headless Chrome, PDF generation from HTML, screenshots, web scraping, page automation, Chromium control"