Table of Contents

Architecture snapshot

The plugin is built in three concentric layers, each with one strict job.

You almost never notice the boundary in day-to-day work, but it explains a lot of "why does this thing live here and not there" once you know it.

This is the deeper read. If you just want to ship, the quickstart is enough; this page is for when you want to understand what is going on under it.


Why look at it this way

The split responds to a structural seam in Eleventy itself. Data resolves through the cascade, templates render, then a separate transform phase writes HTML to disk with no native channel back to what the cascade produced.

A few Eleventy surfaces are also closed by design (the directory map only honours its built-in keys).

The runtime layer is the bridge: registry-scoped stores that capture cascade-time values and hand them out on demand.

Naming the three layers keeps the bridge clean. State is what the user said; runtime is what the build is currently doing; modules are what each feature does with both.

Each has one input, one output, and no other way to talk to the others.

The line worth keeping in mind:
Cascade-time work is plain Eleventy data; transform-time work has access to everything the cascade produced; modules stay out of each other's way.


The three layers


State

State is your input, normalised.

Whatever you pass to baseline(settings, options) gets validated, defaulted, and shaped into the canonical form the rest of the plugin reads from.

Two top-level objects:

  • state.settings: site identity. Title, tagline, url, languages, the head extras object. The things a designer or content author would name.
  • state.options: runtime flags. Which optional modules are on, log verbosity, fine-grained module options like head.titleSeparator or assets.esbuild.

State is computed once, at plugin-init, and treated as immutable from that point on. Modules read it; nothing writes back.

Templates see one slice of state: the _baseline global, carrying name and version alongside options, env, features and paths:

  • env: the package name and the resolved ELEVENTY_ENV mode.
  • options: the resolved state.options, in the shape the plugin settled on.
  • features: booleans only, one per module, plus a derived flag for whether eleventyImageTransformPlugin is loaded.
  • paths: the directory map, including the asymmetric paths.public (the virtual key) versus paths.assets (also virtual), built from the resolved eleventyConfig.directories.

The globals reference covers the exact shape; the plugin entrypoint reference covers the option list that feeds into it.


Runtime

Runtime is the lazy access layer over Eleventy's lifecycle.

It is where the things that are not knowable at plugin-init live: the templates Eleventy is going to build, the relationships between translated pages, the resolved per-page data.

Three pieces matter:

  • The content map. Every template Eleventy sees during the build, available through a getter. Populated by the eleventy.contentMap event; modules read it without subscribing to events themselves.
  • The translation map, and the translation index. Which pages are translations of which, held twice over. The map is written by multilang's collection and read by wikilinks while markdown renders; the index is grouped from the content graph and read by head for hreflang. Two of them because markdown renders during the pre-pass, where the graph does not exist yet.
  • The page context. A normalised per-page object built at cascade-time and cached for transform-time. The user-visible surface is the _pageContext global; the page context reference covers the full shape.

The point of runtime is that modules read through getters, never by direct event subscription. A module asks "what are the translations right now?" and the runtime resolves it.

If the answer is not ready yet, that is a bug in the calling code, not a race condition between modules. The decoupling is structural.

A small set of cross-cutting primitives sits underneath:

  • The registry is the scope primitive (per-config cache, values map, listener dedup). It is the substrate every store and registry below sits on.
  • Virtual directories are Baseline-synthesised keys on eleventyConfig.directories (the assets and public keys), mounted there because Eleventy's directory machinery only honours its fixed set. They read like any other directory key from a module's point of view.
  • Stores are the convention for runtime singletons attached to a registry scope (the content-map store, the two translation stores, and the slug index wikilinks resolves against).

These are internals you can mostly ignore. The internals reference covers them in case you hit one through the navigator.


Modules

Modules are the five feature plugins layered on top of state and runtime.

  • assets: the asset pipeline.
  • head: replaces the <baseline-head> placeholder with the right tags.
  • multilang: directory-based multilingual support. Opt-in.
  • navigator: runtime-introspection tooling, and the read surface for the content graph. The phpinfo()-style virtual page is on in development by default; the read surface is always available.
  • sitemap: XML sitemap generation.

Each is registered through a small declarative registry inside the plugin entry point. multilang and sitemap carry an activation condition read from state.options; the other three always load, and where they have a switch (the navigator's virtual page) it gates one surface rather than the module.

They receive a uniform module context object: { env, state, runtime, directories, helpers, log, snapshots, resolvePageContext, resolveSeoGraph }. The last two are handed over only to the modules that declared they consume them, which today means head alone; every other module gets null there.

That object is the module's only window onto the plugin; nothing else is in scope. Modules never share state directly.

If head needs the translation index, it reads it from runtime. If sitemap needs to know which languages exist, it reads state.settings.

The contract is small and uniform on purpose. The full per-module reference lives in the modules chapter.


How the layers communicate

Your content and templates live in src/content/ and src/_includes/.

The modules wrap around them: assets are compiled, head tags are injected, sitemaps are generated, hreflang is wired. Eleventy writes the result to dist/.

The runtime layer in this instance is the mediator.

  • assets wires PostCSS and esbuild to entry points under src/assets/ and exposes inline filters (inlinePostCSS, inlineESbuild) for critical-path CSS or JS. It reads state.options.assets.esbuild and the assets virtual directory; it does not talk to other modules.

  • head runs in two stages. At cascade-time, the page-context builder produces a normalised head seed object from state.settings.head and the page's own front matter.

    At transform-time, a PostHTML plugin reads those seeds, plus the translation index (when multilang is active) for hreflang alternates, and replaces <baseline-head> with a capo-sorted, deduped element list.

  • multilang populates both translation stores and adds the translationsMap collection, the translation filters, and the computed page.lang / page.locale / page.translationKey / page.isDefaultLang / page.translations fields.

    Activation requires explicit opt-in: options.multilingual: true, plus settings.defaultLanguage or settings.defaultLocale, plus a non-empty settings.languages map.

  • navigator exposes the debug globals and filters, the _snapshot surface, and the site-wide read surface for the content graph.

    The snapshot reads from runtime stores so you can drop {{ _snapshot.contentMap | _json }} into a template and see the build's current state. The site-wide surface and the per-page cascade keys are the same substrate at different scopes; Globals has both shapes.

  • sitemap registers virtual templates that walk collections.all to generate the XML, and (when multilang is on with more than one language) produces per-language sitemaps plus an index.

The cross-module link a careful reader will encounter is head ↔ multilang via the translation map. Multilang writes it; head reads it. Neither calls the other; runtime owns the data.


Where to go next

  • The core reference index for the lookup view of options, globals, filters, and the page context.
  • The page-context reference for the exact shape of the per-page object.
  • The internals reference for the registry, stores, and virtual directories.
  • The modules chapter for what each feature plugin does and the options it accepts.