Nix as a Static Site Generator
A pathway incremental builds and reproducability
This is a commentary to a blog post make
as a
Static Site Generator and a HN comment which said the
following:
A problem with this approach is that deleting a file from source/ does not delete it from build/. In my own projects, simply rebuilding the whole site is fast enough, so I opt to remove the whole build folder before a rebuild: https://github.com/jez/jez.github.io/blob/source/Makefile#L1… This defeats a big part of why you’d want a build system in the first place (incremental builds), but at least if you know the page you want to regenerate you can still
make
that file directly. If there’s a common workaround for this pattern in makefiles I’d love to learn it.
I use Nix to generate the pages on this blog. It has some interesting upsides: I have incremental builds while ensuring that the build folder is always up to date. I host the source code at GitHub.
When I write a new blog post, I make a directory first at
blogPosts
folder. Then, I use Nix templates to generate the
basic config: nix flake init -t ../../
which generates a
flake.nix
file. I then change the title
,
description
, pubDate
, and name
entry on the flake.nix
. Then, to make these new files
appear for Nix, I run git add flake.nix
.
I then start writing Markdown:
nix run ../..#neovim -- main.md
. This pulls in some
additional packages for writing my Neovim configuration as described in
my blog post Modular
Neovim with Nix.
When done, I run git add main.md
followed by
nix build
. This creates a lockfile flake.lock
which pins the dependencies like template files for me. This is useful
so that if I update my template file later in a breaking way, it does
not break previously generated entries. In other words, each page is
ensured to work and look the same even if I update CSS or HTML. The
actual build script is a bash script like this:
rec {
packages.default = pkgs.stdenv.mkDerivation
title = "Nix as a Static Site Generator";
description = "A pathway incremental builds and reproducability";
pubDate = "10 Sep 2023 16:45:00 GMT";
name = "";
src = ./.;
buildInputs = with pkgs; [
inputs'.barbell.packages.barbell
nodePackages.js-beautify
pandoc
python311Packages.python-slugify
validator-nu
woff2];
phases = [ "unpackPhase" "buildPhase" "checkPhase" ];
buildPhase = ''
mkdir -p $out/css
mkdir -p $out/img
mkdir html
cp -r ${inputs'.web-components.packages.default}/html/* ./html
cp ${pkgs.ibm-plex}/share/fonts/opentype/IBMPlexMono-Regular.otf .
cp -r img/* $out/img
woff2_compress IBMPlexMono-Regular.otf
cp IBMPlexMono-Regular.woff2 $out/
pandoc main.md --katex -o main.html
echo "${title}" > title.bar
echo "${description}" > description.bar
echo "${pubDate}" > pubDate.bar
echo "${name}" > name.bar
slugify ${title} > slug.bar
date -d "${pubDate}" -Iminutes > datetime.bar
cat main.md | wc -w > wordCount.bar
barbell main.html > article.bar
barbell html/template_article.html > $out/$(slugify ${title}).html
js-beautify -f $out/$(slugify ${title}).html -r
'';
doCheck = true;
checkPhase = ''
vnu $out/$(slugify ${title}).html
'';
};
So what this does is as follows:
- in
buildInputs
I pull packages that the bash script needs buildPhase
includes the actual commands. I first create bunch of folders, generate the font file forcode
blocks, then usepandoc
to generate a HTML page- I then create files that the template file uses using a template
engine I wrote in BQN called
barbell
– what this does is that it creates variables that get included in the HTML template where blocks such as|variable|
exist - then the
barbell
command is used to create an article page:barbell main.html > article.bar
barbell
is used recursively to also include blocks in the html template:barbell html/template_article.html > $out/$(slugify ${title}.html)
- file is beutified in-place:
js-beautify -f $out/$(slugify ${title}).html -r
- the
checkPhase
runs a sanity check using W3C validatorvnu $out/$(slugify ${title}).html
– this is useful to check that the page was generated OK without actually looking at the file - I can now preview the page in the build folder:
open result
When I’m done, I run git add flake.lock
and I push this
to GitHub. This does not include the page on my blog yet, but it creates
an URL that I can import. The URL looks like:
github:jhvst/jhvst.github.io?dir=blogPosts/${title}
.
I then update my main flake which generates my blog, which is found from the project root folder. This works as follows:
- run
nix flake update
so that the new dir folder resolves - add a new import: suppose the name of the entry is foo, then I add
foo.url = "github:jhvst/jhvst.github.io?dir=blogPosts/foo
- in the
buildPhase
of my root flake, I add the following lines:mkdir -p $out/blogPosts/foo
andcp -r ${inputs.foo.outputs.packages.${system}.default}/* $out/blogPosts/foo
– this copies the build assets to an URL that I want to have the page - I update the
rss.xml
file if I want it to appear in my RSS feed git commit
andgit push
will trigger a GitHub Action which builds my site, and pushes the resulting build folder to GitHub pages
Done. To see the diffs see: 1) and 2).
Somewhat of a downside is that if I want to update my blog post, I always need two git pushes. For example, see 1) and 2). However, overall I’m quite happy with my setup and don’t see why to go back anymore.