Building a Hugo site with Nix
How I learned to build up a Hugo site using Nix.
So when the time came to rebuild this blog with Hugo, I knew it was probably a good excuse to learn how to use Nix for more general-purpose tasks, like generating a static site with a generator. There were some bumps along the way, but for the most part, having my builds be declarative & reproducible was a good idea, I think.
It all starts out with a Hugo site. If you’ve not got one, they have a great guide for that. Once you have that, you can move on to configuring a Nix flake. All I did to start with (while I figured my way around the Hugo CLI) was set up a dev shell.
{
inputs.nixpkgs.url = "github:nixos/nixpkgs/release-23.11";
outputs = inputs@{ flake-parts, ... }:
flake-parts.lib.mkFlake { inherit inputs; } {
systems = inputs.nixpkgs.lib.systems.flakeExposed;
perSystem = { self', pkgs, ... }:
let
inherit (pkgs) just hugo jq;
in
{
devShells.default = pkgs.mkShell {
buildInputs = [ just jq hugo ];
};
};
};
}
This let me run nix develop
to drop into a pre-configured shell with just
,
jq
, and hugo
installed. Ignoring the other two packages, we’re gonna be
using hugo
here to quickly test out our site.
$ hugo serve
...
Web Server is available at http://localhost:1313/ (bind address 127.0.0.1)
Press Ctrl+C to stop
Cool, so our site works. Now, what I could do is just blindly use GitHub’s
template for building sites, but being the tinkerer I am, that wasn’t the best
I could do. So what I did was dig into the way that Nixpkgs’s runCommand
util
worked. It seemed to do what I wanted, it lets you run a series of commands and
populate a path at the variable $out
with a file or directory to be persisted
in the Nix store. So I set about doing that.
I knew that Hugo sets up some in-situ directories to track build state (things
like lockfiles, pre-compiled static assets, etc.), so I was going to need a
temporary working directory to run all this in. I also knew that I would need
to probably call it with the --minify
flag to be relatively performant.
All said, here’s what I landed on, putting it in my perSystem
block in my
flake:
{ pkgs, ... }: {
...
packages = {
default = pkgs.runCommand "dist" {
src = ./.;
buildInputs = [ hugo ];
} ''
work=$(mktemp -d)
cp -r $src/* $work
(cd $work && hugo --minify)
cp -r $work/public $out
rm -rf $work
'';
};
}
So what we’ve got here is runCommand
, creating an output in the store called
dist
, taking the current directory (i.e. where both my flake and Hugo site
root are) as a source input, and configuring hugo
to be available in the
build’s PATH
.
Then, we’re giving it a multi-line string which is pretty much equivalent to a
bash script in this instance. We create a temporary working directory (which we
store the path to in a variable called work
), make a copy of our sources
there, and then run hugo --minify
from that folder to build our site fully.
With that done, we can finally copy the contents of the public/
directory to
the $out
path in the Nix store.
All this comes together to give us a path on the nix store
(/nix/store/<hash>-dist/
) with our fully-built and ready-to-deploy website.
Nice. So we build this with nix build .#default
, and we get the contents of
our would-be public/
dir in a symlink to the Nix store path called result
.
Note: If you want to run this with symlinked themes in your
themes/
dir like me, you need to build.?submodules=1#default
, not just.#default
. This is because Nix ignores submodules in Flakes for some reason unless you explicitly tell it to use them.
$ ls -lah result
Permissions Size User Date Modified Name
.r--r--r-- 3.0k root 1 Jan 1970 404.html
.r--r--r-- 65k root 1 Jan 1970 android-chrome-192x192.png
.r--r--r-- 336k root 1 Jan 1970 android-chrome-512x512.png
.r--r--r-- 59k root 1 Jan 1970 apple-touch-icon.png
dr-xr-xr-x - root 1 Jan 1970 categories
dr-xr-xr-x - root 1 Jan 1970 css
.r--r--r-- 987 root 1 Jan 1970 favicon-16x16.png
.r--r--r-- 3.0k root 1 Jan 1970 favicon-32x32.png
.r--r--r-- 15k root 1 Jan 1970 favicon.ico
dr-xr-xr-x - root 1 Jan 1970 images
.r--r--r-- 3.9k root 1 Jan 1970 index.html
.r--r--r-- 1.9k root 1 Jan 1970 index.xml
dr-xr-xr-x - root 1 Jan 1970 posts
.r--r--r-- 672 root 1 Jan 1970 sitemap.xml
dr-xr-xr-x - root 1 Jan 1970 tags
(I’m using eza as an ls
replacement, hence the weird
formatting)
Cool. So now, I can run a Nix build in GitHub actions and zip up the contents
of public
using their actions/upload-pages-artifact@v2
action, and deploy
it to Pages like it was any other tool :)
All in all, I’m not sure using Nix for this provides much raw benefit over just using a normal CD pipeline to do this (i.e. using their template), but it was fun to set up and simplifies being able to run this on any machine I want to work on it from.