Eleventy Tricks
I recently moved this site away from Hugo and onto Eleventy. Given my simple needs, I was attracted to Eleventy because it too is simple and flexible. Unfortunately, those very positive traits mean that developers may need to invent a few features for themselves — one size does not fit all, after all.
This page collects some of the tricks, and snippets I ended up with for this site to function as I wanted it to. There may be better ways, but these worked for me.
Global Data
I like to define "variables" for things I refer to often across the site. I chose to define these in .eleventy.js
as global data:
module.exports = function (eleventyConfig) {
// Global data
eleventyConfig.addGlobalData('site.url', absoluteUrl)
eleventyConfig.addGlobalData('site.author.name', 'Jamie Schembri')
eleventyConfig.addGlobalData('site.language', 'en-GB')
eleventyConfig.addGlobalData('site.author.email', 'jamie@schembri.me')
eleventyConfig.addGlobalData('site.title', 'The Chronicles of Jamie')
eleventyConfig.addGlobalData('site.description', 'Jamie\'s little corner of the Web.')
eleventyConfig.addGlobalData('site.copyright', 'Copyright 2021 Jamie Schembri, CC BY-NC 4.0')
eleventyConfig.addGlobalData('site.image', 'schembri.jpg')
//other stuff
}
These are then accessible in your templates, via e.g.:
{{ site.url }}
Global data is a big concept in Eleventy, so be sure to read the docs.
Excerpts
In Hugo, I used the <!--more->
comment to delimit summary data from the rest of the content. Getting this working in Eleventy is pretty easy:
eleventyConfig.setFrontMatterParsingOptions({
excerpt: true,
excerpt_separator: '<!--more->',
excerpt_alias: 'excerpt'
})
Notably, setting the excerpt_alias
provides the value as a variable to your templates. Thus, in your layout you can print the excerpt content with:
{{ excerpt }}
More info at the Eleventy docs.
Meta tag generation
Perhaps you'd like to override meta tags per post/page. I find this useful for Open Graph data.
Add this to your template, somewhere in head. I'm using liquid in this example:
{% for property in meta %}
<meta property="{{ property[0] }}" content="{{ property[1] }}">
{% endfor %}
This will read each item in the meta
object and output it as a meta tag, allowing us to do something like this in page frontmatter
---
meta:
"profile:first_name": "Jamie"
"profile:last_name": "Schembri"
"profile:username": "shkm"
"profile:gender": "male"
---
Per-page images (Open Graph)
It's quite common to display an image for each post on your site. I personally opted not to do that, but I still wanted auto-generated link cards that we see on pretty much anything these days (Slack, Twitter, Discord, etc.) to potentially have a unique image per post. This means adding an og:image
per page. Strictly speaking, this should be an absolute URL.
But it should also fall back if there isn't such an image for a page. So this turned out to be a little tricky.
In .eleventy.js
:
// replace the default URL with your own
const absoluteUrl = process.env.URL || 'https://schembri.me/'
const toAbsoluteUrl = (path) => {
return new URL(path, absoluteUrl).href
}
// Hack: requires passing in data. See
// https://github.com/11ty/eleventy/issues/1154
eleventyConfig.addShortcode("pageImage", function(data) {
// It's page-relative if there is an image for the page.
if (data.image && !data.image.startsWith('/')) {
return(toAbsoluteUrl(this.page.url + data.image))
}
return toAbsoluteUrl(data.image || eleventyConfig.globalData['site.image'])
})
First we want a way to fetch our absolute URL. Ideally you can override this for local testing, or testing Open Graph data with something like ngrok (you might want dotenv for this).
We then add a shortcode, pageImage
, which takes a parameter — data. See the linked issue to track the parameter not being a requirement.
This also requires that we expose data. I took to using eleventyComputed
in a file named after my input directory, _content
.
So, in _content/_content.11tydata.js
:
module.exports = {
eleventyComputed: { data: (data) => data }
}
Finally, let's use pageImage
in the template to produce the Open Graph image:
<meta property="og:image" content="{% pageImage data %}">
Simple CSS Bundling
I had a couple of CSS files and wanted to serve them all simply, as a single bundled CSS file. There are thousands of asset bundlers out there, but we can manage without any additional dependencies.
Create a file in the input directory; I called mine bundle.css.liquid
, reflecting that it's processed by liquid but ultimately a CSS file:
---
permalink: /bundle.css
---
{% capture css %}
{% include "concrete.css/concrete.css" %}
{% include "highlight.js/styles/xcode.css" %}
@media (prefers-color-scheme: dark) {
{% include "highlight.js/styles/tokyo-night-dark.css" %}
}
{% include "stylesheets/main.css" %}
{% endcapture %}
{{ css | cssmin }}
First we have frontmatter, dictating that the "output" file should be /bundle.css
.
After that we do some liquid magic: capture the content of this block into a variable named css
, and, inside the block, import the CSS files. This includes files from within node_modules
, so it's easy to include CSS from elsewhere.
Finally, we spit out the css on the last line and pass it through a custom cssmin
filter. Minification is of course optional, but is detailed in the following subsection.
Minifying it with CleanCSS
Add the package:
npm add --save-dev
Add the filter in .eleventy.js
:
eleventyConfig.addFilter('cssmin', function (code) {
return new CleanCSS({}).minify(code).styles
})
Overriding the template language
I ran into this when writing this post. I wanted to output liquid code, but it was getting swallowed. It is in fact noted in the official Eleventy docs on Markdown that Markdown is, by default, pre-processed with Liquid.
Oops.
Fixing this on a per-post basis is as simple as overriding the template engine to be just markdown in the frontmatter:
templateEngineOverride: md