Eleventy Data Cascade as the Smallest Suitable Design for Page Assembly
An architecture note on using Eleventy's data cascade (global data, directory data, front matter, computed data) as the minimal design for page data assembly, covering trust boundaries, failure modes, and operational verification steps.
11 Jan 2026, 22:55 UTC

Requirements: What the Data Layer Must Solve
Static sites need a predictable way to assemble page data from shared configuration, section defaults, and page-specific overrides. The requirements are straightforward:
- Global values (site title, navigation, build-time constants) defined once.
- Per-section defaults (blog layout, product category metadata) that apply to a subtree of pages.
- Page-level overrides (title, date, canonical URL, SEO fields) without duplicating shared structure.
- Derived fields (canonical URLs, normalized titles, computed dates) that depend on merged values.
- Build-time safety: no silent overwrites, no unvalidated external input, no whole-build failures from a single bad page.
Eleventy's data cascade satisfies these with a defined precedence order and a computed-data slot evaluated last. The smallest suitable design uses only three layers: global data files, directory data files, and front matter—plus eleventyComputed for derived fields.
Smallest Suitable Design
Layer 1: Global Data in _data/
Place static, site-wide values in _data/*.json or _data/*.js files. Each file becomes a top-level key named after the filename (without extension).
// _data/site.json
{
"title": "Acme Corp",
"url": "https://acme.example.com",
"navigation": [
{ "label": "Home", "href": "/" },
{ "label": "Blog", "href": "/blog/" }
]
}
These values are available to every template as site.title, site.url, etc.
Layer 2: Directory Data Files
Create a _data.json (or _data.11tydata.js) inside any directory to set defaults for that subtree. For a blog section:
// blog/_data.json
{
"layout": "layouts/post.njk",
"tags": ["blog"],
"permalink": "/blog/{{ page.fileSlug }}/"
}
All templates under blog/ inherit these unless overridden. Directory data merges after global data, so blog/_data.json can add or replace keys from site.
Layer 3: Front Matter (Template & Layout)
Page-specific values go in the template's front matter. Layout front matter is evaluated before template front matter, so the template wins.
---
layout: "layouts/post.njk"
title: "Introducing Eleventy Data Cascade"
date: 2026-10-12
canonical: "https://acme.example.com/blog/eleventy-data-cascade/"
---
Post content here.
Precedence order (lowest to highest):
- Global data files (
_data/) - Directory data files (nearest ancestor first)
- Layout front matter
- Template front matter
- Computed data (
eleventyComputed)
More specific sources override broader ones. A front-matter title replaces any title from directory or global data.
Layer 4: Computed Data for Derived Fields
Use eleventyComputed in front matter (or a data file) for values that depend on the already-merged cascade. It runs last and can reference any earlier key.
---
title: "Introducing Eleventy Data Cascade"
date: 2026-10-12
eleventyComputed:
canonical: "{{ site.url }}{{ page.url }}"
normalizedTitle: "{{ title | slugify }}"
readingTime: "{{ content | countWords | divide: 200 | round }}"
---
This keeps templates free of logic and ensures derived fields always reflect the final merged values.
Trust and Data Boundaries
JS Data Files Execute Arbitrary Code
Any _data/*.js or _data/*.11tydata.js file runs with full Node privileges at build time. It can read the filesystem, call external APIs, or spawn processes. Treat the output of such files as untrusted until validated.
Example of a risky pattern:
// _data/external.js — DO NOT USE WITHOUT VALIDATION
const fetch = require('node:fetch');
module.exports = async () => {
const res = await fetch('https://api.example.com/config');
return res.json(); // raw external data injected into cascade
};
Safer pattern: validate, cache, and fail explicitly.
// _data/external.js — safer
const fetch = require('node:fetch');
const fs = require('node:fs/promises');
const CACHE_FILE = '.cache/external-config.json';
const SCHEMA = { /* JSON Schema or Zod schema */ };
module.exports = async () => {
let data;
try {
const cached = await fs.readFile(CACHE_FILE, 'utf8');
data = JSON.parse(cached);
} catch {
const res = await fetch('https://api.example.com/config', { timeout: 5000 });
if (!res.ok) throw new Error(`Config fetch failed: ${res.status}`);
data = await res.json();
// Validate against schema
if (!validate(data)) throw new Error('Config validation failed');
await fs.writeFile(CACHE_FILE, JSON.stringify(data));
}
return data;
};
Run this validation in a separate build step or CI job, not implicitly during page rendering.
Front Matter from Untrusted Editors
If content authors supply front matter (via CMS, headless editor, or PRs), treat it as untrusted input. Add a pre-build validation step that checks required fields, types, and allowed values before Eleventy runs.
// scripts/validate-frontmatter.js
const { glob } = require('glob');
const matter = require('gray-matter');
const schema = require('./frontmatter-schema.json');
const files = await glob('**/*.md', { ignore: 'node_modules/**' });
for (const file of files) {
const { data } = matter.read(file);
const result = validate(data, schema);
if (!result.valid) {
console.error(`Invalid front matter in ${file}:`, result.errors);
process.exit(1);
}
}
Run this in CI before npx @11ty/eleventy.
Operational Checks
Verify Precedence with a Debug Page
Create a temporary page that prints the resolved value of a key defined at multiple levels.
// _data/debug.json
{ "layer": "global", "value": "from-global" }
// blog/_data.json
{ "layer": "directory", "value": "from-directory" }
// blog/test-cascade.njk
---
title: "Cascade Test"
layer: "front-matter"
value: "from-front-matter"
---
Cascade Test
{{ layer }} → {{ value }}
// Expected output: "front-matter → from-front-matter"
Run npx @11ty/eleventy --serve and visit /blog/test-cascade/. Confirm the front-matter value wins. Remove the test page after verification.
Inspect Merged Data with --json or Debug Dump
Eleventy can output the full data cascade for a page:
npx @11ty/eleventy --json=blog/test-cascade/index.html 2>/dev/null | jq '.data'
Or add a debug template that dumps page and site objects:
// debug-dump.njk (excluded from production via eleventyIgnore)
---
eleventyExcludeFromCollections: true
permalink: false
---
{{ page | dump | safe }}
{{ site | dump | safe }}
Use the dump filter (from @11ty/eleventy-plugin-dump or a custom filter) to see the merged object.
Confirm Computed Data Sees Merged Values
Add a computed field that references a key from each layer:
---
eleventyComputed:
cascadeTrace: "global={{ site.layer }}, dir={{ page.layer }}, fm={{ layer }}"
---
Check the rendered output or the JSON dump to verify cascadeTrace reflects the final merged state.
Version-Specific Behavior Check
Deep-merge behavior for nested objects changed across Eleventy versions. Before relying on it, check the installed version's changelog:
npx @11ty/eleventy --version
# Then consult CHANGELOG.md or GitHub releases for that version
As of Eleventy 2.x, deep merge is opt-in via dataTemplateEngine: "njk" and mergeData: true in the config, but behavior varies. Test your specific nesting case.
Failure Modes
Silent Key Collisions
A front-matter key unintentionally overrides a global value. Example: a global site.description used for SEO meta tags gets replaced by a page's description meant for an excerpt. The template then renders the excerpt in the meta tag.
Mitigation: namespace global keys (site.seoDescription) and document the reserved namespace. Use a linting rule that flags front-matter keys matching global top-level keys.
Stale Cached Remote Data
JS data files that fetch remote config and cache it locally can serve stale data if the cache isn't invalidated. The build succeeds but publishes outdated values.
Mitigation: include a cache timestamp or content hash in the cached file; invalidate on a schedule or via webhook. Run a separate "refresh data" CI job nightly.
Computed-Data Errors Fail the Entire Build
An error in eleventyComputed (e.g., a filter throwing on missing input) stops the build for all pages, not just the offending one.
Mitigation: write defensive computed functions. Use optional chaining and default values:
eleventyComputed:
canonical: "{{ (site.url || '') + (page.url || '') }}"
readingTime: "{{ (content | countWords | default: 0) | divide: 200 | round | default: 1 }}"
Test computed fields in isolation with a minimal page before adding to shared layouts.
Shallow Merge Replaces Nested Objects
Without deep merge enabled, a directory data file defining seo: { title: "Blog" } completely replaces a global seo: { title: "Site", description: "..." }, losing the description.
Mitigation: either enable deep merge in .eleventy.js (verify version support) or flatten the structure (seoTitle, seoDescription) so shallow merge suffices.
Conditions That Would Change the Design
| Condition | Design Change | Migration Path |
|---|---|---|
| Need per-request data (A/B tests, personalization, auth) | Move to Eleventy Serverless or edge functions; data cascade becomes build-time only | Keep cascade for static shell; add serverless endpoints for dynamic fragments |
| Very large datasets (10k+ items, heavy transforms) | Externalize to a build-time data pipeline (Node script, SQLite, Parquet) that writes JSON to _data/ before Eleventy runs |
Add a prebuild script in package.json; Eleventy only consumes generated JSON |
| Untrusted editors supply front matter | Add schema validation gate before Eleventy (CI step or pre-commit hook) | Write JSON Schema/Zod schemas; fail build on invalid front matter |
| Multiple sites from one repo with different global data | Use --config flag with separate config files, each pointing to its own _data/ directory |
Structure as sites/site-a/_data/, sites/site-b/_data/; build with eleventy --config=site-a.config.js |
| Need runtime i18n with per-locale data | Use directory-per-locale (en/_data.json, fr/_data.json) or a plugin like @11ty/eleventy-plugin-i18n |
Restructure content directories; cascade still works per-locale subtree |
Practical Verification Checklist
- Precedence test: Create a page with a key defined at global, directory, and front-matter levels. Confirm front-matter wins.
- Computed-data test: Add a computed field referencing keys from each layer. Verify it sees the final merged values.
- Deep-merge test: Define a nested object at global and directory levels. Check whether the result is merged or replaced. Adjust config or flatten structure accordingly.
- JS data file audit: List all
_data/*.jsfiles. Confirm each either (a) has no side effects, or (b) validates and caches external input. - Front-matter schema: If editors write front matter, run the validation script locally and in CI. Confirm it catches missing required fields and type mismatches.
- Version lock: Pin Eleventy version in
package.json(e.g.,"@11ty/eleventy": "^2.0.1"). Document the version-specific behaviors you rely on.
Limitations
- The cascade is build-time only. No per-request variation without Serverless/edge.
- Deep-merge behavior is version-dependent and not universally enabled. Test your version.
- JS data files run with full Node privileges. There is no sandbox.
- Computed data errors are fatal to the build. No per-page graceful degradation.
- No built-in schema validation for front matter or data files. You must add it.
Summary
Eleventy's data cascade—global _data/, directory data, front matter, eleventyComputed—is the smallest suitable design for assembling page data in a static site. It satisfies the requirements with four explicit layers, a defined precedence order, and a late-evaluation slot for derived fields. The trust boundary sits at JS data files and untrusted front matter; both require explicit validation. Failure modes are predictable (silent collisions, stale caches, fatal computed errors, shallow merge) and mitigable with naming conventions, cache invalidation, defensive computed functions, and flattening or opt-in deep merge. When requirements shift to per-request data, massive datasets, or untrusted editors, the design changes in documented ways: Serverless, external pipelines, or validation gates. Verify the actual behavior against your installed Eleventy version before committing to the pattern.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.