Go to content

Migration from Jekyll to Eleventy

Published on

Jekyll and Eleventy are static site generators. Until recently, this site was generated via Jekyll. Jekyll did the job, but for some reason I’m always struggling with Ruby, the language it’s built on. During the build it cannot find the right Ruby version, gems (dependencies) are installed in a different version or Bundler (a dependency manager) raises issues, or complains about different versions. Jekyll was slow on my machine, but it had improved the rendering a lot in the latest major update. Maybe a MacBook Air isn’t meant for development, but generating a bunch of HTML files is not like mining Bitcoins, is it?

When I searched for alternatives, I ran into Eleventy. Eleventy is a static site generator built with JavaScript. I decided to give it a try. I wanted to keep the existing functionality on the same URLs. This post describes how I have accomplished that.


The majority of the steps have been described in Turn Jekyll up to Eleventy. That was the starting point for the migration, and it solved the vast majority of my questions. After I had followed the instructions, all the blogposts were generated. With the built in pagination there was a complete archive again.


While Jekyll has built-in support for SASS, Eleventy does not. Luckily there’s an Eleventy SASS plugin for that. Jekyll expects that the main .scss file starts with some frontmatter, which makes all editors complain for SASS. For the Eleventy SASS plugin you must remove this frontmatter which makes the .scss files valid again.

The plugin for syntax highlighting takes care of the code blocks inside the posts.

Archive per year and month

My blog pages follow the URL structure: /blogposts/year/month/name-of-the-post.html. All the posts already contained a permalink for Jekyll to keep using the URLs that probably date back to… 2007?! For every year and every month there’s an overview page with the blogposts. It took a while to figure out how to create those. The Github issue Collections for directory indexes gave many clues, but it ended with “[UPDATE] I’ve managed to do it, with a template for months and another for years!”. Okay, so I need two templates. BUT WHAT DO THEY CONTAIN?!?!?!!

First I had added a file contentByDate.js with the contents of this issue comment on collections by date.

Then these collections are exposed in eleventy.js:

const monthsCollection = require("./_utils/contentByDate").contentByMonth;
const yearsCollection = require("./_utils/contentByDate").contentByYear;

module.exports = eleventyConfig => {
  eleventyConfig.addCollection("contentByYear", yearsCollection);
  eleventyConfig.addCollection("contentByMonth", monthsCollection);
  // …

This creates two collections: one for each year and one for each month. The URL structure is defined as permalink: "/blogposts/{{ year | date: '%Y' }}/index.html". The last part of the puzzle: how does Eleventy create a page for each year? The solution was: use pagination over that collection, with a page size of 1. Maybe there’s a more efficient way to solve this, but I couldn’t find it.

The template for each year, /archive-year.html:

layout: default
sitemap: false
eleventyExcludeFromCollections: true
    data: collections.contentByYear
    size: 1
    alias: year
permalink: "/blogposts/{{ year | date: '%Y' }}/index.html"
<div class="archive">
    <h1>Articles written in {{ year | date: '%Y' }}</h1>
    {% include "archive-list.html", posts: collections.contentByYear[year] %}

After finding the solution for each year, the archive per month was merely the same. It uses a different collection and a slightly different permalink, but the solution is similar.

The template for each month, /archive-month.html:

layout: default
sitemap: false
eleventyExcludeFromCollections: true
    data: collections.contentByMonth
    size: 1
    alias: month
permalink: "/blogposts/{{ month | date: '%Y/%m' }}/index.html"
<div class="archive">
    <h1>Articles written in {{ month | date: '%B %Y' }}</h1>
    {% include "archive-list.html", posts: collections.contentByMonth[month] %}

The list itself is a fragment _includes/archive-list.html:

<ul class="archive-list" id="articles" aria-label="Articles">
    {% for post in posts %}
        <h2 class="title"><a href="{{ post.url | prepend: site.baseurl }}" rel="bookmark">{{ post.data.title }}</a></h2>
        <p class="meta"><span>Published on </span>
            <time datetime="{{ post.date | date: '%Y-%m-%dT%H:%M:%S%z' }}">{{ post.date | date: "%b %d, %Y" }}</time>
        <p class="excerpt">
            {{ post.templateContent | strip_html | strip_newlines | truncate: 250 }}
    {% endfor %}


The RSS plugin came in handy for my RSS feed. My feed does not contain the entire post as HTML, but an excerpt as plain text. The libraries string-strip-html and html-entities convert the HTML into plain text.

Configuration in eleventy.js:

const stripHtml = require("string-strip-html");
const HtmlEntities = require('html-entities');
const htmlEncoder = new HtmlEntities.AllHtmlEntities();

module.exports = eleventyConfig => {
  eleventyConfig.addNunjucksFilter("rssItemTitle", htmlString => {
    return htmlEncoder.decode(htmlString);

  eleventyConfig.addNunjucksFilter("rssItemDescription", htmlString => {
    return htmlEncoder.decode(stripHtml(htmlString)).replace(/(\r\n|\n|\r)/gm, '').substring(0, 250);

  eleventyConfig.addNunjucksFilter("rssPubDate", collection => {
    if (!collection || !collection.length) {
      throw new Error("Collection is empty in rssPubDate filter.");

    // Newest date in the collection
    return collection[collection.length - 1].date.toUTCString();
  // …

The template for the RSS feed, /rss.njk:

permalink: rss.xml
eleventyExcludeFromCollections: true
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
    <title>{{ site.title }}</title>
    <atom:link href="{{site.url}}{{site.baseUrl}}/rss.xml" rel="self" type="application/rss+xml"/>
    <link>{{ site.url }}{{ site.baseurl }}</link>
    <pubDate>{{ collections.posts | rssPubDate }}</pubDate>
    <description>RSS feed for jasha.eu</description>
    {%- for post in collections.posts | reverse %}
    {% set absolutePostUrl %}{{ post.url | url | absoluteUrl(site.url) }}{% endset %}
        <title><![CDATA[{{ post.data.title | rssItemTitle | safe }}]]></title>
        <link>{{ absolutePostUrl }}</link>
        <pubDate>{{ post.date.toUTCString() }}</pubDate>
        <dc:creator><![CDATA[{{ post.data.author | safe }}]]></dc:creator>
        {% for tag in post.data.tags %}
        <category><![CDATA[{{ tag | safe }}]]></category>
        {% endfor %}
        {% if post.data.guid %}
        <guid isPermaLink="false">{{ post.data.guid }}</guid>
        {% else %}
        <guid isPermaLink="true">{{ absolutePostUrl }}</guid>
        {% endif %}
        <description><![CDATA[{{ post.templateContent | rssItemDescription | safe }}]]></description>
    {%- endfor %}



For the sitemap I had found some clues in a Github issue. With some copy-paste-adapt it’s working. The sitemap uses the collection "all" to list every page on the site. It utilises the rssDate from the RSS plugin.

The template for the sitemap, /sitemap.njk:

permalink: sitemap.xml
eleventyExcludeFromCollections: true
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
{% for item in collections.all | reverse %}
    <loc>{{site.url}}{{ item.url }}</loc>
    <lastmod>{{ item.date | rssDate }}</lastmod>
{% endfor %}


The migration from Jekyll to Eleventy went pretty fast. Nearly everything I had used in Jekyll was possible in Eleventy. Eleventy generates my site faster than Jekyll, although Jekyll had improved its speed a lot in the latest major update.

The only thing that I haven’t found a solution for, are drafts. In Jekyll, you add published: false to the frontmatter and it won’t be published. Eleventy doesn’t have this built in functionality, but I’ve found a few clues how to solve this. I haven’t taken the time to apply them though. As a result, I had accidentally uploaded this page before it contained useful content 😬.

Now it’s time to write content!