A page#
Every .md file under pages/ is a route. Frontmatter drives the document title, the meta description and the page heading:
--- title: Getting started description: Install, run the dev server, and produce the static bundle. ---
pages/_app.tsx reads it from pageProps.markdoc.frontmatter and hands it to the layout.
Tags#
Tags are the part Markdown does not have. They are declared in markdoc/tags/index.js, where render points at the imported React component — the plugin resolves it at build time, so there is no string-to-component map that can drift:
import { Callout } from "../../components/Callout";
export const callout = {
render: Callout,
attributes: {
type: { type: String, default: "note", matches: ["note", "warning", "danger"] },
title: { type: String },
},
};
Used as:
Which renders:
The content gate#
@markdoc/next.js parses and transforms your content. It never calls Markdoc.validate, so on its own the schema is decoration. Measured on this repo before the gate existed: a page with {% callout type="explosive" %} and an entirely undefined tag built with exit 0 and shipped class="callout callout--explosive" into the HTML. errorLevel: "critical" did nothing, because nothing was reading it.
scripts/validate-content.mjs is what reads it. It runs Markdoc.validate over every page, checks that each one has non-empty title and description frontmatter, and checks that every sidebar link resolves to a real route. It is wired into pnpm build, so it guards the deploy too:
✗ pages/docs/getting-started.md:43 — [critical] attribute-value-invalid: Attribute 'type' must match one of ["note","warning","danger"]. Got 'explosive' instead. ✗ pages/docs/getting-started.md:47 — [critical] tag-undefined: Undefined tag: 'totally_unknown_tag' validated 4 page(s), 4 route(s); 2 blocking problem(s)
Run it alone with pnpm validate:content.
Nodes#
Nodes override the built-in Markdown elements. markdoc/nodes/index.js overrides heading to slugify an id and attach an anchor link, so any section is addressable.
Adding a component#
- Write the component under
components/. - Declare the tag in
markdoc/tags/index.jswith its attributes anderrorLevel. - Link the page in
components/nav.ts— the gate fails if a nav link points at a route no page exports, and a page absent from the nav is reachable only by URL.