Slotted content in Eleventy

Some types of template data are awkward to maintain in any of the many places from where Eleventy can read it. Markdown's front-matter data can hold simple pieces of information just fine, but becomes unwieldy for rich content.

If you've ever wanted to art-direct individual pages with custom styles defined inline, I'm sure you're not exactly thrilled with front-loading a wall of CSS-in-YAML:

---
title: I wrote this on my portable typewriter
custom_style: |
<style type='text/css'>
body { font-family: monospace; }
</style>

---

The front-matter approach is workable, but has some drawbacks:

One solution to these annoyances, of which I'll try to convince you in this article, is to embed pieces of data in the content part of the Markdown file, that can then be referenced by name from the HTML layout, just like any other template data.

Adding slotted content to Eleventy

Say you're making a recipe website, and you want to put the ingredients and the instructions in different places in the HTML layout, without cluttering each Markdown file with presentational markup.

Let's create a {% slot %} shortcode to define the recipe parts:

---
title: 'Best pesto of your life'
layout: layouts/recipe.njk

---


{% slot 'ingredients' %}

* fresh basil
* pine nuts
* olive oil
* pecorino
* garlic clove
* salt

{% endslot %}

{% slot 'instructions' %}

1. Wash the basil leaves
2. Grate the pecorino
3. ...

{% endslot %}

...that we can then place in the appropriate spots in the HTML layout:


<article>
<h1>{{ title }}</h1>
<div class='layout'>
<div class='ingredients'>
<h2>Ingredients</h3>
{{ slots.ingredients | safe }}
</div>
<div class='instructions'>
<h2>Instructions</h3>
{{ slots.instructions | safe }}
</div>
</div>
</article>

Step 1: Add slots data to each page

We're going to store all slots for all pages in the slots object, which acts as a map-of-maps keyed by the page's inputPath. Each page has access to its own slots via Eleventy's Computed Data feature:

/* .eleventy.js */
module.exports = function(config) {
const slots = {};
config.addGlobalData('eleventyComputed.slots', function() {
return data => {
const key = data.page.inputPath;
slots[key] = slots[key] || {};
return slots[key];
}
});
};

Step 2: Register the slot shortcode

To pick up slot values from the Markdown files, we register the slot paired shortcode.

/* .eleventy.js */
module.exports = function(config) {
config.addPairedShortcode('slot', function (content, name) {
if (!name) throw new Error('Missing name for {% slot %} block!');
slots[this.page.inputPath][name] = content;
return '';
});
}

After storing the content in the appropriate slot for the page, the shortcode returns an empty string so that the content is not also rendered inline in the Markdown file.

The content of {% slot 'ingredients' %} can then be used as {{ slots.ingredients }} anywhere in the HTML layout.

The throw clause at the beginning guards against a common error. You may expect {% slot ingredients %} to define a slot named 'ingredients', but ingredients in the context of an Eleventy shortcode is an identifier, and what the shortcode gets as the name argument is the value bound to that identifier. The slot name needs to be quoted as {% slot 'ingredients' %}. Checking for an empty name doesn't make the shortcode error-proof, but it will cover this frequent typo.

Step 3: Use the Eleventy Render plugin

The implementation so far assigns pieces of content to their respective slots as plain text. Any Markdown style in slot content will be used literally in the HTML output. To render slots the same way as the rest of the content, we need to use Eleventy's Render plugin, which registers a helpful renderTemplate shortcode.

/* .eleventy.js */
const { EleventyRenderPlugin } = require('@11ty/eleventy');

module.exports = function(config) {
config.addPlugin(EleventyRenderPlugin);
};

Change the HTML layout to render slot content as Nunjucks + Markdown and you're good to go:

<!-- Before: --> 
<div class='ingredients'>
<h2>Ingredients</h3>
{{ slots.instructions }}
</div>

<!-- After: -->
<div class='ingredients'>
<h2>Ingredients</h3>
{% renderTemplate 'njk,md', slots %}
{{ ingredients | safe }}
{% endrenderTemplate %}
</div>

Note that by default content inside the renderTemplate shortcode only has access to the page and eleventy objects, so slots needs to be passed in as additional data.

Conclusion

Nunjucks, like other templating languages of Jinja lineage, technically comes with template inheritance via the block tag. It would make a lot of sense to define slotted content with named blocks. However, Nunjuck's template inheritance only applies to templates that extend other templates, and a Markdown file rendered with Nunjucks does not extend its layout file.

In the absence of the built-in template inheritance, this is the most concise implementation for Markdown slotted content in Eleventy I could come up with lift wholesale from eleventy-plugin-bundle. If it can be further simplified, I'd love to know.