Markdoc Guide: Stripe's Documentation Framework for Developers
Markdoc is an open-source documentation framework created by Stripe to power their developer documentation. It extends standard Markdown with a custom tag system that lets you embed validated, typed components directly in your content — without reaching for MDX or managing a custom parser. The result is documentation that reads like Markdown but behaves like a proper component system.
Why Markdoc Over Standard Markdown
Markdown is excellent for simple documentation, but it breaks down when you need callouts, code groups, interactive elements, or any content that requires rendering logic. The usual solution is MDX — embedding JSX directly in Markdown files. MDX works, but it blurs the line between content and code in ways that make documents harder to edit and impossible to validate statically.
Markdoc takes a different approach. Custom tags are explicit, typed, and validated against a schema you define. A content editor can add a {% callout type="warning" %} tag without touching JavaScript. The rendering engine validates that type is one of the allowed values. The TypeScript schemas give you a single source of truth for what's valid in your documentation.
This separation between content authoring and component rendering is where Markdoc genuinely earns its place.
Getting Started with Markdoc and Next.js
The most practical setup for a Markdoc documentation site is Next.js with the official Markdoc Next.js plugin.
npx create-next-app@latest my-docs --typescript
cd my-docs
npm install @markdoc/markdoc @markdoc/next.js
Update next.config.js to enable the Markdoc plugin:
const withMarkdoc = require('@markdoc/next.js');
module.exports = withMarkdoc()({
pageExtensions: ['md', 'mdoc', 'js', 'jsx', 'ts', 'tsx'],
});
Create a markdoc/ directory at the project root for your tag schemas, and a pages/docs/ directory for your documentation pages. Any .md file in pages/ is now handled by the Markdoc renderer.
Writing Markdoc Documents
A Markdoc document looks like standard Markdown with optional frontmatter and custom tags:
---
title: Getting Started
description: Install and configure the SDK
---
Install the package using npm:
```bash
npm install my-sdk
{% callout type="info" %} Make sure you're running Node.js 18 or later before proceeding. {% /callout %} ````
The {% callout %} and {% /callout %} syntax is Markdoc's tag format. Tags are self-closing or block-level, they accept typed attributes, and they map to React components you define.
Defining Tag Schemas in TypeScript
Create markdoc/tags/callout.markdoc.ts:
import { Tag } from '@markdoc/markdoc';
import type { Schema } from '@markdoc/markdoc';
export const callout: Schema = {
render: 'Callout',
attributes: {
type: {
type: String,
default: 'info',
matches: ['info', 'warning', 'error', 'success'],
errorLevel: 'error',
},
title: {
type: String,
},
},
};
The matches array means Markdoc will throw a validation error if someone writes type="danger" — a type that isn't defined. This is the static validation that makes Markdoc different from MDX, where a typo in props silently renders nothing.
Create the corresponding React component in components/Callout.tsx:
type CalloutProps = {
type: 'info' | 'warning' | 'error' | 'success';
title?: string;
children: React.ReactNode;
};
export function Callout({ type, title, children }: CalloutProps) {
return (
<div className={`callout callout-${type}`}>
{title && <strong>{title}</strong>}
{children}
</div>
);
}
Register both in your Markdoc config and pass it to the renderer. The official documentation at markdoc.dev covers the full schema API, including nodes, functions, and variables.
Validating Your Content
One of Markdoc's underused features is programmatic validation. Before building or publishing, you can run:
import Markdoc from '@markdoc/markdoc';
import { callout } from './markdoc/tags/callout.markdoc';
const source = await fs.readFile('content/guide.md', 'utf-8');
const ast = Markdoc.parse(source);
const errors = Markdoc.validate(ast, { tags: { callout } });
if (errors.length > 0) {
errors.forEach(error => console.error(error));
process.exit(1);
}
This makes broken documentation a build-time error, not a runtime surprise. At Stripe's scale, this kind of validation across hundreds of pages is what makes Markdoc worth the setup cost over simpler alternatives.
Deploying Markdoc Sites with DeployHQ
A Markdoc/Next.js documentation site deploys like any other Next.js application. The build output depends on whether you're using static export or server-side rendering.
For static export (recommended for documentation sites), add this to next.config.js:
module.exports = withMarkdoc()({
output: 'export',
pageExtensions: ['md', 'mdoc', 'js', 'jsx', 'ts', 'tsx'],
});
DeployHQ can handle the full build and deployment pipeline. Connect your Git repository, add your server, and configure your build commands:
npm ci
npm run build
For a static export, set your deployment path to the out/ directory. For SSR deployment, the .next/ directory contains what you need.
Configure a webhook in your Git provider so DeployHQ automatically triggers a deployment on every push to your main branch. For documentation sites, this is particularly useful — technical writers and developers can push content changes and the updated site deploys without any manual intervention.
DeployHQ also handles environment variables, which matters when your documentation site pulls content from an API or needs build-time tokens. Set them in the DeployHQ project settings and they're injected during the build step.
Ready to ship your documentation site? Sign up for DeployHQ to automate your deployment pipeline. Questions? Reach us at support@deployhq.com or on Twitter at @deployhq.