I Finally Understand Eleventy's Data Cascade.

Caveat

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. If you notice something that's wrong, please reach out.

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 any of 10 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 #

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, in general, 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 v1.0.0 will provide a means of defining global data in your .eleventy.js configuration file, intended mostly to be used by plugins. In the meantime, there's only one way to define global data: 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.js 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://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: 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:

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 3: 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:

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.

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.

Step 4: 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.

The fact that layout frontmatter occupies an intermediary precedence between template data files and template frontmatter is odd, and throws a wrench in understanding the cascade in terms of colocation. After all, template data files and template frontmatter can only apply to individual templates, whereas layouts can impact multiple templates. This is a known hiccup, and is planned to be resolved in v1.0 of Eleventy. After v1.0 is released, layout frontmatter will likely come between global data and directory data in terms of precedence.

Step 5: Template Frontmatter #

We resume your regularly scheduled colocation to bring you 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 6: 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!)


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!