How to set up a nix-backed blog, again (ft. flakes this time)

Now that my NixOS system is freshly flake’d, I want to figure out how to manage my Hugo blog project (ie this very computers.lel) using nix flakes rather than a shell.nix + channels. This will give me an opportunity to see how flakes for development differs from flakes for NixOS system management.

Making sense of my past mistakes

Here was my first go at building a nix-backed Hugo blog. Why did I make the blog source private? what was i hiding?? And why did I deploy through Netlify and not just publish a static Gitlab page? We may never know…

What I do know is that the channels-backed shell.nix that defines the current development environment and is referenced in gitlab-ci.yml will be replaced by a flake.nix! And I’ll drop the extra hop to Netlify and publish directly as a Gitlab page.

Plan

  1. Make a flake.nix
  2. Get a “build my blog” output working locally via flake.nix
  3. Get hugo server running locally via flake.nix
  4. Build & publish the blog using Gitlab Pages

1: Initial flake.nix

I’m going to start from a basic Nix flake in the root directory of my Hugo project and incrementally figure out what I need to add to it. Running:

nix flake init

generates the following “hello world” flake.nix example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
{
  description = "A very basic flake";

  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
  };

  outputs = { self, nixpkgs }: {

    packages.x86_64-linux.hello = nixpkgs.legacyPackages.x86_64-linux.hello;

    packages.x86_64-linux.default = self.packages.x86_64-linux.hello;

  };
}

This is a super minimal flake.nix with nixpkgs as the only input, and 2 outputs: a “hello” package and a “default” package. The hello (or more specifically pkgs.x86_64-linux.hello) package points to the existing GNU Hello package in nixpkgs. The default package, which also points back to hello, is what gets used when you don’t specify a flake target during calls to nix run, nix build, etc.

2: Build a hugo blog with flake.nix

What I want to do is replace the “hello” package with something of my own - a “website” package that descries how to build my hugo site. But unlike the hello package (which references a preexisting hello in nixpkgs), I’ll have to define a custom derivation1 for website, ie an expression describing how to build a thing in nix. I also know I’ll need to add my hugo theme as an input so that its version can be locked.

After looking at some examples of what other people did and reading through Wombat’s nix book, here’s what I cobbled together:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
{
  description = "A flake for computers.lol, aka my lil hugo blog";

  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";

    hugo-theme = {
      url = "github:monkeyWzr/hugo-theme-cactus";
      flake = false;
    };
  };

  outputs = inputs@{ self, nixpkgs, ... }:
  let 
    pkgs = import nixpkgs { system = "x86_64-linux"; };
  in
  {
    # derivation for building the static hugo site. call this from gitlab
    packages.x86_64-linux = rec {
      website = pkgs.stdenv.mkDerivation {
        name = "website";
        src = self;
        buildPhase = ''
          mkdir -p themes
          ln -sn ${inputs.hugo-theme} themes/hugo-theme-cactus
	      ${pkgs.hugo}/bin/hugo
        '';
        installPhase = "cp -r public $out";
      };
      default = website;
    }; 
  };
}
  • L7-10: Providing my Hugo theme as an input, with L9 specifying that the git repo I’m referencing is just a regular repo (i.e. isn’t managed by a flake.nix)
  • L13: Output function declarion2 with parameters passed using @-pattern3 argument set notation
  • L14-16: let...in block to declare a shorthand for nixpkgs as pkgs within the output function; also so I don’t need to keep typing x86_64-linux
  • L20: declaration of website package, or packages.x86_64-linux.website, using the function stdenv.mkDerivation4
  • L24-25: buildPhase: symlink my hugo theme (via inputs param) into my hugo site’s themes folder
  • L26: buildPhase: run hugo command to generate a public folder5 to be picked up later in gitlab-ci.yml deploy step
  • L28: installPhase: iiuc this phase needs some derivation output results to exist in the nix store path (stored in $out) ; afaict I don’t care about this for Gitlab Pages purposes but I’ll copy the public folder into here so mkDerivation can succeed running?
  • L29: Declaration of default package (or packages.x86_64-linux.default) which points to the website package. This is why I put rec (“recursive attribute set”6) on L19, because default refers an attribute that was defined in the same scope.

Running this with nix build seems to work! I get a results folder (with the contents of the public folder I moved on L28) in my project directory that symlinks back to the nix store. This will be useful for calling within the .gitlab-ci.yml file I’ll need to deploy a Gitlab Page, but it doesn’t let me develop my hugo site locally. For that I need to define a dev shell7!

Note: I saw a lot of examples using flake-utils or flake-parts to clean away output declaration boilerplate, which I’ll likely end up doing too. But for baby’s first flake.nix I wanted to write things out the long way.

3: Run “hugo serve” in a dev shell with flake.nix

A development shell or devShell7 is an output hook for nix develop, and replaces shell.nix/nix-shell functionality. You can use it to describe a dev environment for running the thing you’re trying to build. In my case I want to be able to run hugo server in my dev shell so that I can locally dev on my hugo site.

Starting at L33, I’ll use the stdenv function mkShell8 to declare my shell:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
{
  description = "A flake for computers.lol, aka my lil hugo blog";
  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
    hugo-theme = {
      url = "github:monkeyWzr/hugo-theme-cactus";
      flake = false;
    };
  };
  outputs = inputs@{ self, nixpkgs, ... }:
  let 
    pkgs = import nixpkgs { system = "x86_64-linux"; };
  in
  {
    # derivation for building the static hugo site. call this 
    # flake tag in wherever i'm deploying from
    packages.x86_64-linux = rec {
      website = pkgs.stdenv.mkDerivation {
        name = "website";
        src = self;
        buildPhase = ''
          mkdir -p themes
          ln -sn ${inputs.hugo-theme} themes/hugo-theme-cactus
	  ${pkgs.hugo}/bin/hugo
        '';
        installPhase = "cp -r public $out";
      };
      default = website;
    }; 
    # get you a `nix develop` shell with hugo available and the 
    # hugo theme symlinked from nix store into {website_dir}/themes
    # so that `hugo server` actually works for local dev :3
    devShells.x86_64-linux.default = pkgs.mkShell {
      buildInputs = [ pkgs.hugo ];
      shellHook = ''
        mkdir -p themes
        ln -snf ${inputs.hugo-theme} themes/hugo-theme-cactus
      '';
    };
  };
}
  • L34: List any pkgs I want available in the devShell - just hugo in my case
  • L35-38: Set shellHook, which in my case sets up the hugo theme from my inputs

Now I can do:

nix develop
hugo server

:D

4. Publish to Gitlab

The .gitlab-ci.yaml to drive the flakes-backed build and deploy to Gitlab Pages looks like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
image: nixos/nix

pages:
  script:
    - nix --extra-experimental-features "flakes nix-command" build
    - mkdir public && cp -r result/* public/
  artifacts:
    paths: 
      - public
    untracked: true
  • L1: nix image!!
  • L5: Run nix build (with flags to enable experimental flakes)
  • L6: Okay wait-

this line kinda sucks because the git runner refuses to refer to an artifact that lives outside of the project’s own directory. The public folder contents that exist in the nix store path of the website package I just built on the previous line is an artefact that lives outside of the project’s own directory ;_; because it lives at /nix/store. To work around this, I have to recursive copy the contents (symlinked to the nix store within results/) back into the current directory. Maybe another approach would be to do something like nix develop && hugo serve but at this point I got sad and started googling GitHub Pages so I didn’t try it.

  • L8: Declare public folder in current directory as an artifact, because apparently artefacts can’t live in the nix store -_-
  • L8: The public folder isn’t tracked in my repo, so I specify that

Technically nix-backed, f u gitlab

So this works but the double copy step of the nix store results is kinda silly. But, this is good enough for now!