Decoupling Blog Logic with Eleventy Global and Directory Data
Move navigation and blog metadata out of Eleventy templates into _data files. Use global files for site-wide variables and directory-scoped data for section-specific data to keep templates clean and content-driven.
12 Jun 2026, 22:19 UTC

Hardcoding navigation, site metadata, or blog lists directly into Eleventy templates creates maintenance debt. Add a new section and you edit a Nunjucks file instead of data. The useful takeaway is to move that data out of templates into Eleventy's Data Files system, so the UI updates automatically from JSON, YAML, or JavaScript files in _data.
Eleventy parses files in _data before rendering and merges them via the data cascade. Global files at the project root become site-wide variables. Directory-scoped files limit exposure to pages in that folder. That separation keeps templates focused on presentation.
Global data for site-wide configuration
Place a _data folder at the project root. Any .json, .yaml, .yml, or .js file there is loaded once per build and exposed globally by filename.
Create _data/site.json with site metadata. Run this in the project root with write permission to the repo.
# _data/site.json
{
"siteName": "Tech Insights Blog",
"description": "Deep dives into static generators",
"nav": [
{ "label": "Home", "url": "/" },
{ "label": "Blog", "url": "/blog/" },
{ "label": "About", "url": "/about/" }
]
}
In any template, e.g. src/_includes/base.njk, reference it as {{ site }}.
<header>
<h1>{{ site.siteName }}</h1>
<nav>
{% for item in site.nav %}
<a href="{{ item.url }}">{{ item.label }}</a>
{% endfor %}
</nav>
</header>
Check the result by running npx eleventy --serve from the project root and inspecting the rendered header. If site is undefined, verify the file is named site.json not site.JSON and sits in the root _data folder.
Directory-scoped data for section-specific variables
Data does not have to be global. Eleventy supports scoped data by matching a data file name to a directory. A file named posts.json inside _data is merged into pages under src/posts/.
This avoids polluting the global namespace with variables only used in one section.
# _data/posts.json
{
"sectionTitle": "Blog Posts",
"showExcerpt": true
}
Now src/posts/index.njk can use {{ sectionTitle }} while src/about/index.njk cannot. The practical check is to render both pages and confirm sectionTitle is present only in the posts section.
Worked example: a dynamic tag list from a JS data file
JavaScript data files allow pre-processing before the build. Create _data/tags.js at the project root to derive a unique tag list from a static array, avoiding hardcoding.
// _data/tags.js
module.exports = function() {
const posts = [
{ tags: ["eleventy","ssg"] },
{ tags: ["ssg","data"] }
];
const tagSet = new Set();
posts.forEach(p => (p.tags || []).forEach(t => tagSet.add(t)));
return Array.from(tagSet).sort();
};
In a template:
<ul>
{% for tag in tags %}
<li>{{ tag }}</li>
{% endfor %}
</ul>
Risk: JavaScript data files run on every build. If you fetch external APIs here, network failures or slow responses will block the build. Keep external calls idempotent and consider caching.
Trade-offs and limitations
Naming collisions. Front matter on a page overrides a data file with the same key. If a Markdown file defines tags in front matter, it will shadow a global tags data file for that page only. Verify by printing both values during development.
Build performance. Large JSON files are parsed for the whole build. A multi-megabyte _data file increases memory use and startup time. Split large datasets into smaller scoped files or generate them with a JS data file that returns only needed fields.
Data cascade order is deterministic but subtle: page front matter > directory data > global data. When debugging missing values, inspect the data cascade in Eleventy's debug output.
Start with a global site.json for navigation and metadata, add directory-scoped files for section settings, and use JS data files only when transformation is needed. That keeps templates clean and content-driven.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.