EnglishNederlandsFrançais Toggle theme

Eleventy Baseline

Start building your site, skip the recurring setup work.
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, exposed as _baseline.{ env, features, paths }:

  • env: package name, version, and the resolved ELEVENTY_ENV mode.
  • features: the resolved state.options plus a couple of derived flags (for example, 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. Which pages are translations of which. Written by multilang when it is active; read by head (for hreflang) and sitemap (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 _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 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 (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 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 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 map (when multilang is 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, plus settings.defaultLanguage, plus a non-empty settings.languages map.
  • navigator exposes the _runtime and _ctx globals, the inspector filters (_inspect, _json, _keys), and the _snapshot debug 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

Previous: Content helpers

Next: Tutorials