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.
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.
- I have articles written in HTML (exports from Posterous) and Markdown, using a bit of Liquid to link to other articles (with the
post_urlJekyll tag). The Markdown articles use GitHub Flavored Markdown, including syntax-highlighted fenced code blocks, with embedded HTML. Ideally I shouldn't have to update the articles at all.
- I only have 4 templates only (
postlayouts) so migrating to another templating engine wouldn't really be a problem. The
index.htmluses pagination (even though I still only have a single page). The
defaultlayout builds a Content Security Policy using flags from the articles' front matter.
- I also have a few static files: CSS, JS, and images (and a file to verify ownership for the Google Search Console).
- Of course, because cool URIs don't change, the permalinks have to be ported to the new solution.
- I hadn't identified it at first, but I actually have an old article that's not published, through Jekyll's
published: falsein the front matter. In the worst case, I'd just delete it (it'd still be there in the Git history).
- Nice to have: I kinda like Jekyll's
_draftsfolder using the file's last modified date, and
_postsfolder with the publication date as part of the file name. (I don't commit my drafts, and yes that means I don't have backups; I don't have many drafts, and I'll probably never finish and publish them so 🤷)
- Of course I want something I'm comfortable using for the next 10 years, in terms of technology and ecosystem. This means essentially that I'd like a Node-based solution.
- Last, but not least, I want the output to be (almost) identical (for now at least) to the Jekyll site: must be static HTML, with
<script>s added by the layouts and possibly right from the articles, no client-side hydration and upgrading to a Single Page Application.
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
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.
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.
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/deploy-pages actions. I took inspiration from the Astro starter workflow and updated it for Eleventy.
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).
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
I did the same with the
css extension so I could use an
include to bring in the syntax-highlighting theme in my
I had to rename my layout files to use a
.liquid extension rather than
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:
date_to_long_string filters, and the
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:
date: with the appropriate format (this made it possible to fix my
<time> elements erroneously including the time), and
"today" with the
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
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
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.
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
style.css are defined right in those files' front matter.
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).
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
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
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.
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!