Eleventy's Data Cascade: Stop Repeating Configuration Across Templates
Eleventy's data cascade lets you define configuration once at the global level, override per directory, and fine-tune per template—without custom plugins. Here's how the four layers merge, where arrays replace instead of concatenate, and how to debug with --dry-run --output=json.
28 Jun 2026, 15:39 UTC

The problem: configuration sprawl across dozens of pages
You're building a site with Eleventy. The blog section needs a layout: 'post' and tags: ['blog'] on every article. The documentation section needs layout: 'doc' and a different sidebar. The marketing pages need SEO defaults. Without a system, you copy front matter into every file—or write custom plugins to inject data. Both approaches get messy fast.
Eleventy's data cascade solves this with a predictable priority order: global data, directory data, template data, then front matter. Later layers override earlier ones. You define defaults once, override per-section, and fine-tune per-page—no custom code required.
How the cascade layers stack up
Eleventy loads data from four sources in this order (lowest to highest priority):
- Global data files in
_data/*.js|json— available to every template. - Directory data files named
dirname.11tydata.js|json— apply to all templates in that directory and subdirectories. - Template data files named
template-name.11tydata.js|json— sit beside a single template. - Front matter in the template itself (YAML, JSON, or JavaScript) — highest priority.
Each layer receives the merged result of all previous layers. A directory data file can read global data, modify it, and pass it down. A template data file can do the same. Front matter gets the fully merged object and can override any key.
Deep merge for objects, replacement for arrays
This is where developers get tripped up. When merging plain objects, Eleventy does a deep merge:
// _data/seo.json
{ "seo": { "title": "My Site", "description": "Default description" } }
// blog/post.md front matter
---
seo:
title: "Blog Post Title"
---
// Result in the template
{ "seo": { "title": "Blog Post Title", "description": "Default description" } }
But arrays are replaced entirely, not concatenated:
// _data/site.json
{ "tags": ["global"] }
// blog.11tydata.json
{ "tags": ["blog"] }
// post.md front matter
---
tags: ["post"]
---
// Final value: ["post"] — not ["global", "blog", "post"]
To combine arrays, use a computed property in front matter or a template data file:
// post.11tydata.js
module.exports = (data) => ({
tags: [...data.tags, "post"] // manually concatenate
});
Async data functions: fetch at build time, keep templates clean
Template data files (*.11tydata.js) can export async functions. The function receives the merged cascade so far, letting you derive data or hit APIs at build time:
// related-posts.11tydata.js
module.exports = async (data) => {
const res = await fetch(`https://api.example.com/related?tags=${data.tags.join(',')}`);
const related = await res.json();
return { ...data, relatedPosts: related };
};The template stays clean—just loop over relatedPosts. Note: async functions in template data files run once per template file, not per pagination page. For per-page async data, use pagination's resolve callback or a global data file with a keyed cache.
Worked example: a multi-section site
Let's wire up a site with three sections: marketing pages, a blog, and documentation.
1. Global defaults (_data/site.json)
{
"site": {
"name": "Acme Corp",
"url": "https://acme.example",
"seo": { "twitter": "@acme" }
},
"nav": [
{ "text": "Home", "url": "/" },
{ "text": "Blog", "url": "/blog/" },
{ "text": "Docs", "url": "/docs/" }
]
}
2. Blog section defaults (blog.11tydata.json)
{
"layout": "layouts/post.njk",
"tags": ["blog"],
"seo": { "type": "article" }
}
Every template under /blog/ inherits these. Subdirectories like /blog/2024/ get them too.
3. Docs section defaults (docs.11tydata.json)
{
"layout": "layouts/doc.njk",
"tags": ["docs"],
"seo": { "type": "reference" },
"sidebar": "docs"
}
4. A single blog post (blog/2024/launch.md)
---
title: "We Launched!"
date: 2024-01-15
tags: ["announcement", "launch"]
seo:
description: "Announcing our public launch."
---
Content here...
The final merged data for this post includes:
site.name,site.url,site.seo.twitterfrom globallayout,tags: ["blog"],seo.typefrom directory datatitle,date,tags: ["announcement", "launch"](replaces directory tags),seo.description(merges with directoryseo.type) from front matter
Trade-offs and gotchas
- Array replacement surprises. If you expect tags to accumulate, they won't. Plan for it with computed properties.
- Directory data recurses. A
blog.11tydata.jsonaffects/blog/authors/author.mdtoo. Use template data files or front matter to opt out. - JavaScript front matter runs in a sandbox. No
fs,path, or Node built-ins. Need Node APIs? Use a.11tydata.jsfile instead. - Cascade freezes after
beforeBuild. Plugins modifying global data ineleventy.beforework; shortcodes or transforms modifying data won't affect other templates. - Circular references crash the build. Keep data files as plain objects or functions returning plain objects.
Debug with --dry-run --output=json
When the cascade behaves unexpectedly, run:
npx @11ty/eleventy --dry-run --output=json > cascade.json
Open cascade.json and search for your template's inputPath. The data object shows the final merged result—every layer applied. This is the authoritative view; no guessing required.
Works on Eleventy ≥2.0.0 (cascade behavior stable since v0.11.0). Check your version with npx @11ty/eleventy --version.
Next time you copy front matter across files
Stop. Add a directory data file instead. Let the cascade do the inheritance. Your future self—debugging at 2 AM with --dry-run --output=json—will thank you.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.