Making GitHub Pages Work With Jekyll 4+ and Any Theme and Plugin

Updated

GitHub Pages makes it easy to publish and host a Jekyll site for free. In theory, at least. In my Beginner’s Guide To Bundler and Gemfiles, we saw how the official GitHub documentation contains incorrect or confusing instructions that can prevent people from running the Jekyll site at all, whether locally (meaning on their computer) or on GitHub Pages.

Another thing we learned is that the github-pages gem only supports specific versions of Jekyll and other gems. It also only supports specific themes and plugins. It can be frustrating to find a cool plugin that adds some needed functionality, only to find out that it doesn’t work with the github-pages gem. It can also be time-consuming to look for themes that both fit your needs and that are compatible with github-pages.

When you create a new Jekyll project from the command line with jekyll new, the github-pages gem is disabled by default, but if you push the entire project to GitHub, it will still generate your site on GitHub Pages. Having the github-pages gem enabled in your Gemfile is not a requirement to be able to publish your site to GitHub Pages. What is not obvious is that when you push your project to GitHub, it gets built in the cloud by GitHub using the github-pages gem.

If your site is using different versions of gems, or unsupported plugins, you might be surprised that your site works fine locally, but that it doesn’t match what shows up on GitHub Pages. Sometimes, the site might fail to build entirely on GitHub Pages.

So, if we want to use the latest and greatest Jekyll, and any theme and plugin we want, while still taking advantage of GitHub’s free hosting, is that possible? Yes! But first, it helps to understand how GitHub Pages and static site generators in general work.

When you run bundle exec jekyll serve to view your site locally in a browser, Jekyll is converting the content in your Markdown files into HTML files, which is what the browser needs to display your site. Similarly, when you run bundle exec jekyll build, it creates all the necessary files to display your site in a browser, in a folder called _site. To make your site available on the internet for the world to see, you need to place the contents of the _site folder on a computer that can serve those files to the public. This is what GitHub Pages does for you.

There are various ways you can configure your GitHub repo to trigger a Jekyll build. One of them is to have a branch called gh-pages. You can push your entire Jekyll project (meaning all the Markdown files, Gemfile, _config.yml, etc.), without having to run jekyll build, and GitHub will automatically run jekyll build on its servers, and then serve the contents of the generated _site folder. But as we saw earlier, this uses the restrictive github-pages gem.

So, instead of letting GitHub build the site for us, we can do it on our own locally by running bundle exec jekyll build, and then pushing only the contents of the _site folder to the gh-pages branch. This requires that you have two separate branches: one for your Jekyll project (typically called master or main), and the gh-pages branch that is only used to hold the contents of the _site folder every time you build the site. That’s all there is to it in order to use the latest Jekyll and any theme and plugin you want. 1

However, having to run bundle exec jekyll build from the main branch, and then switching to the gh-pages branch to push the latest _site can get tedious. Luckily, there is a solution that allows us to automate this process. We can use GitHub Actions, which is a feature that allows you to automate workflows based on certain events that happen on your repo. For example, we can create a workflow that automatically builds the site using the same version of Jekyll and other gems in our project, and deploys it every time we push to the main branch. It can also automatically create the gh-pages branch.

Let’s test this out together step by step. To follow along, you’ll need the following prerequisites:

First, check if you already have Ruby 3.2.3:

ruby -v

If it doesn’t say 3.2.3, then try switching to it:

chruby 3.2.3

If it doesn’t say anything, you already have it. If it says unknown Ruby, you’ll need to install it:

ruby-install 3.2.3

If it says chruby: command not found, and you have another version manager such as RVM, rbenv, or asdf, then use it to install and/or switch to 3.2.3. I can’t guarantee the rest of this tutorial will work though.

To get chruby, I recommend using my Ruby on Mac script.

Once we have our kitchen set up, we can start cooking. First, we’ll create a new playground folder. This is where I put temporary projects for testing things out. If you followed some of my previous guides, you might already have this folder. If not, let’s create it:

cd ~ # or wherever you keep your coding projects
mkdir playground && cd playground

Let’s create a new folder called jekyll-github-actions and initialize it as a Git repository:

git init jekyll-github-actions && cd jekyll-github-actions

To make sure we’re using Ruby 3.2.3, switch to it:

chruby 3.2.3

And then we can create a new Jekyll project:

gem install bundler jekyll
jekyll new .

Next, we’ll add a .ruby-version file so that the correct Ruby version is used whenever we cd into our jekyll-github-actions directory:

echo 'ruby-3.2.3' >> .ruby-version

To test that deploying through GitHub Actions works, we’ll add a plugin that I know doesn’t work with the github-pages gem, such as the jekyll-timeago plugin referenced in the Jekyll documentation.

Update your Gemfile so that it looks like this:

source "https://rubygems.org"
# Hello! This is where you manage which Jekyll version is used to run.
# When you want to use a different version, change it below, save the
# file and run `bundle install`. Run Jekyll with `bundle exec`, like so:
#
#     bundle exec jekyll serve
#
# This will help ensure the proper Jekyll version is running.
# Happy Jekylling!

ruby "3.2.3"

gem "jekyll", "~> 4.2.0"
# This is the default theme for new Jekyll sites. You may change this to anything you like.
gem "minima", "~> 2.5"
# If you want to use GitHub Pages, remove the "gem "jekyll"" above and
# uncomment the line below. To upgrade, run `bundle update github-pages`.
# gem "github-pages", group: :jekyll_plugins
# If you have any plugins, put them here!
group :jekyll_plugins do
  gem "jekyll-feed", "~> 0.12"
  gem "jekyll-timeago", "~> 0.13.1"
end

Save the file, then run bundle install.

Open your Gemfile.lock, and look towards the bottom in the PLATFORMS section. If ruby is not listed, add it with this command:

bundle lock --add-platform ruby

To test that the jekyll-timeago plugin works, we’ll update our index.markdown to make use of it:

---
# Feel free to add content and custom Front Matter to this file.
# To modify the layout, see https://jekyllrb.com/docs/themes/#overriding-theme-defaults

layout: home
---

Testing the timeago plugin on GitHub Pages

{% assign date = '2020-04-13T10:20:00Z' %}

- Original date - {{ date }}
- With timeago filter - {{ date | timeago }}

To trigger a workflow, GitHub expects a YAML file inside the .github/workflows folder structure, so let’s create it:

mkdir -p .github/workflows
touch .github/workflows/jekyll-github-pages.yml

The -p option allows us to create nested directories with mkdir.

Copy and paste the following into the jekyll-github-pages.yml file.

name: Build and Deploy a Jekyll Site to GitHub Pages

on:
  push:
    branches:
      - main

jobs:
  jekyll:
    runs-on: macos-latest
    steps:
      - name: 📂 setup
        uses: actions/checkout@v2

        # include the lines below if you are using jekyll-last-modified-at
        # or if you would otherwise need to fetch the full commit history
        # however this may be very slow for large repositories!
        # with:
        # fetch-depth: '0'
      - name: 💎 setup ruby
        uses: ruby/setup-ruby@v1
        with:
          ruby-version: 3.2

      - name: 🔨 install dependencies & build site
        uses: limjh16/jekyll-action-ts@v2
        with:
          enable_cache: true

      - name: 🚀 deploy
        uses: peaceiris/actions-gh-pages@v3
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          publish_dir: ./_site
          # if the repo you are deploying to is <username>.github.io, uncomment the line below.
          # if you are including the line below, make sure your source files are NOT in the "main" branch:
          # publish_branch: main

This workflow uses 4 different GitHub Actions. The first action, actions/checkout, is an official one provided by GitHub that copies the repo to the computer in the cloud that will build the Jekyll site.

The second one, ruby/setup-ruby downloads a prebuilt Ruby and adds it to the PATH in about 5 seconds.

The third one, limjh16/jekyll-action-ts is where the Jekyll site gets built. It uses caching to determine whether or not it needs to run bundle install. This saves time if the Gemfile.lock hasn’t changed.

Once the site is built, we use peaceiris/actions-gh-pages to push the contents of the _site folder to the gh-pages branch.

We also need to update the baseurl in our _config.yml to match the name of our repo because this will be a “project” site as opposed to a “user” site:2

baseurl: "/jekyll-github-actions"

Finally, you can optionally add a README.md so you can remember how you created this repo:

touch README.md

Then add the following to it:

This is an example repo that shows how you can automatically deploy a 
Jekyll 4+ site to GitHub Pages with GitHub Actions. 
It was created by following this guide: 
https://www.moncefbelyamani.com/making-github-pages-work-with-latest-jekyll

Let’s commit these changes:

git add .
git commit -m "New Jekyll site deployed to GitHub Pages with GitHub Actions"

Now we can push this to a brand new repo on GitHub, which we can do from the command line. First, we need to give permission to the GitHub CLI to access our repo and to create and modify workflows. We do that by logging into GitHub via the GitHub CLI:

gh auth login --scopes repo,workflow

Follow the instructions in the terminal, picking the default options each time. Once you see “Congratulations, you’re all set!” on the GitHub site, go back to the terminal, and you should see this:

✓ Authentication complete. Press Enter to continue...

Keep going with the default options.

When it asks if you want to authenticate Git with your GitHub credentials, say yes.

The whole interaction should look something like this:

~/p/p/jekyll-github-actions main+ λ gh auth login --scopes repo,workflow
? What account do you want to log into? GitHub.com
- Logging into github.com
? How would you like to authenticate? Login with a web browser

! First copy your one-time code: 7002-A402
- Press Enter to open github.com in your browser...
✓ Authentication complete. Press Enter to continue...

? Choose default git protocol HTTPS
- gh config set -h github.com git_protocol https
✓ Configured git protocol
? Authenticate Git with your GitHub credentials? Yes
✓ Logged in as monfresh

Now let’s create a new GitHub repo from the command line:

gh repo create jekyll-github-actions --public --push -r origin -s . -d "Example Jekyll site deployed with GitHub Actions"

Now we need to create a gh-pages branch and push that up first before we push our main branch. This tells GitHub to turn on the GitHub Pages feature in our repo.

git checkout --orphan gh-pages
git add .
git commit -m "Push to gh-pages branch to turn on GitHub Pages"
git push origin gh-pages

This should automatically publish the site to GitHub Pages, but for now using the gh-pages gem. You can verify this by going to your repo’s “Settings” tab, and then scroll down to the GitHub Pages section. You should see a green bar that says Your site is published at https://<username>.github.io/jekyll-github-actions. Click on the link, and you should see the site. This is a good time to pay attention to the text on the site that shows With timeago filter - 2020-04-13T10:20:00Z. This proves that the github-pages gem does not support the jekyll-timeago plugin. Instead of the timestamp, we should see the date expressed in words, such as 9 months and 3 weeks ago.

And now we can push the main branch, which should automatically trigger the GitHub Action workflow. You can see it running in the “Actions” tab of your repo.

git checkout main
git push -u origin main

Wait for the workflow to finish (typically a little less than 3 minutes the first time), then refresh your published site, and you should see the correct timeago output. Yay!

Once last thing you’ll need to do is change your default branch from gh-pages to main. You can do that by clicking on the Settings tab in your repo, and then Branches in the left sidebar, then click on the double arrows icon under Default Branch, then choose main from the dropdown and click Update. Here’s an animated GIF of the process.

If you will be creating many Jekyll sites (whether for yourself or your clients), you probably don’t want to repeat this whole process manually. The good news is that I’ve automated the whole process for you. With a single command, you can generate a brand new Jekyll 4 site and have it deployed to GitHub pages in seconds! This will be available soon as a paid add-on to Ruby on Mac.


  1. Depending on how you set up the GitHub repo, you might also need to go to your repo’s settings and set the “source” for GitHub Pages to the gh-pages branch. 

  2. GitHub has an article explaining the different types of GitHub Pages sites. To keep things simple in this article, and to avoid conflicts in case you already have a “user” site, I chose a “project” site for this example.