Configuring Hugo With Nix Flakes (like this blog)
This blog is built with Hugo, running on a NixOS machine hosted by Hetzner.
Working with Nix Flakes to develop, run, and deploy the blog turned out to be a fascinating experience. In this post,
I’ll walk you through how I set everything up by explaining the flake.nix
configuration I created. You can find the
current version of the file in use at GitHub.
Using Nix I can run build and run this blog easily on different operating systems and CPU architectures. For example, I develop this blog on an Intel macOS laptop and deploy it to a ARM server running NixOS.
This post does not cover the NixOS configuration for actually serving the website available. I will cover that in a future post.
Features#
- Make hugo available to run the blog locally on my machine
- Use hugo to generate the final static files to serve the blog
Flake structure#
Like any other flake, this one has inputs and outputs.
Inputs#
The inputs section defines the dependencies used to build the flake. These can be other flakes, but don’t necessarily need to be Flakes themselves.
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs";
utils.url = "github:numtide/flake-utils";
hugo-terminal = {
url = "github:panr/hugo-theme-terminal";
flake = false;
};
};
In the above section, you can see that I am pulling the theme I use for this blog
(Terminal) as an input. Note the flake = false
attribute letting Nix
know that this is a non-flake repository.
The other inputs are
NixOS/nixpkgs
, which is a huge collection of software packages that can be installed in Nix.numtide/flake-util
, a collection of utility functions making writing nix flakes easier.
Outputs#
This section is where the majority of the logic lives and defines the end results and what should be produced.
Wrapper#
outputs = { self, nixpkgs, utils, hugo-terminal, ... }:
utils.lib.eachDefaultSystem
(system:
let
pkgs = import nixpkgs {
inherit system;
};
in
{
This part begins the outputs block, where the following happens:
self, nixpkgs, utils, and hugo-terminal
are injected inputs, allowing access to the previously defined resources.utils.lib.eachDefaultSystem
ensures that this configuration is compatible with all supported systems (Linux, macOS, etc. and their associated CPU architectures). This enables you to use this flake on machines with different operating systems and CPU architectures.- The
let
expression imports nixpkgs for the current system, providing access to the available packages for that system.
Packages#
Packages are derivations (build outputs) that define the software and tools needed for the project.
In this flake, hugo-blog
is a package that describes how to build the static site using Hugo and the specified theme.
Packages are more general-purpose and can be reused in different contexts.
packages.hugo-blog = pkgs.stdenv.mkDerivation rec {
name = "hugo-blog";
src = self;
configurePhase = ''
mkdir -p "themes/terminal"
cp -r ${hugo-terminal}/* "themes/terminal"
'';
buildPhase = ''
${pkgs.hugo}/bin/hugo --minify
'';
installPhase = "cp -r public $out";
};
packages.default = self.packages.${system}.hugo-blog;
This section defines a package called hugo-blog using pkgs.stdenv.mkDerivation
function. This function builds the
package with a “standard environment” that provides a lot of common building tasks and lets you breakdown the package
into “phases”.
Here’s what’s happening:
- name: The package is named hugo-blog.
- src: The source code for the package is the current Flake (self).
- configurePhase: In this phase, the theme from hugo-terminal is copied into the project’s theme folder.
- buildPhase: Hugo is run with the
--minify
flag, which generates the static site. - installPhase: The generated files (from the public directory) are copied into the output directory (
$out
).$out
is a special variable referencing the package’s output directory.
The last line sets the hugo-blog
package, that we just created, as this system’s default package.
Apps#
Apps are executable programs or services that you can directly run. These are typically a higher-level abstraction than packages, meant to be easily runnable.
apps = rec {
build = utils.lib.mkApp { drv = pkgs.hugo; };
serve = utils.lib.mkApp {
drv = pkgs.writeShellScriptBin "hugo-serve" ''
${pkgs.hugo}/bin/hugo server -D
'';
};
newpost = utils.lib.mkApp {
drv = pkgs.writeShellScriptBin "new-post" ''
${pkgs.hugo}/bin/hugo new content posts/"$1".md
'';
};
default = serve;
};
Here, we define a few apps build
, newpost
, and serve
and set the last as the default app. With this definition
nix run
andnix run .#serve
will run thehugo server -D
command, letting me preview the blog post quickly.nix run .#build
will run thehugo
build process to generate static HTML pages, if I wanted to take a look at the final version of built pages.nix run .#newpost -- post-title
will create a new post for me by delegatingpost-title
to thehugo new content
command
Development environment#
The dev shell defines a shell environment for developers, pre-configured with the necessary tools and dependencies. It’s particularly useful for onboarding new contributors or ensuring that the development environment remains consistent across systems.
devShells.default =
pkgs.mkShell {
buildInputs = [ pkgs.hugo ];
shellHook = ''
mkdir -p themes
ln -sn "${hugo-terminal}" "themes/terminal"
'';
};
});
Here, the default develop environment includes the hugo
package so I can use the CLI to generate new content or pages.
The shellHook
attribute specifies what should occur when I run nix develop
. It will create the themes directory and
create a symbolic link to the hugo-terminal
directory that was pulled in as a dependency as described in the inputs
section.