cudoc

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: false stops 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 class header-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.

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:

cudoc