Skip to content
v0.6.5

Official plugins

A Markdown plugin is a build-time transform added to the content compiler. It receives one Markdown or MDX syntax tree and returns transformed syntax or HTML. A plugin can also declare the CSS and browser scripts required by that page. The deployed result remains static HTML; the plugin itself does not run in production.

For example, math() turns $E = mc^2$ into KaTeX HTML during the build. mermaid() loads its diagram client only on pages that contain executable Mermaid fences. A search provider has a different lifecycle: it receives the completed site and writes a site-wide index instead of transforming one document.

Official plugins are published in @canofold/plugins:

pnpm
pnpm add -D @canofold/plugins

Available capabilities

table
externalLinks()markdown.pluginsAdd safe attributes to external linksNone
linkCard()markdown.pluginsRender a standalone link as a cardNone
readingTime()markdown.pluginsShow reading time below the titleNone
math()markdown.pluginsRender mathematical notationImport @canofold/plugins/math.css when using the Markdown package directly
mermaid()markdown.pluginsRender Mermaid diagramsInstall mermaid
plantUml()markdown.pluginsRender PlantUML diagramsConfigure a trusted server or host renderer
kroki()markdown.pluginsRender Graphviz and D2 through KrokiConfigure a trusted server or a host renderer
pagefind()search.providerBuild a Pagefind search indexInstall pagefind

Configure and use plugins

These packages run during local development and site builds, so install them as development dependencies. Add only the capabilities the site needs; when enabling Mermaid or Pagefind, install the corresponding dependency as well:

bash
pnpm add -D mermaid@^12.2.0 pagefind

Mermaid 12 requires Node 22.12+, Safari 17.4+, or equivalent ES2024 browser support. Canofold defaults to Dagre layout and the Classic look; authored diagram configuration can still select ELK or another layout. Its runtime is built from the site's installed package, not bundled inside @canofold/plugins; upgrading the plugin alone does not upgrade Mermaid. Mermaid 11 remains API-supported but retains a low-severity advisory in its old KaTeX dependency. New projects should use 12.2.0 or a newer supported release rather than a cross-version dependency override.

canofold.config.ts
import { externalLinks, math, mermaid, pagefind } from '@canofold/plugins'import { defineConfig } from 'canofold'export default defineConfig({  search: {    provider: pagefind()  },  markdown: {    plugins: [      math(),      mermaid(),      externalLinks({ internalHosts: ['docs.example.com'] })    ]  }})

readingTime() includes English and Chinese labels. Pass labels only when adding another locale or changing the wording.

After configuration, write the syntax owned by the plugins:

markdown
Inline math $E = mc^2$.```mermaidflowchart LR  Markdown --> HTML```

canofold check loads the config and uses plugin declarations to validate directives and code fences; it does not render complete pages. canofold build and canofold dev execute plugin transforms. Removing a plugin stops its syntax from being transformed, and fences not claimed by another plugin remain subject to the unknown-language policy.

Kroki and PlantUML send diagram source to an external service only when server is configured. Canofold emits one explicit warning when a document actually activates either external renderer. Public content may use a reviewed service, while private or restricted content should use a trusted self-hosted endpoint. The warning does not replace network, CSP, or data-governance review.

Host-provided diagram rendering

Editor, React, and webview hosts can keep diagram source local by selecting renderer: 'client' and passing a renderer to the matching client enhancer:

ts
import { kroki } from '@canofold/plugins/kroki'import { enhance, type DiagramRenderer } from '@canofold/plugins/client/kroki'const plugin = kroki({ renderer: 'client' })const renderer: DiagramRenderer = async ({ diagramType, source, signal }) => ({  svg: await renderLocally({ diagramType, source, signal })})const diagrams = enhance(markdownRoot, { renderer })await diagrams.readydiagrams.dispose()

The host owns workers, native processes, and editor message bridges. The renderer receives the plugin kind, authored language, mapped diagram type, source, and an AbortSignal, and returns a non-empty SVG string. Dispose the enhancer before replacing or unmounting the Markdown DOM. A restrictive CSP must allow blob: in img-src. server and renderer: 'client' are mutually exclusive; client rendering supports SVG only.

The host implements renderLocally; it is not a runtime exported by the plugin package. PlantUML uses the matching @canofold/plugins/client/plantuml enhancement entry.

Run and verify

bash
pnpm docs:checkpnpm docs:dev

Open the page containing the sample syntax. The formula should be typeset, and Mermaid should display a diagram instead of source. External links should have the configured target and rel attributes.

Check production output before publishing:

bash
pnpm docs:buildpnpm docs:preview

Search for the page in the preview and confirm that Pagefind returns it. Ordinary pages should not load the Mermaid client; the client is added only to pages with executable Mermaid fences. If a plugin has no effect, see Troubleshooting.

Option reference

Every option is optional. The table lists the behavior used when an option is omitted:

table
externalLinksnewTabtrueOpen external links in a new window
rel['noopener', 'noreferrer']Values written to external-link rel
internalHosts[]Hosts and subdomains treated as internal
linkCardinternalHosts[]Hosts that are not converted to external cards
includeRelativefalseAlso convert site links beginning with /
readingTimewordsPerMinute220Latin words per minute; must be positive and finite
cjkWordsPerMinute300CJK characters per minute; must be positive and finite
includeCodefalseInclude code in the estimate
label'{minutes} min read'Fallback template when no locale label matches
labelsBuilt-in ChineseOverride templates by locale while retaining {minutes}
maththrowOnErrorfalseThrow when KaTeX encounters invalid input
errorColor'#b42318'Error color when errors are rendered
trustfalseAllow trusted KaTeX commands that emit URLs or HTML
strict'warn'KaTeX strictness policy
macros{}Custom KaTeX macros
mermaidmoduleUrlBuild of the local dependencyOverride the browser Mermaid ESM URL
plantUmlserveromittedTrusted PlantUML HTTP(S) base URL; source-only output when omitted
rendererunsetUse 'client' to delegate SVG rendering to the host
krokiserveromittedTrusted Kroki HTTP(S) base URL; source-only output when omitted
rendererunsetUse 'client' to delegate SVG rendering to the host
languagesGraphviz, Dot, GV, D2Markdown language to Kroki type mapping
format'svg'Emit svg or png
pagefindincludeCharacters'._-'Characters retained during Pagefind tokenization
keepIndexUrlfalsePreserve index-page URLs
writePlaygroundfalseGenerate Pagefind's debugging playground

Plugins, providers, and extensions

Starting in 0.5.0, mermaid() builds browser resources from the project's installed mermaid by default. Rebuild and deploy after dependency updates. Setting moduleUrl uses that module instead; you manage its version and availability. See security boundaries.

table
Markdown pluginSyntax or HTML transformation within one documentmarkdown.plugins
Search providerAn index generated from the complete sitesearch.provider
ExtensionRepository source transforms, page metadata, or extra filesextensions

The package manager installs the complete @canofold/plugins package. Only plugins passed to the config process content and affect build output. Capabilities with browser runtimes also load assets per page. Only an executable Mermaid fence activates Mermaid; a Mermaid example shown inside a tutorial code block does not load the runtime.

Use the package root in normal site config. Capability subpaths remain public APIs for libraries and tools that resolve one focused entry.

Build a minimal plugin

A custom plugin can live in the project and be imported by canofold.config.ts. This complete example adds a project-owned data-section attribute to every level-two heading without relying on internal Canofold classes:

markdown/section-labels.mjs
import { defineMarkdownPlugin } from '@canofold/markdown'function markSections() {  return (tree) => {    const walk = (node) => {      if (node.type === 'element' && node.tagName === 'h2') {        node.properties ??= {}        node.properties.dataSection = ''      }      node.children?.forEach(walk)    }    walk(tree)  }}export function sectionLabels() {  return defineMarkdownPlugin({    name: 'section-labels',    version: '1',    rehypePlugins: [markSections]  })}
canofold.config.ts
import { defineConfig } from 'canofold'import { sectionLabels } from './markdown/section-labels.mjs'export default defineConfig({  markdown: { plugins: [sectionLabels()] },  styles: ['./docs/section-labels.css']})
docs/section-labels.css
h2[data-section] {  border-inline-start: 0.2rem solid currentColor;  padding-inline-start: 0.75rem;}

Run pnpm docs:dev and verify that level-two headings have a leading rule. Then run pnpm docs:build and confirm that generated level-two headings have a data-section attribute. The first result verifies the project CSS; the second verifies the plugin transform.

name is unique within one compiler. Bump version when transform behavior changes and put resolved options in cacheKey. A plugin that owns custom directives or code fences must also declare directiveNames or fenceLanguages, so typo and unknown-language checks can distinguish valid plugin syntax.

Browser resources

Each assets.clients[].resources item declares a module resolvable from the client package and an output filename relative to its resource directory. Files are copied by default; prebuilt chunk directories can be declared with directories. For browser modules with npm imports, set bundle: true so the Canofold host bundles installed dependencies into ESM while preserving lazy chunks. Output must end in .js or .mjs, and directories cannot be combined with bundling. Bundling does not execute the entry and is not a security sandbox: plugins and dependencies must remain trusted.