My avatar. Arnaught A pride flag.

This Is An Eleventy Blog Now!

August 25, 2024 | Tags: personal programming


I have migrated this blog to Eleventy[1]! There wasn’t really a major reason why. It’s just that Jekyll keeps randomly breaking on me, and that’s a little annoying.

Migration wasn’t too bad. I largely followed along with this blog post, which covers some basic things to get Eleventy to behave a little more like Jekyll, such as the post_url shortcode and markdown footnotes and heading links. Heading anchors[2] are something I wanted to set up in Jekyll before, but I couldn’t find an easy way to do it.

Include Syntax #

I use {% include %} liquid tags to include components in the blog like custom emotes, images, and iframes. If I wanted to include an emote (like this:neocat:), I would type:

{%- include emote name="neocat" -%}

However, Eleventy has slightly different syntax for includes. The include name must be in quotes, and parameters are assigned with key: value instead of key=value. So I renamed _includes/emote to _includes/emote.liquid and I now I include emotes like this:

{%- include 'emote', name: "neocat" -%}

Finally, the way includes receive the parameters is also a little different. Originally, _includes/emote looked like this:

<img src="/assets/emotes/{{ include.name }}.png" alt=":{{ include.name }}:" title=":{{ include.name }}:" class="emote">

However, with Eleventy, include isn’t needed in the parameter. So the new _includes/emote.liquid instead is:

<img src="/assets/emotes/{{ name }}.png" alt=":{{ name }}:" title=":{{ name }}:" class="emote">

This is also true in some other places as well. For example, I needed to change the <title> tag in _layouts/default.html from <title>{{ page.title }}</title> to <title>{{ page.title }}</title>

Tagging and Archives #

With Jekyll, I used jekyll-archives to generate archive pages, like /blog, /blog/2023[3], and /tagged/programming. It is unfortunately a little janky. For example, it doesn’t give titles correctly. Rather than naming the page List of posts tagged "$TagName" or something similar, they just get the name $TagName all in lowercase.

Eleventy provides an easy way to get tag pages with a single Nunjucks[4] page.

---
pagination:
  data: collections
  size: 1
  alias: tag
  filter:
    - posts
    - all
    - drafts
permalink: /tagged/{{ tag }}/
layout: default
eleventyComputed:
    title: List of posts tagged "{{ tag }}"
---

<h1>List of posts tagged "{{ tag }}"</h1>

<ul>
{% set taglist = collections[ tag ] %}
{% for post in taglist | reverse %}
  {% if post in collections.posts %}
  <li><a href="{{ post.url }}">{{ post.data.title }}</a> - {{ post.data.date | toReadableDate }}</li>
  {% endif %}
{% endfor %}
</ul>

There’s a couple modifications in my version from the one in the example. First, I check if the post is in collections.post, because I ran into a weird issue where it was including all pages in the tag lists, not just blog posts. I also include the date and pass it through the toReadableDate filter I made.

const monthText = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
function toReadableDate(date) {
    const year = date.getUTCFullYear().toString();
    const month = monthText[date.getUTCMonth()];
    const day = date.getUTCDate().toString();
    return `${month} ${day}, ${year}`;
}

module.exports = function(eleventyConfig) {
    // [...]

    eleventyConfig.addLiquidFilter("toReadableDate", toReadableDate);
    eleventyConfig.addNunjucksFilter("toReadableDate", toReadableDate);

    // [...]
}

Basically, you can add custom filters to Nunjucks and liquid by writing functions in JavaScript. This could be used for more fine control over how included data is handled. I’m sure I could use this to improve some of my current _includes.

eleventyComputed is the property that lets you set the title for each generated page.

I also made a page with a list of all tags (available at /tagged), which jekyll-archives didn’t support.

---
permalink: /tagged/
type: tag
layout: default
title: Tags
---

<h1>List of Tags</h1>

<ul>
{% set tags = collections | keys | sort %}
{% for tag in tags %}
    <li><a href="/tagged/{{ tag }}">{{ tag }}</a></li>
{% endfor %}
</ul>

And a page with the list of all blog posts (available at /blog).

---
eleventyImport:
    collections: ["posts"]
permalink: /blog/
layout: default
title: All Blog Posts
---

<h1>All Blog Posts</h1>

<ul>
{% for post in collections.posts | reverse %}
    <li><a href="{{ post.url }}">{{ post.data.title }}</a> - {{ post.data.date | toReadableDate }}</li>
{% endfor %}
</ul>

The one thing I didn’t get working is the date based archive pages, like /blog/2023 or /blog/2023/11 I couldn’t find an easy way to make one, either by hand or with a plugin. Honestly, it’s not really that big of a loss. I don’t post super often, so most of the date pages only had one or two links on them. Also, I doubt most people noticed they were there.

RSS Feeds #

RSS feeds are pretty simple to set up with Eleventy, using eleventy-plugin-rss. There was one little snag: the current version of eleventy-plugin-rss requires a beta version of Eleventy by mistake, so I need to be on the Eleventy beta.

Off By One #

In that blog post I mentioned I was following earlier, it mentions using data directory files to set a permalink for posts. Following this, I set my _posts/_posts.json to:

{
    "layout": "post",
    "permalink": "{{ page.date | date: '%Y/%m/%d' }}/{{ page.fileSlug }}.html"
}

This works great and matches what I had with Jekyll. Except I fell for a common pitfall! Eleventy keeps dates in UTC, but the liquid date filter formats it in local time. This means that depending on your time zone, dates might appear off by one!

The way to fix this is to create a custom filter to format the date in UTC and changing the permalink to use this filter instead:

eleventyConfig.addLiquidFilter("toUTCDatePermalink", (date) => {
    const year = date.getUTCFullYear().toString();
    const month = (date.getUTCMonth() + 1).toString().padStart(2, "0");
    const day = date.getUTCDate().toString().padStart(2, "0");
    return `${year}/${month}/${day}`;
})
{
    "layout": "post",
    "permalink": "{{ page.date | toUTCDatePermalink }}/{{ page.fileSlug }}.html"
}

Syntax Highlighting #

The syntax highlighting plugin uses Prism.js to handle highlighting for code blocks, as opposed to Jekyll which used Pygments. I needed to change some of the CSS for code blocks, and replace my Pygments CSS file with a Prism.js one (so the colors are different), but other than that, getting syntax highlighting working was easy.

Drafts #

Eleventy has a guide for allowing draft posts. Once set up, by putting a draft post in _drafts and draft: true in the front matter, I can have draft posts be included when building with --watch or --serve, while not including them in the published build. That was always a pain point for me with Jekyll. I want to draft a post before I publish it and see it in the browser to make sure I didn’t screw up the formatting. I had to move the drafts into _posts when working on it, and remember to put it back in _drafts before publishing.[5]

But with this, I can just keep drafts separately and only move them into _posts when I’m ready to publish!

Conclusion #

Anyway, that’s basically everything notable about my switch to Eleventy. Hopefully my Eleventy config won’t break as much as my Jekyll config did. And hopefully, this motivates me to work on some more blog posts!

Footnotes #


  1. See also: Making A Jekyll Blog With Neocities ↩︎

  2. The little # after the headings that links to a specific heading. ↩︎

  3. This is a dead link now, FYI ↩︎

  4. I don’t know if it needs to be nunjucks. You could probably do it in liquid, but the only example given is in nunjucks so 🤷 ↩︎

  5. Surely there has to be a better way to do this with Jekyll. I just don’t know how. ↩︎