Pipeline extensions
The HTML pipeline is a sequence of cheerio passes — small async
functions that receive a parsed document and mutate it in place. Two
extension points let consumer projects splice their own passes in:
transformers run before the built-in passes,
finalizers run after. Both receive the same shape, so
deciding which to use is mostly a question of ordering.
When passes run
The order from the plugin’s perspective:
-
Eleventy renders the source article into an HTML string. The pipeline receives this article fragment — the layout hasn’t wrapped it yet.
-
The fragment is parsed into cheerio.
-
Transformers run, in declared order.
-
Built-in content passes run on the article: SVG inlining, image processing, link rewriting, section anchors, code embedding, code highlighting, fragment IDs, ToC building, symbol extraction, Pagefind-ignore tagging, and section-id lifting.
-
Finalizers run, in declared order.
-
The processed article is serialized and wrapped in the apidocs layout by Nunjucks.
-
Whole-document passes run on the wrapped page: current-year injection, then
$VAR$substitution — last, so variables resolve in layout markup (footer year, header site name) as well as the article body. -
URLs are relativized for portability.
In practice: use transformers when you want to inject or
reshape markup before the built-ins see it (e.g. expanding
a custom shortcode into an embeddable shape). Use
finalizers when you want to operate on the
already-processed output (e.g. tagging external links, generating an
RSS payload, computing reading time).
Pass signature
A pass is an async function that receives the cheerio root and a context object:
async function myPass($, ctx) {
// $ is the cheerio root for this article fragment
// ctx is the per-page context
}The context exposes:
- ctx.page
-
Eleventy’s page object. Has
url,inputPath,outputPath, and the usualfileSlug/date. - ctx.sourceDir
-
Directory of the source HTML file. Use it to resolve relative paths (file embeds, image sources, fixtures).
- ctx.outputDir
-
Eleventy’s output root for this build (e.g.
_site). Resolved from the page’s output path. - ctx.variables
-
The
options.variablesmap. Available so passes can substitute or read declared values. - ctx.symbols
-
Build-scoped accumulator for API symbols. Push entries here to contribute to
symbols.json. - ctx.toc
-
Populated by the ToC builder mid-pipeline. Available to finalizers as the nested entry tree the layout would render.
Transformers
Transformers run on the raw article fragment, before any built-in pass
and before the layout wraps it. Wire them via
options.transformers:
import apidocs from "@carrotsearch/eleventy-apidocs";
async function expandShortcodes($, ctx) {
// Rewrite <option name="foo" type="string">…</option> into
// the canonical <dt class="api"> / <dd> pair.
$("option[name]").each((_, el) => {
const $el = $(el);
const name = $el.attr("name");
const type = $el.attr("type");
const dd = $el.html();
$el.replaceWith(`
<dt class="api" id="${name}">${name} <em>${type}</em></dt>
<dd>${dd}</dd>
`);
});
}
export default async function (eleventyConfig) {
return apidocs(eleventyConfig, {
transformers: [expandShortcodes]
});
}By the time the built-in passes see the page, the shortcode is indistinguishable from hand-authored markup — anchor injection, symbol extraction, and fragment IDs all apply to the rewritten structure automatically.
Finalizers
Finalizers run last in the content pipeline, with full access to the processed article — every built-in pass has run, but the layout hasn’t wrapped it yet. They’re the place for cross-cutting concerns that depend on the built-in passes having already run:
async function markExternalLinks($, ctx) {
$("a[href^='http']").each((_, el) => {
const $a = $(el);
if ($a.attr("data-external") !== undefined) return;
$a.attr("target", "_blank");
$a.attr("rel", "noopener noreferrer");
});
}
async function reportSymbols($, ctx) {
console.log(`[symbols] ${ctx.page.url}: ${ctx.symbols.length} entries`);
}
export default async function (eleventyConfig) {
return apidocs(eleventyConfig, {
finalizers: [markExternalLinks, reportSymbols]
});
}
Because finalizers run after symbol extraction,
ctx.symbols contains every symbol harvested up to and
including the current page — useful for emitting per-page
cross-reference data, validating that every symbol has a unique
anchor, or building a search-results preview.
Staying fast
Passes run on every page on every build, so the time you spend in a pass is multiplied by the size of the site. The built-ins keep their cheerio traversals tightly scoped (one selector per pass, no whole-document iteration); custom passes should do the same.
-
Use specific selectors (
article p[data-flag]) over broad ones ($("*")). -
Don’t parse HTML by hand —
$.html()/$.parseHTML()are there for a reason. -
Resolve filesystem paths via
ctx.sourceDir, never withprocess.cwd()— the latter is unstable when Eleventy walks subdirectories.