Build a Jekyll blog using Nix

EDIT: This blog doesn't use jekyll anymore and has been moved from Github Pages.

This blog is powered by Github pages and jekyll. I started it when I had never ever heard of Nix, but since then I've moved to NixOS and wanted to be able to write again here. Unfortunately it wasn't that easy...

Indeed the documentation out here about building this setup with Nix is very outdated and a lot of it just doesn't work anymore because package evolved, purity is now enforced using flakes, etc ...

I've managed to get something to work and I'll write it down for you:

Base flake structure

The base structure of the flake is fairly simple:

  description = "Build the blog";
  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs/22.11";
  outputs = inputs: with inputs; let
    system = "x86_64-linux";
    pkgs = import nixpkgs { inherit system; };
    # Utility to run a script easily in the flakes app
    simple_script = name: add_deps: text: let
      exec = pkgs.writeShellApplication {
        inherit name text;
        runtimeInputs = with pkgs; [
            gnumake     # Required by some dependencies in order to build
        ] ++ add_deps;
    in {
      type = "app";
      program = "${exec}/bin/${name}";
  in {
    apps.${system} = {
      default = simple_script "serve_blog" [] ''
        # nix run -> Serve the website locally
      generate = simple_script "generate_blog_env" [] ''
          # nix run .#generate
          #   Will be used to re-generate the Ruby environment after modifying
          #     a dependency, the version number, etc ...

The workflow

At initialisation, and after that every time you modify a dependency, bump a version, or modify something in the Ruby environment, call nix run .#generate

It will use bundler and bundix to generate the Gemfile, Gemfile.lock,gemset.nix and even .bundle/config.
This way, we only need to configure variables inside the flake, the rest will magically work.

Once this is done, you can serve the website locally by calling nix run

It will use the Ruby environment we created using the generate script, and calljekyll serve --trace

Generating the Ruby environment

First for the environment generation, we will generate the Gemfile file using good ol' nix variables:

github-pages-version = 227;
inc_gems = {
  minima = "2.5";
  webrick = "1.7";          # Required for the jekyll serve to work
inc_plugins = {
  jekyll-feed = "0.12";
# Generation of the Gemfile
generate_gemfile = let
  gems = builtins.concatStringsSep "\n" (pkgs.lib.attrsets.mapAttrsToList (name: version:
    "gem \"${name}\", \"~> ${version}\""
  ) inc_gems);
  plugins = builtins.concatStringsSep "\n" (pkgs.lib.attrsets.mapAttrsToList (name: version:
    "  gem \"${name}\", \"~> ${version}\""
  ) inc_plugins);
    # This file is autogenerated by the flake.nix file, do not edit
    source ""
    # Gems dependencies to be installed
    gem "github-pages", "~> ${builtins.toString github-pages-version}", group: :jekyll_plugins
    # Github Pages plugins
    group :jekyll_plugins do

This Gemfile will be used by bundler to create a Gemfile.lock to pin the dependencies, which in turn will be used by bundix to generate a gemset.nix, the nixified ruby gems.

All these files are used by the nix bundlerEnv function like this:

# The build environment
env = pkgs.bundlerEnv {
  name = "blog";
  ruby = pkgs.ruby_3_1;
  gemdir = ./.;

Now, let's tie the two pieces together in the generate_blog_env script:

generate = simple_script "generate_blog_env" [ pkgs.bundix ] ''
    set -e
    rm -f gemset.nix Gemfile Gemfile.lock .bundle/config
    cat << EOF > Gemfile
    bundler update
    bundler lock
    bundler package
    bundix --magic -d -l
    rm -rf vendor

So far so good.

Bundle configuration and platform issues

Nowadays, bundler doesn't like command line flags to be used, and it prefers to read from either the .bundle/config file, or directly from environment variables.

In the generate_blog_env script, I adapted the flags passed to generate the configuration before generating the gemset.nix file like so:

export BUNDLE_PATH=vendor
export BUNDLE_CACHE_ALL=true

Notice the BUNDLE_FORCE_RUBY_PLATFORM, it is needed because some dependencies (at least nokogiri) are platform-specific and will raise an issue if it detects the machine platform. This configuration will ensure that it won't raise any compatibility issue.

Using the environment

Yay ! The environment builds without any trouble !

Now let's serve the website locally using jekyll. In the serve_blog script, we simply have to write:

    echo "Bundler env: ${env}"      # For debugging / exploration of the files
    ${env}/bin/bundler exec ${env}/bin/jekyll serve --trace

And now we can serve the whole thing locally without any troubles !

It finally builds

The full flake source can be seen here

All this setup is the result of hours or research and debugging, and I unfortunately wasn't able to find all the links and pages that I used, so I cannot credit them the way I'd like to. However a big Thank you is in order for all these people

This setup builds perfectly for my (weird) setup, but it's far from clean, optimized, and could maybe not work for you.
However feel free to reach me if you have some trouble, or if you wish to make this setup better, for now it only lies in my blog repo, but why not having a repository specially made for building a Github pages + jerkyll setup using Nix.

↑ Go to top