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:

  1. Eleventy renders the source article into an HTML string. The pipeline receives this article fragment — the layout hasn’t wrapped it yet.

  2. The fragment is parsed into cheerio.

  3. Transformers run, in declared order.

  4. 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.

  5. Finalizers run, in declared order.

  6. The processed article is serialized and wrapped in the apidocs layout by Nunjucks.

  7. 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.

  8. 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 usual fileSlug / 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.variables map. 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 with process.cwd() — the latter is unstable when Eleventy walks subdirectories.