Erik's Blog - How the Sausage (This Blog) is Made - 2023-10-05

Motivation

I had some time off last week and almost immediately caught a nasty cold, so instead of spending my time on the beach, I tried to keep myself busy while staying in bed. So, as a result, this blog is now built with Hakyll instead of Pelican.

Why Switch?

Pelican is a great tool, it’s what Codeberg uses to publish their blog. It makes it easy to get started by requiring a strict directory structure; based on that and a configuration file, it does a lot of things out of the box like creating an RSS feed. However, it also does a lot of things that I don’t need, like generating tags.

I stumbled across Hakyll on kotatsuyaki’s blog and was intrigued. Hakyll is not a Static Site Generator per se; it is a Haskell library that allows you to specify the generation of your site declaratively with an embedded DSL.

And so it was decided; if I wasn’t going to get my feet wet in the ocean I could at least play around with Haskell.

The Setup

The site generator is easy enough to write; the DSL is quite intuitive.

-- site.hs
main :: IO ()
main = hakyll $ do
    match "css/*" $ do
        route   idRoute
        compile compressCssCompiler

    match "posts/*" $ do
        route $ setExtension "html"
        compile $ pandocCompiler
            >>= loadAndApplyTemplate "templates/post.html"    postCtx
            >>= loadAndApplyTemplate "templates/default.html" postCtx
            >>= relativizeUrls
--...

If you are interested in the full source code, you can find it here. Compiling this yields an executable site which builds the static content from the markdown files in the posts directory.

The main piece that pulls this setup together is the Nix flake. It declares one package:

    packages.ssg = with pkgs; stdenv.mkDerivation {
      name = "ssg";
      src = self;
      nativeBuildInputs = [
        ( haskellPackages.ghcWithPackages (ps: with ps; [
            cabal-install
            openssh
            zlib
            hakyll
          ])
        )
      ];
      buildInputs = [ ];
      # needed to avoid trying to build in a read-only directory
      preBuild = "export HOME=$TMPDIR";
      installPhase = ''
        ghc --make site.hs
        mkdir -p $out/bin
        install -t $out/bin site
      '';
      outputs = [ "out" ];
    };
    packages.default = packages.ssg;

This declares the build steps and dependencies for the static site generator. The command nix shell will build the site executable and enter a shell in which this executable is available, giving us access to the subcommands site build, site clean, site watch. Once we have the executable, we no longer need any of the build dependencies, such as the Haskell compiler, when we just want to write content, so they are not available in this shell.

When we do want to make changes to the static site generator, we can instead use nix develop. This gives us a shell, which includes all the build dependencies, but not the site executable, thus allowing us to hack the generator and compile it manually, as well.

Here, we use nix shell and nix develop as short-hands for using the default package. nix shell .#ssg and nix develop .#ssg would have the same effect. We can also use nix build to build the site generator executable.

If you are unfamiliar with Nix, what nix build does is it builds the package in a separate part of the file system (called the Nix store) and puts a symlink result in the current working directory. This symlink points to a directory which only contains the “final products” as specified by the outputs of the package definition.

If you are familiar with Nix, you will probably want to ignore this oversimplified explanation.

This blog is currently hosted on Codeberg Pages. This means, there is a git repository containing the static content; this one to be exact.

There are ways of keeping everything (the generator, markdown, and html) in the same repository. It usually involves keeping the generator and markdown in a subdirectory, which is what I was doing when using Pelican; but this approach never really clicked with me and always felt a little hack-y. So now, I keep the generator separate and put only the stuff in the pages repository that is actually to be hosted, since that is its whole purpose. All we need to do is to copy the content of the _site directory (produced by site build) to the pages repository and push our changes.

This workflow could likely be automated with CI steps and all the bells and whistles, but with the current posting frequency of this blog, it doesn’t seem worth the effort.

Final Thoughts

All in all, the switch to Hakyll has been a very pleasant experience. The DSL is fun to write and powerful enough to do everything I need. I also enjoy the declarative nature of it. Finally, it gives you a better understanding of what goes into a static site generator, what is needed to generate an RSS feed or a site map, for example. It’s always good to know a little more about how things work, even if you do end up using a framework that does it for you.