Migrating from Jekyll to Eleventy

Yes, this is going to be yet another one of those articles explaining how I migrated this blog from Jekyll to Eleventy. You've been warned.

Why?

I don't really have issues with Jekyll and I've been using it for 10 years now here, but I haven't really chosen Jekyll: it's been more-or-less imposed on me by GitHub Pages. But GitHub now has added the possibility to deploy using a custom GitHub Actions workflow, and this is game-changer!

I could have kept using Jekyll with unlocked possibilities, but I'm not a Rubyist, that's just not a language I'm comfortable with, and I know almost nothing about Gems, so definitely not something I'd be comfortable maintaining going forward.

I also could have just kept using the built-in Jekyll Pages integration, and this is what I would have done if I hadn't found any satisfying alternative. I'm not forced to change, so at least I have a fallback in the form of the status quo.

So what would replace it? Let's evaluate my requirements.

The Requirements

The choice

The HTML-first approach rules out (a priori, correct me if I'm wrong) every React or Vue based approach, or similar.

I've quickly evaluated a couple alternatives, namely Astro and Eleventy.

Astro is fun, but I must say it doesn't really look content oriented, relegating the content into its src/pages, or worse, a subfolder inside src/content/. I really like the typesafe nature of content collections, but moving everything down to src/content/blog really hides the content away IMO. Extracting the publication date from the file name is possible, but it looks more and more like a development project rather than a content project. It's great, but not what I'm looking for here.

I then looked at Eleventy. I have to admit my first contacts with the Eleventy documentation months ago left me with a bitter taste as I couldn't really figure out how collections worked and how you were supposed (or not) to organize your files. Looking at tweetback more recently didn't really help: absolutely everything is JS, loading content from a SQLite database.

I decided to give it a chance: maybe I misunderstood the documentation the last time(s) I read it. And indeed it was the case: moving from Jekyll to Eleventy probably couldn't be easier.

How?

I felt my way a bit, so I'll summarize here what I ended up doing, also describing some things I tried along the way.

Getting Started

Removing Jekyll consists in deleting the _config.yml and possibly Gemfile (I didn't have one). Adding Eleventy means initializing a new NPM packaging and adding the @11ty/eleventy dependency (and of course adding node_modules to the .gitignore), and creating a configuration file (I chose eleventy.config.cjs rather than the .eleventy.js hidden file).

Because the deployment workflow is different, the CNAME file becomes useless and can be deleted. A new GitHub Actions workflow also has to be created, using the actions/configure-pages, actions/upload-pages-artifact, and actions/deploy-pages actions. I took inspiration from the Astro starter workflow and updated it for Eleventy.

Markdown

Eleventy supports Markdown out of the box, with all the options I needed, except syntax highlighting and heading anchors for deep linking. It also automatically extracts the date from the file name.

Syntax highlighting is as easy as using the official plugin, but then the generated HTML markup is different than with the Rouge highlighter in Jekyll, so I had to change the CSS accordingly. I ended up importing an existing theme: display would be slightly different than before, but actually probably better looking.

Deep linking requires using the markdown-it-anchor plugin, and to make sure existing deep links wouldn't break I provided my own slugify function mimicking the way CommonMarkGhPages computes the slug from the heading text (I happen to have a few headings with <code> in them, and CommonMarkGhPages would compute the slug from the rendered HTML leading to things like codejavaccode; I chose to break those few links in favor of better-looking anchor slugs). I also disabled tabIndex to keep the same rendering as previously (I'll read more on the accessibility implications and possibly revert that choice later.)

I reimplemented the post_url first as a custom short code but that meant updating all articles to quote the argument (due to how Eleventy wires things up), so I ended up using a custom tag; that's specific to the Liquid template engine (in case I would want to change later on) but at least I don't have to update the articles.

In terms of rendering, besides syntax highlighting, the only difference is the <br> which are now rendered that way rather than <br /> (there's an option in markdown-it but I'll keep the less XHTML-y, more HTML-y syntax).

The rss.xml file wouldn't be treated as a template by default, so I aliased the xml extension to the Liquid engine, and added an explicit permalink: to avoid Eleventy creating an rss.xml/index.html file. I did the same with the css extension so I could use an include to bring in the syntax-highlighting theme in my style.css.

Liquid Templating

I had to rename my layout files to use a .liquid extension rather than .html. I didn't want to move them though, so I configured a layouts directory instead.

I also had to handle all the Jekyll-specific things I was using: xml_escape, date_to_xmlschema, date_to_string, and date_to_long_string filters, and the site.time and site.github.url variables (we already handled the post_url tag above).

At first, I tried to recreate them in Eleventy (which is easy with custom shortcodes and global data files), but finally decided that I could replace most with more standard Liquid that would be compatible right-away with LiquidJS: xml_escape becomes escape, date_* become date: with the appropriate format (this made it possible to fix my <time> elements erroneously including the time), and site.time becomes "now" or "today" with the date filter. I put that in a separate commit as that's compatible with Jekyll Liquid as well. And all that's left is therefore site.github.url that can be put in a global data file (a JS file getting the value out of an environment variable, fed by the actions/configure-pages output in the GitHub Actions workflow).

Finally, I actually had to update all templates to use Eleventy's way of handling pagination, and looping over collections.

Speaking of collections, I initially used directory data files to assign a post tag to all posts in _posts and _drafts. This didn't handle the published: false, so I used a custom collection in the configuration file instead. I probably could have also used a computed eleventyExcludeFromCollections to exclude it, but this also helped fix an issue with the sort order and apparently a bug in LiquidJS's for loop with both reversed and limit: where it would limit before reversing whichever way I wrote things, contrary to what the doc says.

One last change I made: update the Content Security Policy to account for the Eleventy dev mode autoreload; I used eleventy.env.runMode != "build" to detect when run with autoreload.

Static Files

Contrary to Jekyll where any file without front matter is simply copied, static files have to be explicitly declared with Eleventy. I also had to ignore those HTML files I needed to just copy without processing.

Permalinks for the rss.xml and style.css are defined right in those files' front matter. The index.html uses pagination so I declared a mapping there as well.

Finally I decided to compute the permalink for posts right in the front matter of the post layout, using the page.fileSlug gives me exactly what I want (the date part has already been removed by Eleventy). Using a JS front matter allowed me to filter out the published: false article so it wouldn't ever be rendered to disk (I already excluded it from the posts collection, but Eleventy would still process and render it).

Drafts

To handle drafts, I'm using the getFilteredByGlob function when declaring the posts collection, so I can decide whether to include the _drafts folder depending on an environment variable. This would include the drafts in the posts collection so they would appear in the index.html and rss.xml.

More importantly though, when not including drafts, I have to ignore the _drafts folder, otherwise the drafts are still processed and generated (despite not being linked to as they don't appear in the posts collection). This is actually not really a problem given that I don't commit drafts to my Git repository, so I would observe this behavior only locally.

Comparing the results

To make sure the output was identical to the Jekyll-based version, I built the site once with Jekyll before any modification and backed up the _site folder; then compared it with the output of Eleventy to make sure everything was OK.

Conclusion

As I felt my way and learned about Eleventy, this took me nearly two weekends to complete (not full time, don't worry!) What took me the most time actually was probably finding (and deciding on) the new syntax-highlighting theme! Otherwise, things went really smoothly.

I'm very happy with the outcome, so I switched over. And now that I control the build workflow, I know I could setup an asset pipeline, minify the generated HTML, bring in more Eleventy plugins to split the syntax-highlighting theme out and only send it when there's a code block on the page, etc.

A big would recommend!

Discuss: Dev.to