---
title: 'Architecture snapshot'
description: 'The three-layer model behind baseline: state, runtime, modules. Why it is built that way and how the layers talk to each other.'
slug: 'architecture-snapshot'
type: 'article'
date: 2026-04-28T00:00:00.000Z
lang: 'en'
url: 'https://www.eleventy-baseline.dev/docs/concept/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 | 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.

{% alertBlock %}

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

{% endalertBlock %}

---

## 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 | globals reference]] covers the exact shape; the [[plugin-entrypoint | 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. The user-visible surface is the `_pageContext` global; the [[page-context | 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 | 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 public read surface for plugin-produced cross-page data (`_navigator.nodes`, `_navigator.edges`, `_navigator.backlinks`). 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, conditionally activated based on `state.options`.

They receive 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 [[module | 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.

{% stepsBlock "compact" %}

- **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`), the `_snapshot` debug surface, and the `_navigator` global (`{ nodes, edges, backlinks }`) for cross-page reads of the [[content-graph | content graph]].

  The snapshot reads from runtime stores so you can drop {% raw %}`{{ _snapshot.contentMap | _json }}`{% endraw %} into a template and see the build's current state. The `_navigator` global pairs with the per-page cascade keys `data._node`, `data._backlinks`, and `data._outgoing` for the same substrate at different scopes.

- **sitemap** reads the content map, generates the XML, and (when multilang is on) produces per-language sitemaps plus an index.

{% endstepsBlock %}

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 | core reference]] index for the lookup view of options, globals, filters, and the page context.
- The [[page-context | page-context]] reference for the exact shape of the per-page object.
- The [[internals | internals]] reference for the registry, stores, and virtual directories.
- The [[module | modules]] chapter for what each feature plugin does and the options it accepts.
