Docusaurus
English | 한국어 · All guides
Set up cudoc in a Docusaurus site: the Markdown extensions first, then document embedding, and an optional HTML export at the end.
See it first: the Docusaurus demo is the runnable example built by Docusaurus 3.10.2, with links to the Markdown it was built from. → Host demos
Follow the steps in order. Steps 1–3 enable the Markdown extensions; steps 4–6 add document embedding, which lets one document reuse another; step 7 is optional.
Step 1 — Install
Install cudoc, its remark adapter and the Docusaurus adapter:
npm install @cudoment/cudoc cudoc-remark cudoc-docusaurus
Docusaurus 3.10 depends on four packages whose advisories npm audit reports, and its own ranges do not let npm choose the fixed releases:
| Package | Where Docusaurus 3.10 uses it |
|---|---|
serialize-javascript 6 |
its webpack plugins |
uuid 8 |
its development server |
postcss-selector-parser 6 |
its CSS minifier |
tinypool 1 |
building pages on worker threads |
The example site builds and runs its development server with the fixed releases pinned, so add the same overrides to package.json, beside any it already has, and install again:
{
"overrides": {
"postcss-selector-parser": "7.1.6",
"serialize-javascript": "7.1.3",
"tinypool": "2.2.0",
"uuid": "11.1.1"
}
}
npm install
Remove an entry once a Docusaurus release depends on the fixed version itself; npm audit shows whether one is still needed.
Step 2 — Register the remark plugins
Merge this into docusaurus.config.mjs, keeping your site's other settings:
// docusaurus.config.mjs
import { cudocRemarkPlugins } from "cudoc-docusaurus"
export default {
// Your site's own settings stay as they are.
title: "My docs",
url: "https://docs.example.com",
baseUrl: "/",
markdown: { format: "detect" },
presets: [
[
"classic",
{
docs: {
path: "docs",
routeBasePath: "docs",
beforeDefaultRemarkPlugins: cudocRemarkPlugins({ syntax: {} }),
},
theme: { customCss: "./src/css/custom.css" },
},
],
],
}
It must be beforeDefaultRemarkPlugins, not remarkPlugins. cudoc assigns heading anchors, and Docusaurus generates its own heading IDs and table of contents afterwards; running cudoc second would leave the two disagreeing.
Step 3 — Import the stylesheet
Import the stylesheet once, at the top of your custom CSS:
/* src/css/custom.css — add at the top */
@import "@cudoment/cudoc/styles.css";
This styles callouts, badges, code titles, tabs, table notes and document trees. It sets no text colour of its own, so it follows your site's light and dark themes.
Stop here if you only want the syntax extensions. Start your site and the features in Markdown syntax work. Continue for document embedding.
Step 4 — Add the embed plugin and the library loader
The embed plugin goes in the same docs options, in remarkPlugins, so it runs after Docusaurus's own processing. The loader goes in a small plugin of the site's own. With both, the configuration reads as follows, your site's other settings kept:
// docusaurus.config.mjs
import path from "node:path"
import { cudocRemarkPlugins } from "cudoc-docusaurus"
import embed from "cudoc-remark/embed"
import { libraryLoader } from "cudoc-remark/loader"
export default {
// Your site's own settings stay as they are.
title: "My docs",
url: "https://docs.example.com",
baseUrl: "/",
markdown: { format: "detect" },
presets: [
[
"classic",
{
docs: {
path: "docs",
routeBasePath: "docs",
beforeDefaultRemarkPlugins: cudocRemarkPlugins({ syntax: {} }),
remarkPlugins: [
[embed, { sourceRoot: "docs", outDir: ".cudoc/documents" }],
],
},
theme: { customCss: "./src/css/custom.css" },
},
],
],
plugins: [
() => ({
name: "cudoc-library",
configureWebpack: () => ({
module: {
rules: [
{
test: /\.mdx?$/,
include: [path.resolve("docs")],
use: [libraryLoader(".cudoc/documents")],
},
],
},
}),
}),
],
}
The embed plugin splices prepared content into the page as it compiles, so the compiled page depends on .cudoc/documents/embeds.json. The loader makes a recollection reach pages the dev server and the build cache have already compiled; it leaves your files alone and adds one invisible reference definition to the compiled input.
The rule has to name your content directory in include: Docusaurus builds its fallback MDX loader from the include of every rule matching .mdx, and a rule without one stops the build with an invalid webpack configuration. Docusaurus loads the config as CommonJS, so resolve the path from the working directory rather than from import.meta.
Step 5 — Add the collector
Create collect.mjs in your site root. It runs the actual Docusaurus MDX processor, captures what Docusaurus does to each document, and hands the result to collectDocuments, which writes the library and the prepared embeds together: a failed run leaves the previous pair in place.
// collect.mjs
import path from "node:path"
import { createRequire } from "node:module"
import { cudocRemarkPlugins } from "cudoc-docusaurus"
import { createCompilerCapture } from "cudoc-remark"
import { collectDocuments } from "@cudoment/cudoc/node/watch"
// The options docusaurus.config.mjs passes to cudocRemarkPlugins.
const documentOptions = { syntax: {} }
// Docusaurus's own MDX processor, the one @docusaurus/core depends on. It
// is not a public entry point.
const core = createRequire(import.meta.url).resolve(
"@docusaurus/core/package.json",
)
const { createProcessorUncached } = createRequire(core)(
"@docusaurus/mdx-loader/lib/processor.js",
)
await collectDocuments({
sourceRoot: "docs",
outDir: ".cudoc/documents",
routeBase: "/docs",
host: "docusaurus",
// Change it whenever these settings or the Docusaurus version change.
compilerId: "docusaurus-v1",
...documentOptions,
async compiler(source, { filePath, options }) {
// An image named through Docusaurus's `@site` alias is recorded at the
// address the site serves it from, which the check and the export find.
const capture = createCompilerCapture({ aliases: { "@site/static/": "/" } })
const processor = await createProcessorUncached({
format: options.format,
options: {
siteDir: process.cwd(),
// The site's `staticDirectories`, where `/img/…` images resolve.
staticDirs: [path.resolve("static")],
admonitions: true,
removeContentTitle: false,
// The `markdown` settings of docusaurus.config.mjs, with Docusaurus's
// defaults for anything the site does not set.
markdownConfig: {
anchors: { maintainCase: false },
hooks: {
onBrokenMarkdownLinks: "warn",
onBrokenMarkdownImages: "throw",
},
mdx1Compat: { comments: true, admonitions: true, headingIds: true },
emoji: true,
mermaid: false,
},
beforeDefaultRemarkPlugins: [
...cudocRemarkPlugins(documentOptions),
capture.remark,
],
rehypePlugins: [capture.rehype],
},
})
await processor.process({
content: source,
filePath,
frontMatter: {},
compilerName: "server",
})
return capture.read()
},
})
Set sourceRoot, outDir and routeBase to match your site.
Collection and rendering must agree: the same syntax options, the same native Markdown settings. The collector holds its document options in one documentOptions object, which it spreads into the collectDocuments configuration and passes to the cudocRemarkPlugins call inside its compiler. Give that object the options your site configuration passes to cudocRemarkPlugins, and when you change one, change the other. → Compiler capture
Your static directories are named in four places, so that an image written as @site/static/img/logo.png is checked and exported as /img/logo.png:
staticDirsin the collector lists each of them.aliasesmaps@site/followed by each one to/, the address Docusaurus serves its files at.check.assetDirsincudoc.config.mjsnames each of them.--asset-dirnames each of them on every export.
The collector reads Docusaurus's internal processor module,
@docusaurus/mdx-loader/lib/processor.js, which is not a public API. Check it when you upgrade Docusaurus; supported versions lists the version it is tested with.
Step 6 — Collect before every build
Add the scripts to package.json:
{
"scripts": {
"collect": "node collect.mjs",
"check": "cudoc check --config cudoc.config.mjs",
"start": "npm run collect && docusaurus start",
"build": "npm run collect && npm run check && docusaurus build"
}
}
cudoc check reads the library the collector published and collects nothing, so it runs after collect, and its configuration only has to say where that library is and where the site serves root-relative images from:
// cudoc.config.mjs
export default {
sourceRoot: "docs",
outDir: ".cudoc/documents",
check: { assetDirs: ["static"] },
}
Collection has to run before the site does. To collect again as you write, call watchDocuments from @cudoment/cudoc/node/watch with the same options instead of collectDocuments. The embed plugin inserts the prepared content automatically.
cudoc check reports every broken link, anchor, image and embed in one pass and exits non-zero, so a broken reference stops the build before the site is generated. → Reference checking
Step 7 — Optionally export HTML
Reuse the library you just collected to produce a shareable HTML bundle:
npm install cudoc-export
npx cudoc-export build docs --library .cudoc/documents --out-dir shared-html \
--links host --host-url https://docs.example.com/project/ --asset-dir static
Your Docusaurus build and its collected data are not modified. → Export
What you can now write
The first eight rows are the syntax extensions from steps 1–3 and the last five the embed shapes from steps 4–6; each row links to its reference:
| Feature | Example | Details |
|---|---|---|
| Explicit heading anchors | ## Limits (#limits) |
Syntax |
| Heading badges | ## Limits (#limits) (@New) |
Syntax |
| Callouts with titles | > [!NOTE] Before you start |
Syntax |
| Nested lists in table cells | - Account<br />-- Verified email |
Syntax |
| Merged table cells | ^^ or >> alone in a cell |
Syntax |
| Code titles and highlighted lines | ```js title="retry.js" {2} |
Syntax |
| Tabs | > [!TAB] npm |
Syntax |
| Variables | (=version) |
Syntax |
| Embed a whole document | sources: [reference.md] |
Embedding |
| Embed one section | sources: [reference.md#limits] |
Embedding |
| Heading summary table | select: { depth: 2 } |
Embedding |
| Document tree | render: { type: tree } |
Embedding |
| Replace text in the copy | replace: [{ find, replace }] |
Embedding |
Docusaurus specifics
Native syntax alongside cudoc syntax. Docusaurus admonitions and {#id} heading IDs keep working. To have cudoc normalize them too, so that embedded copies and HTML export carry the same semantics:
cudocRemarkPlugins({ syntax: { headingAnchor: "both", callout: "both" } })
Supported native forms are listed in the syntax guide.
Heading IDs are kept as written. The adapter hands a heading ID that Docusaurus's slugger would change to Docusaurus as a trailing {#id}, which Docusaurus keeps rather than slugging again, so ## Version (#v1.2) stays v1.2 in the page and in its table of contents. Other IDs stay on the heading, where the slugger leaves them as they are and numbers a later heading that slugs to the same value past them.
The reverse order differs between hosts. An ID that an earlier heading's text already produced, ## Setup followed by ## Intro (#setup), comes out as setup-1 here and on Nextra, where VitePress, Eleventy and the standalone export keep setup on the explicit one and number the earlier heading. Write the heading with the explicit ID first, or give the other heading its own ID.
The page's # heading always keeps its ID on the heading, because Docusaurus reads the page title from its text before taking a {#id} out. Give that heading a slug-shaped ID, since one such as (#v1.0) comes out as v10 there.
Headings that arrive through an embed are anchors on the page but not entries in the table of contents, which Docusaurus builds before the embed plugin runs.
Format detection. markdown: { format: "detect" } keeps .md as plain Markdown, where {value} stays literal text, and treats .mdx as MDX. Author your own React components in .mdx. → Choosing .md or .mdx
No theme plugin. This setup needs no cudoc theme plugin and no component registration. Authors write Markdown.
Custom slugs. Supply routes to the collector when your document IDs and URLs differ, and bump compilerId when you change settings that affect compilation.
Next
Continue with the embedding guide, the adapter internals or the runnable example:
- Document embedding: selection, multiple sources, refresh rules
- Adapter internals: ordering, capture, what the adapter sets
- Runnable example: a working site