Upgrade to Jekyll 4 and Ruby 3

Introduction

Does this still work? Turns out it doesn’t! In this post, I go over my 4.5 year journey to fix that, driven by a new desire to more explicitly document my personal projects. Starting several years ago when I first drafted this post, I migrated to Jekyll 4 and Netlify, then just stopped. Now I have completed the journey and this site is built with Jekyll (4.4.1) and Ruby (3.4.2), all with automated dependency setup.

2020: Jekyll 3 -> 4

In September of 2020, I updated the site to run on Netlify. Which is great! I pay nothing, and pushes to master are deployed in a few minutes, with full support of Jekyll and custom plugins. This required upgrading to Jekyll 4 and fixing up dependency management to be reproducible on their build servers.

The first step was upgrading to Jekyll 4, dropping the rdiscount rendering engine (no longer supported), and removing the Pygments syntax highlighter (now built in). These updates bring GitHub flavored markdown code block language syntax, so no more {{% highlight <foo> %}}.

Netlify is awesome enough to build with whatever I have, but to do that I had to actually learn Ruby package management. So I added a Gemfile and Gemfile.lock to pin versions and used bundle exec in my Rakefile. This didn’t make my setup truly robust because Ruby was not pinned and nothing else was automated, but that was a problem for future me.

My resulting Gemfile:

source 'https://rubygems.org'

gem 'jekyll', '~>4.1'
gem 'jekyll-paginate', '~>1.1'
gem 'rake', '~>13.0.1'

2025: Ruby 2 -> 3

Now fast forward to 2025, and I actually want to get back to the blog, but it’s been two years since Ruby 2 has been updated. Since this is the only Ruby based system I use, I want everything automated and hermetic so it’s repeatable. My challenge is to get things working with Ruby 3.4.2, not install packages globally, and script setup. Build tasks are already done with a Rakefile (like a Makefile but using Ruby instead of shell).

I created the setup.sh script and moved system dependencies and rbenv to install Ruby, totally dropping manually setup instructions from the README. The best repository is one you can checkout, doesn’t require docker, has run a single setup script and works on any system. Then having the ruby version in .ruby-version and rbenv setup in my environment ensures the right Ruby is used.

Ruby still doesn’t have something like Python’s virtual environments, but you can configure bundler to install dependencies locally in the repository. The vendor/gems directory feels pretty standard, so I used that and added it to .gitignore. The resulting .bundle/config is:

---
BUNDLE_PATH: "vendor/gems"

This gets me to a setup that builds, but Jekyll errors out because the version I have is not compatible with Ruby 3.4.2.

2025: Jekyll version update

Ruby is not Go, so Jekyll doesn’t just work with the newer Ruby. To close this out, we have to update all our versions to the latest and then see if everything works! I was frustrated by not finding an automated way of doing this, so I manually updated the versions in the Gemfile and then ran bundle update to install and freeze them. I also future proofed things by explicitly using the logger package as needed for Ruby 3.5. The process for this was:

  1. Search rubygems.org for each package used and update the Gemfile to the latest
  2. Run bundle update
  3. Discover the need for an explicit logger package
  4. Find the latest logger version: gem search --remote "^logger$"
  5. Manually add gem 'logger', '~>1.7' to the Gemfile

Now rake build works and the site builds cleanly!

Bonus: Font Icon Fixes

As a small bonus, I discovered two things: font icons for my “socials” no longer showed up, and Google+ actually doesn’t exist anymore (since 2019 actually, and I sort of forgot it even existed). Fixing the latter was simply deleting a line of markup; the former required a bit more investigating.

My site is served and built as a branch--foo.netlify.app URL while it’s served from josephlisee.com. Browsers are strict about CORS and fonts (allowing a website served from host A to load content from host B), but Netlify makes configuring this as easy as adding this to my netlify.toml:

[[headers]]
  for = "font/*"
  [headers.values]
    access-control-allow-origin = "https://josephlisee.com"