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, theheadextras 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 likehead.titleSeparatororassets.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, exposed as _baseline.{ env, features, paths }:
env: package name, version, and the resolvedELEVENTY_ENVmode.features: the resolvedstate.optionsplus a couple of derived flags (for example, whethereleventyImageTransformPluginis loaded).paths: the directory map, including the asymmetricpaths.public(the virtual key) versuspaths.assets(also virtual), built from the resolvedeleventyConfig.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.contentMapevent; modules read it without subscribing to events themselves. - The translation map. Which pages are translations of which. Written by
multilangwhen it is active; read byhead(for hreflang) andsitemap(for per-language sitemap routing). - The page context. A normalised per-page object built at cascade-time and cached for transform-time. Shape:
{ site, page, entry, query, meta, render, head }. The user-visible surface is the_pageContextglobal; 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 is the translation map 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 the content-map store, translation-map store, and page-context registry sit on.
- Virtual directories are Baseline-synthesised keys on
eleventyConfig.directories(theassetsandpublickeys), 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 translation-map store).
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: debug tooling. On in development by default.
- sitemap: XML sitemap generation.
Each is registered through a small declarative registry inside the plugin entry point, conditionally activated based on state.options. Each receives a uniform module context object: { env, state, runtime, directories, helpers, log, snapshots, resolvePageContext }. 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 map, 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 talk to each other
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 is the mediator. Concretely:
- assets wires PostCSS and esbuild to entry points under
src/assets/and exposes inline filters (inlinePostCSS,inlineESbuild) for critical-path CSS or JS. It readsstate.options.assets.esbuildand theassetsvirtual directory; it does not talk to other modules. - head runs in two stages. At cascade-time, the page-context builder produces a normalised
headseed object fromstate.settings.headand the page's own front matter. At transform-time, a PostHTML plugin reads those seeds, plus the translation map (whenmultilangis active) for hreflang alternates, and replaces<baseline-head>with a capo-sorted, deduped element list. - multilang populates the translation map and adds per-language collections, the i18n filters, and
eleventyComputed.page.locale. Activation requires explicit opt-in:options.multilingual: true, plussettings.defaultLanguage, plus a non-emptysettings.languagesmap. - navigator exposes the
_runtimeand_ctxglobals, the inspector filters (_inspect,_json,_keys), and the_snapshotdebug surface. The snapshot reads from runtime stores so you can drop{{ _snapshot.contentMap | _json }}into a template and see the build's current state. - sitemap reads the content map, generates the XML, and (when multilang is on) 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.
Previous: Content helpers
Next: Tutorials