Eleventy
English | 한국어 · All guides
Set up cudoc in an Eleventy site: the Markdown extensions first, then document embedding, and an optional HTML export at the end.
See it first: the Eleventy demo is the runnable example built by Eleventy 3.1.7, with links to the Markdown it was built from. → Host demos
Follow the steps in order. Steps 1–4 enable the Markdown extensions; steps 5–7 add document embedding, which lets one document reuse another; step 8 is optional.
Step 1 — Install
Install the core package, the Eleventy adapter and three markdown-it plugins:
npm install @cudoment/cudoc cudoc-eleventy markdown-it-attrs markdown-it-anchor markdown-it-container
Eleventy uses markdown-it, not remark, so it does not use cudoc-remark. cudoc-eleventy brings the shared markdown-it layer, cudoc-markdown-it, with it. The three markdown-it-* plugins are the native syntax the next step registers.
Step 2 — Build the renderer in a shared module
Create the markdown-it instance once, in a module that both the configuration and the collector import, because Eleventy owns that instance:
// markdown.mjs
import attrs from "markdown-it-attrs"
import anchor from "markdown-it-anchor"
import container from "markdown-it-container"
import { createMarkdownRenderer } from "cudoc-eleventy"
export const syntax = { headingAnchor: "both", callout: "both" }
export const createRenderer = (library) =>
createMarkdownRenderer({ syntax, library }, (md) => {
md.use(attrs, { allowedAttributes: ["id"] })
md.use(anchor, { permalink: anchor.permalink.linkInsideHeader() })
for (const type of ["warning", "tip", "info", "danger", "details"])
md.use(container, type)
})
Native syntax comes from the plugins you register. callout: "host" has nothing to normalize until markdown-it-container is registered, and headingAnchor: "host" needs markdown-it-attrs.
Step 3 — Wire it into the config
Merge this into eleventy.config.mjs, keeping your site's plugins, filters and other settings. dir.input is the directory the collector will collect, docs here:
// eleventy.config.mjs
import { createRenderer } from "./markdown.mjs"
export default function (eleventyConfig) {
eleventyConfig.setLibrary("md", createRenderer())
eleventyConfig.addPassthroughCopy({
"node_modules/@cudoment/cudoc/styles.css": "cudoc.css",
})
return {
dir: { input: "docs", output: "_site" },
markdownTemplateEngine: false,
}
}
Two of these settings are required, not preferences.
markdownTemplateEngine: falsestops Liquid rewriting the Markdown before markdown-it sees it. cudoc reads source positions out of the token stream, and the adapter raises an error when it detects a source another engine already rendered.
anchor.permalink.linkInsideHeader()puts the permalink inside the heading, and cudoc tells the permalink from the heading's text by its default classheader-anchor. A permalink that wraps the heading puts the heading's own text inside a link, where cudoc's(#id)anchors are no longer part of it.
Step 4 — Link the stylesheet from your layout
Link /cudoc.css, copied through by step 3, from your layout: Eleventy ships no theme, so the layout is where the stylesheet and navigation belong.
The stylesheet draws callouts, code titles and tabs dark when the system prefers dark and leaves the text colour to the page. So a layout that stays light gives its <html> the class light, and one that follows the system declares <meta name="color-scheme" content="light dark">.
Stop here if you only want the syntax extensions. Build your site and the features in Markdown syntax work. Continue for document embedding.
Step 5 — Add the collector
Create collect.mjs in your site root. It builds the renderer from the same markdown.mjs the site uses and passes createDocumentCompiler(md) to collectDocuments, which writes the library and the prepared embeds together: a failed run leaves the previous pair in place.
// collect.mjs
import { collectDocuments } from "@cudoment/cudoc/node/watch"
import { createDocumentCompiler } from "cudoc-eleventy"
import { createRenderer, syntax } from "./markdown.mjs"
// The renderer the site builds with. It needs no library: collection
// resolves embeds from the documents it collects.
const md = createRenderer()
await collectDocuments({
sourceRoot: "docs",
outDir: ".cudoc/documents",
host: "eleventy",
// Eleventy writes directory URLs, so `reference.md` is served at `/reference/`.
routeSuffix: "/",
syntax,
compiler: createDocumentCompiler(md),
// Change it whenever markdown.mjs or the Eleventy version changes.
compilerId: "eleventy-v1",
})
Step 6 — Load the library into the renderer
Pass loadLibrary(".cudoc/documents") into createRenderer() in eleventy.config.mjs, keeping your site's other settings:
// eleventy.config.mjs
import { loadLibrary } from "@cudoment/cudoc/node/library"
import { createRenderer } from "./markdown.mjs"
export default function (eleventyConfig) {
eleventyConfig.setLibrary(
"md",
createRenderer(loadLibrary(".cudoc/documents")),
)
eleventyConfig.addPassthroughCopy({
"node_modules/@cudoment/cudoc/styles.css": "cudoc.css",
})
return {
dir: { input: "docs", output: "_site" },
markdownTemplateEngine: false,
}
}
Collection and rendering must agree on every Markdown option. Both come from markdown.mjs, so they agree by construction: keep it that way, and change compilerId when relevant settings change.
Step 7 — Collect before every build
Add the scripts to package.json:
{
"scripts": {
"collect": "node collect.mjs",
"check": "cudoc check --config cudoc.config.mjs",
"dev": "npm run collect && eleventy --serve",
"build": "npm run collect && npm run check && eleventy"
}
}
cudoc check runs after collect: it reads the library the collector published and collects nothing, so its configuration only has to say where that library is:
// cudoc.config.mjs
export default { sourceRoot: "docs", outDir: ".cudoc/documents" }
Root-relative images, such as /img/logo.png, are looked up under docs. If your passthrough copies them from a directory outside it, name that directory in check: { assetDirs: [...] } here and with --asset-dir in step 8.
After editing a source document, run collection again and restart the dev server. watchDocuments called with the collector's configuration collects on every change, and so does cudoc collect --watch given a configuration file that holds the collector's whole configuration, compiler included. The restart is needed because the plugin holds the library it loaded when the configuration was evaluated, and reports a stale source until it is reloaded. → Reference checking
Front matter does not count as a change: Eleventy hands the renderer the page without it, and a page whose text after the front matter is what was collected is current.
Step 8 — Optionally export HTML
Install cudoc-export and export the collected library as HTML:
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/
Your Eleventy build and its collected data are not modified. → Export
What you can now write
The syntax rows below work after step 4, and the embedding rows after step 7:
| Feature | Example | Details |
|---|---|---|
| Explicit heading anchors | ## Limits (#limits) |
Syntax |
| Native heading anchors | ## Limits {#limits} |
Syntax |
| Heading badges | ## Limits (#limits) (@New) |
Syntax |
| Callouts with titles | > [!NOTE] Before you start |
Syntax |
| Native containers | ::: warning Title |
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 |
details containers keep their expandable behaviour.
Eleventy specifics
Routes are directory URLs. The collector uses routeSuffix: "/" to match Eleventy's default, so reference.md is collected as /reference/. Change the collected routes when you change permalink or output extensions, and for a pathPrefix unless Eleventy's HtmlBasePlugin is added, which puts the prefix in front of every root-relative address in the built pages, the ones an embed draws included.
Links to .md files stay as written. Eleventy does not rewrite a link's address, so [Limits](reference.md#limits) points at a file the site does not serve. Link to the page's URL, /reference/#limits, in documents and in linkReferences alike; the links an embed draws already use the collected routes.
Gitignored input. Eleventy skips gitignored files by default. If your documents are gitignored, as the synced fixtures in this repository's example are, add eleventyConfig.setUseGitIgnore(false).
Markdown only. Write .md. React .mdx is not processed. → Choosing .md or .mdx
YAML or JSON front matter. Collection splits front matter the way Eleventy does with its default front-matter options, and reads YAML, the default, or ---json. JavaScript front matter (---js) would have to run to be read, so collection fails on it, naming the file; write that page's front matter as YAML.
Leave setFrontMatterParsingOptions at its defaults. An excerpt or other delimiters change the text Eleventy renders, so collection no longer matches it, and another default language has Eleventy read front matter that names no language differently from collection, which reads it as YAML. → markdown-it internals
Your plugins keep their rendering. A code block goes through the fence renderer your site registered. A markdown-it-container whose name is not a callout type, such as a site's own ::: demo, opens and closes as that plugin renders it. Inside it, cudoc reads the content like the rest of the page, so a heading anchor or an embed there still works. cudoc replaces only what it normalizes.
Tables of contents and embedded headings. Eleventy has none of its own. One built by a markdown-it plugin reads the tokens and lists the page's own headings; one built from the rendered HTML, such as an Eleventy transform, also lists headings that arrive through an embed.
Warnings while building. A diagnostic, such as an unregistered callout type or two headings with the same ID, is printed as a warning naming the page and line while Eleventy renders it. The line counts from the top of the file when the renderer has the collected library, and from the end of the front matter otherwise. Pass onDiagnostic(diagnostic, documentId) in the renderer options to handle them yourself. → markdown-it internals
Shared with VitePress. This adapter and the VitePress adapter both sit on cudoc-markdown-it, so the two hosts convert the actual native token stream through the same code and keep heading and TOC integration identical.
Next
Continue with the embedding guide, the markdown-it internals or the runnable example:
- Document embedding: selection, multiple sources, refresh rules
- markdown-it internals: token conversion and host definitions
- Runnable example: a working site