This is a living document!
What follows is my mental model of how Eleventy aggregates data for templates. It's subject to change as I learn more and more about Eleventy, and as Eleventy itself changes.
This post walks through the data cascade as of the 2.0 release. You can skip to the changes that came with 1.0, or the pre-1.0 version of this article has been archived if you need that instead!
Introduction
Last summer, I overhauled my blog, rebuilding it from the ground up with a static site generator called Eleventy. I had just come off the heels of taking Andy Bell's Learn Eleventy From Scratch course, and I was feeling jazzed about being able to build lightweight sites.
There was just one thing, one piece of Eleventy, that took me months to fully wrap my head around: the data cascade.
Eleventy is powered by templating. You can inject data into your contents and layouts using various templating languages. For instance, say your blogpost has some title
data. You could use that title
in your layouts!
<!DOCTYPE html>
<html lang="en">
<head>
<title>{{ title }} | Ben Myers</title>
</head>
<body>
<main>
<h1>{{ title }}</h1>
{{ content }}
</main>
</body>
</html>
When Eleventy generates the pages of your site, it aggregates data supplied from several places and then injects that data into your contents. The process of aggregating this data from each of these different places and deciding which data should have precedence in the case of a conflict is what Eleventy calls the data cascade.
For several months, I didn't feel like I had a good grip on the cascade. I had numerous questions: How would I know whether data was available to me at any given moment? Where could I use data? Which data would override which other data? Why should I place some data here, and some other data there?
I had to read Eleventy's docs about data several times, and then put it into practice on several different sites in several different ways. I'm especially grateful for the Lunch.dev Community Calendar project, which has been built out over several live group sessions. You can practically see the moment the cascade clicked for me in our session on "Add to Calendar" links with computed data.
What follows is my mental model of Eleventy's data cascade, presented in the hopes that it will help you wrap your head around where you can place data in your Eleventy sites and why.
A Few Definitions
- Templates are the files that define your contents. In a blog, for instance, this could be the Markdown file that contains your blogpost.
- Layouts are templates that wrap around other templates. You could, for instance, wrap your blogpost's template in a layout that provides the page's HTML scaffold and its styles.
- Data is provided to your templates (and, therefore, to your layouts as well) as variables that can be injected into your contents. Each template is supplied its own data, based on the data cascade.
Colocation
While I was ambling along with the data cascade, able to define and use data but not totally sure why and how it worked, I built up a bit of an intuition about how it worked: colocation. Data that is defined closer to your content will be evaluated later in the data cascade, and will have a higher precedence.
I'm happy to report that this holds up! Even if you don't totally understand how the data cascade works, you can debug your data first by looking at a template's frontmatter, and then working your way out.
Step 1: Global Data
The first data to be evaluated is global data. Global data is available in every template and layout, but it has the weakest precedence—it'll be overruled by any more-specific data that gets evaluated later. This makes it really ideal for site-wide concerns, as well as for data that needs to be fetched from external sources such as APIs.
Eleventy provides two ways to supply global data: global data files and through your Eleventy config file. In this first step of the data cascade, Eleventy looks at global data defined through global data files. By default, Eleventy will look for a folder at the root level of your project called _data/
. This is your global data folder. You can configure your global data folder's path in your Eleventy configuration file if you want, but I tend not to. The default works just fine for me.
Eleventy will look for all *.js
and *.json
files in your global data folder, and expose their exports to your templates, using the global data file's name as the variable name. For instance, this site has a _data/navigationLinks.json
global data file that looks like this:
[
{"text": "About", "url": "/about/"},
{"text": "Twitch", "url": "https://www.twitch.tv/SomeAnticsDev"},
{"text": "Twitter", "url": "https://www.twitter.com/BenDMyers"}
]
Eleventy takes that JSON array and exposes it for me as the navigationLinks
variable in every one of my templates and layouts. In one of my layouts, I iterate over that navigationLinks
variable to populate the page's <nav>
:
<nav>
{% for link in navigationLinks %}
<a href="{{ link.url }}">
{{ link.text }}
</a>
{% endfor %}
</nav>
Global data was a good fit for defining my navigation links because navbars tend to be a site-wide concern. Even if I do decide to have separate navbar contents on a particular page down the road, it still makes sense to define a sensible global default and override the specifics closer to that particular template.
In addition to JSON global data files, Eleventy supports JavaScript global data files, in which the whole Node.js ecosystem is your oyster. This could be useful, for instance, if you want to fetch any data from external APIs, or expose Node.js environment variables so that your templates know whether the site is being built for production or for development.
One recent use case I had for JavaScript global data was building a contributors page for the Lunch.dev Community Calendar. The repository had an .all-contributorsrc
file, generated by the All Contributors GitHub bot, but because of the repository structure, that data was totally outside Eleventy's data cascade.
I created a file called _data/contributors.js
, and in it, I used Node.js's fs
module to read and parse the .all-contributorsrc
file from the filesystem, and then export its contents:
const fs = require('fs');
const data = fs.readFileSync(`${process.cwd()}/.all-contributorsrc`, 'utf-8');
const {contributors} = JSON.parse(data);
contributors.sort((left, right) => left.name.localeCompare(right.name));
module.exports = contributors;
Then, as I was building the /contributors
route, I was able to iterate over that contributors
variable:
{% for contributor in contributors %}
<article aria-labelledby="h-{{ contributor.login }}">
<img src="{{ contributor.avatar_url }}" alt="" />
<h2 id="h-{{ contributor.login }}">
<a href="{{ contributor.profile }}">{{ contributor.name }}</a>
</h2>
<ul>
{% for contribution in contributor.contributions %}
<li>{{ contribution }}</li>
{% endfor %}
</ul>
</article>
{% endfor %}
I think this just goes to show that the whole Node.js ecosystem is fair game in JavaScript global data files.
Step 2: Config Global Data
The second type of global data, config global data (added in 1.0), allows you to inject global data into the cascade in your Eleventy config file. Like global data defined in global data files, config global data will apply to every template and layout in your project. However, config global data has a higher precedence than global data defined via global data files.
To set up config global data, go to your Eleventy config file (which will be called .eleventy.js
or something like eleventy.config.js
) and call your Eleventy config's addGlobalData
method:
module.exports = function (eleventyConfig) {
eleventyConfig.addGlobalData('siteUrl', 'https://benmyers.dev');
// The rest of your configuration
return {
// ...
};
}
The first argument passed to addGlobalData
is the data property's name, and the second is its value.
Alternatively, you can pass a function (even an async function!) in place of the value, and the data property will be set to whatever that function returns. For instance, you could get info about a given GitHub repository:
const fetch = require('node-fetch');
module.exports = function (eleventyConfig) {
eleventyConfig.addGlobalData('repo', async () => fetch('https://api.github.com/repos/11ty/eleventy'));
// The rest of your configuration
return {
// ...
};
}
Then throughout your project, you could access your repository's information with the repo
property:
<ul>
<li>{{ repo.stargazers_count }} stars</li>
<li>{{ repo.open_issues_count }} open issues</li>
</ul>
You'd use config global data for pretty much the same reasons you'd use global data files for — the only difference being you don't have to scaffold out a global data file to get global data into your project.
So why introduce a second way to provide global data? As convenient as config global data can be, it's not a feature that's really intended for us to use directly in our config — though we totally can! Its intended purpose is to give Eleventy plugins an avenue to inject their own data into the cascade. This opens up worlds of possibilities for plugins to, for instance, integrate content management systems or other external data into your Eleventy workflow.
Step 3: Layout Frontmatter
The next step in the data cascade is layout frontmatter. Layout frontmatter is defined at the top of your layout file, inside a block marked with ---
:
---
title: 'Ben Myers'
socialImage: '/assets/default-social-image.png'
---
<!DOCTYPE html>
<html lang="en">
<head>
<meta property="og:image" content="{{ socialImage }}" />
<title>{{ title }}</title>
</head>
…
From what I can tell, layout frontmatter is best used when a page needs some information—like a path to a social image—and nine times out of ten, you plan to provide a specific value for that information in your templates. You need a fallback on the off chance that you don't supply that information. It's a fallback designed to be overridden.
Prior to 1.0, layout frontmatter came between template data files and template frontmatter.
Step 4: Directory Data Files
Many Eleventy sites use the repository's directory structure to group similar content. For instance, I have a blog/
directory for articles such as this one, and a talks/
directory for presentations I've given.
If your site uses directories to organize your templates like this, you might find yourself wanting some data to apply to every template in your directory at once. A super common use case for this would be applying a default layout to all templates in a directory, like maybe applying a blogpost.html
layout to every template in blog/
. Another use case might be formatting permalinks across the board.
This is what directory data files are for. To make a directory data file for our blog/
directory, we create a new file inside blog/
and call it one of the following names:
/blog/blog.json
/blog/blog.11tydata.json
/blog/blog.11tydata.js
Notice how the filename matches its directory name—this is how Eleventy knows that this JSON or JavaScript file contains directory data. (Also, that 11tydata
suffix is configurable if you like)
Your directory data file should contain/export an object, such as:
{
"layout": "blogpost.html"
}
It's worth noting that directory data files apply to subdirectories, too. If those subdirectories have their own directory data files, the subdirectories' data files overrule the parent directories' data files, thanks to colocation.
In general, I use directory data files to set up sensible defaults across content of a certain kind—defaults that any individual template in the set can override if need be, but which generally hold up across the board.
Step 5: Template Data Files
Just as you can use a data file to define data that applies across an entire directory, you can create a template data file that supplies data for an individual template. For instance, if I wanted to supply data specifically for my /blog/in-with-the-new.md
template, I could create a file called:
/blog/in-with-the-new.json
/blog/in-with-the-new.11tydata.json
/blog/in-with-the-new.11tydata.js
A template data file must live in the same directory as the template and it must have the same name as the template, barring the file extension.
As with directory data files, template data files must contain/export an object, whose properties define the data that should get added to the cascade.
{
"title": "Out With The Old, In With The New",
"date": "2020-08-16",
"description": "How and why I rebuilt my blog from the ground up with Eleventy.",
"socialImage": "/assets/covers/in-with-the-new.png"
}
These fields will overrule fields from global or directory data, since template data files target individual templates and are much more colocated with specific content.
At first, I was surprised to find that template data files existed, since I use template frontmatter for template-specific data. Template data files seem great if you prefer to separate your content from your content's metadata, but I personally prefer to have fewer files and more colocation.
However, I've recently found a use case where I really love template data files: supplying serverless templates with data. Thanks to Eleventy Serverless, introduced in 1.0, one template can be used to generate dynamic pages at request time. When I need to work with serverless data, I find template data files (combined with computed data discussed below) easier and more flexible to use than template frontmatter. For more information about using template data files and computed data with Eleventy Serverless, see my 11ties talk about building a serverless color contrast checker!
Step 6: Template Frontmatter
Up next is template frontmatter. Like layout frontmatter, template frontmatter is defined at the top of the template file, delineated with ---
:
---
title: 'Out With The Old, In With The New'
date: 2020-08-16
description: 'How and why I rebuilt my blog from the ground up with Eleventy.'
socialImage: '/assets/covers/in-with-the-new.png'
---
## Introduction
This summer…
Your data can't get more specific and colocated than being declared in the same file as your content. Because of that, template frontmatter overrides global data, directory data, layout data, and data defined in template data files. This makes it a great choice for setting really content-specific data.
There's not honestly much more one can even say about template frontmatter… except that it's not the end of Eleventy's data cascade. There's still one more step to go.
Step 7: Computed Data
Recently, we implemented "Add to Calendar" links on the Lunch.dev Community Calendar. These links prepopulate an event on your Google Calendar (or other calendar app) with all of its details—its title, date, and description. We wanted Eleventy to generate those links for us based on the data we'd already provided. This ended up being the perfect use case for computed data.
Computed data is data injected at the very end of the cascade, based on all the data that was aggregated previously in the cascade. Because it's evaluated at the end of the cascade, it has the highest precedence, and will overrule data defined earlier.
To define some computed data, go to any step of the data cascade and declare an eleventyComputed
data object. As Eleventy reaches any step along the data cascade, if it notices an eleventyComputed
property, it sets that property aside to be evaluated at the end. eleventyComputed
can be a deeply nested object, and any methods inside that object will be called and their return values used as the values of the data.
In our case, we wanted every event template in our schedule/
directory to generate their own "Add to Calendar" links, so we went to the /schedule/schedule.11tydata.js
directory data file and created an eleventyComputed
property. Inside, we declared methods like googleCalendarLink()
, outlookCalendarLink()
, and so forth. These methods all receive a data
argument that receives every piece of data aggregated by the cascade so far. We were able to pull off just the properties we cared about, and generate multiple "Add to Calendar" links with the calendar-link
npm package. In all, it looked something like this:
const {google} = require('calendar-link');
const location = 'Lunch Dev Community Discord at events.lunch.dev';
const url = 'https://events.lunch.dev/discord';
module.exports = {
eleventyComputed: {
googleCalendarLink({title, description, date}) {
return google({
title,
description,
start: date,
duration: [1, 'hour'],
location,
url,
});
}
}
};
Then, in our layouts, we were able to consume the googleCalendarLink
data:
<a href="{{ googleCalendarLink }}">
Add to Google Calendar
</a>
Even though this eleventyComputed
property happens to be in a directory data file, it receives the title
, description
, and date
data that's been declared in each event's frontmatter.
(As a sidenote, computed data can depend on other computed data — Eleventy does its best to resolve that dependency tree for you!)
Changes to the Data Cascade in Eleventy 1.0
If you've leveraged the data cascade in your Eleventy projects prior to version 1.0, you might have noticed some slight changes.
The first change is purely additive: supplying config global data via addGlobalData
.
The second change involves moving layout frontmatter's place in the cascade. Prior to 1.0, layout frontmatter occupied a bizarre spot in the cascade between template data files and template frontmatter. This was a confusing exception to the cascade's principle of colocation, but since fixing it constituted a breaking change, the fix was held off until 1.0. It now occupies a spot in the cascade between config global data and directory data, which feels like a more natural fit to me.
Takeaways
Eleventy provides many ways for you to inject data into the cascade, depending on how broadly or specifically you want said data to apply. Broadly speaking, the closer your data is to your content, the more likely it is to apply.
You may or may not end up using each step of the cascade! In the sites I've built with Eleventy so far, I've predominately focused on using global data, directory data, and template frontmatter, and I've only needed to sprinkle in a little bit of computed data. This is mostly because the sites I've built haven't really needed the other steps.
This post was written with my mental model based on the sites I've built so far. I'm sure I've missed something, or there are use cases I haven't considered! If there's something I've missed, please feel free to reach out and let me know!