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
- I have articles written in HTML (exports from Posterous) and Markdown, using a bit of Liquid to link to other articles (with the
post_url
Jekyll 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 (
index.html
,rss.xml
, anddefault
andpost
layouts) so migrating to another templating engine wouldn't really be a problem. Theindex.html
uses pagination (even though I still only have a single page). Thedefault
layout 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: false
in 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
_drafts
folder using the file's last modified date, and_posts
folder 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 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
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!