When making (visual) changes on my blog, I used to make a commit and wait until GitHub pages rebuilt and published the site again, before I could verify that I liked the change or if I should modify it further. A single iteration often took several minutes. It worked, but requires a lot of waiting and iterations to reach satisfactory results. At some point, I just googled how to run jekyll locally (on NixOS).
In this post, I will give you some resources and ideas how to run jekyll .
Feel free to follow what works, and ignore what does not. You don’t need to be
on NixOS
– all you need is nix
, and I’ll show you how.
Context
Originally, I started out with the approach described in this blog post I found. As the exact way didn’t work, I did some troubleshooting and fixed all issues that popped up.
In preparation for this post, I tried to retrace these steps, and I
spectacularly failed rebuilding it (nokogiri
, a dependency of the
github-pages
-gem has a different hash than expected. I figured out a way to
solve it originally, but I gave up on it. Don’t fret, I found a better solution).
The goal is to run jekyll serve --watch --incremental
in a nix-shell
environment, so that I don’t need to have anything installed locally.
I’ll explain the original way with my adaptations (which is potentially more flexible), as well as my newfound setup – which is a lot faster!
Package Manager nix
In case of unfamiliarity with nix
, you can learn more about it on their
website, read about how nix works or one of the many
blogposts about what nix is
(Also check out my thoughts on nix here).
The official way to install nix
is (yes I know …):
$ curl -L https://nixos.org/nix/install | sh
This will install nix
and its related binaries. In particular, we will be
using nix-shell
, which provides a local virtual environment with certain
(additional) packages available. You probably need to add a [channel][nix-channel]
for a fresh install.
This is going to be the fastest way with the least amount of pain, if you don’t
have have ruby
and bundle
set up. Either way, you might still want to take
a look at GitHub’s site on testing locally this is
fundamentally based on.
Original way
I’ll tell you about my original setup, how to replicate it, and what I changed. As mentioned earlier, it is mostly a copy from this blog post. It doesn’t build at the time of writing, but might very well do so again in the future, or if you can resolve the dependency issue in some way.
Feel free to skip ahead to the newfound way directly – the original way didn’t end up working again, after all.
First, some explanations.
Gemfile
A Gemfile
specifies the source of ruby (a programming language) package
dependencies (gems). You probably have a Gemfile
with dependencies (e.g. your
theme) for your page in your repository.
Turns out there exists a single package with everything needed to build
github-pages
locally, so we’re going to use it. The Gemfile for it would look
like this:
# file: Gemfile
source 'https://rubygems.org'
gem 'github-pages'
As it is different from the Gemfile
of my github pages site, I temporarily
replaced it (ensure that you don’t commit that change), and later saved this
one as Gemfile-nix-shell
– I’ll tell you when to do that.
Packaging and Building
The Gemfile
specifies our top-level dependencies, but github-pages
has
dependencies itself. We will use bundler
to determine and resolve them. Then,
bundix
will take the Gemfile.lock
and convert it to a nix expression –
which we can then use in our shell.nix
-expression later. For that:
$ nix-shell -p bundler bundix # enter environment with `bundler` and `bundix` available
$ bundler package --no-install --path vendor # execute bundler packaging, this creates e.g. Gemfile.lock
$ bundix # run bundix
$ rm -rf .bundle vendor # remove bundler artifacts
$ exit # leave nix-shell environment
shell.nix
The nix-shell
provides us with an environment of packages we can specify. We
can also use a shell.nix
-file to provide them in a more deterministic way.
When we build a shell.nix
, we can import the gemset.nix
expression based on
the Gemfile.lock
lockfile. At this point, I renamed my Gemfile
to
Gemfile-nix-shell
, to be able to have it and the original Gemfile
in the
repository - GitHub pages will not build with the modified Gemfile
for
shell-nix
.
# file: shell.nix
with import <nixpkgs> { };
let jekyll_env = bundlerEnv {
name = "jekyll_env";
ruby = ruby_2_6;
gemfile = ./Gemfile-nix-shell;
lockfile = ./Gemfile.lock;
gemset = ./gemset.nix;
};
in
stdenv.mkDerivation rec {
name = "jekyll_env";
buildInputs = [ jekyll_env ];
shellHook = ''
exec ${jekyll_env}/bin/jekyll serve --watch --incremental
'';
}
With that, we can then finally run it:
$ nix-shell # building and starting local jekyll server
Usually, nix-shell
just provides a shell with the requested packages
additionally in context. Using the shellHook
we can instead directly execute
a command, in our case the one to run jekyll
.
Fixing gemset.nix and Gemfile.lock
If nix-shell
succeeds – lucky you! The issue I had got fixed.
If not – you should see something similar to this:
error: hash mismatch in fixed-output derivation '/nix/store/pdnlvva6xrzs5m4g8s81xx4bizsvrb8l-nokogiri-1.13.3.gem.drv':
specified: sha256-P2NAZhwqKDszfSJ+oiT4WWI3dbL1wJpr8Ze3hlY5WN8=
got: sha256-vxsbzv+RCrsLetglU1lREBoDYbhZwq0b4VXAEAgey9w=
error: 1 dependencies of derivation '/nix/store/dfvq6ppw9i7xf869z68vcx9j7lvmvbw6-ruby2.7.5-nokogiri-1.13.3.drv' failed to build
error: 1 dependencies of derivation '/nix/store/4ybs0rncdzn2z8dgc6bfd2ifsy3zfbvr-jekyll_env.drv' failed to build
gemset.nix
is a nix
-transformed version of the Gemfile.lock
, which locks
ruby dependencies at a given point in time. They are not intended to be edited
manually, but I remember succeeding by doing so. It was more complicated than
doing just that, but it wasn’t that much more complicated.
I’m sorry, I couldn’t reconstruct what I did. At this point you’re on your own.
But you don’t have to. You can just do what I’m doing now instead.
A Better Way
Since my original setup didn’t work anymore – and I’m only using the
jekyll-paginate
plugin – it should be possible to use fully packaged
jekyll
directly. And indeed, doing this just works:
$ nix-shell -p jekyll rubyPackages.jekyll-paginate
$ jekyll serve --watch --incremental
Which, when transformed in a shell.nix
looks like this:
shell.nix
# file: shell.nix
with import <nixpkgs> { };
mkShell {
buildInputs = [
jekyll
rubyPackages.jekyll-paginate
];
shellHook = ''
exec jekyll serve --watch --incremental
'';
}
Not only does this automatically exit the environment when stopping it (e.g.
via CTRL+C
), but it also rebuilds sites faster on all my machines.
With our shell.nix
written, we can just run nix-shell
instead.
(If you want to try and compare both approaches, you can rename either
shell.nix
to something other than shell.nix
and specify the filename later
with nix-shell <filename>
. shell.nix
is just the default.)
Conclusion
You hopefully learned a bit about the different parts necessary to build github pages locally, even if they are mostly abstracted away. Tell me if you find something even better!
Don’t use this for production deployment though! I use the official
jekyll/jekyll
docker image on my server
(blog.fkarg.de) behind traefik.
Tips and Tricks
- When you have your
shell.nix
, you can run the shell by executing$ nix-shell
in the same directory, and end it withSTRG+C
. - When you make changes to your
_config.yml
, you need to stop thenix-shell
and remove the_site
directory before you can see the changes when restartingnix-shell
again.- This is also true when you create or remove a file in
_posts
. Updating works fine
- This is also true when you create or remove a file in
- Within the
_config.yml
you can exclude paths and specific files from triggering a page rebuild or being available on navigation. I exclude e.g. theREADME.md
or adrafts/
folder. - If you’re confused about
nix-shell
, see this introduction to nix-shell. - Even with the
jekyll
server runnig locally, this doesn’t mean all of your resources are local and available offline. My site used to have many externaljavascript
andcss
dependencies, which aren’t available without internet, making it look very weird and mostly unfunctional when offline.