Luka builds things

Create a static blog with Nix

• Published: • Updated:

This blog has been written with Eleventy. It handles the static site generation part, like converting markdown to HTML, routes and so on. Classic static site generator stuff. Their slogan is "Eleventy is a simpler static site generator" and so far I think it lives up to it.

What I don't like about it is that you need to install Node.js on your machine to get it to work. I seriously considered using Zola instead for this reason alone. In the end I decided for Eleventy because it's more mature and has a bigger community. The documentation is also more complete I feel.

If you want to skip ahead and take a look at the resulting repository, you can find it at tech-blog/create-a-static-blog-with-nix.

Also, there is also a continuation of this post in Caching adventures with Caddy and Nix. It's where we investigate the (tricky) caching issues with this setup and fix them.

Bootstrapping the project

So, given that I decided to go with Eleventy, that means we need to have Node.js installed. I don't want to have it installed system-wide just for Eleventy to work, so I decided to reach for the power of Nix.

To be fair, I would've used Nix even if I went with some other static site generator, but using something like Eleventy means I get to learn how to use Node packages with Nix.

First step is creating a git repository (git init) and a new flake (nix flake init). I like to use flake-utils, so I add that to the inputs and adjust the flake to use it. Run nix flake update to get the flake.lock file and then let's do an initial commit (git add . && git commit -m "Initial commit").

Now we have a good starting point.

Commit: Initial commit

{
  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
    flake-utils.url = "github:numtide/flake-utils";
  };

  outputs =
    {
      self,
      nixpkgs,
      flake-utils,
      ...
    }@inputs:
    flake-utils.lib.eachDefaultSystem (
      system:
      let
        pkgs = nixpkgs.legacyPackages.${system};
      in
      {
        devShells.default = pkgs.mkShell {
          buildInputs = [];
          shellHook = "";
        };
      }
    );
}

Development shell

Next step I always do is add a development shell.

If you're not familiar with that, here is a nice write up (I will use nix develop). But the short summary is that it provides a sort of virtual environment with the tools you need to interact with the project. It is super simple for other people to use and I think it is way better than using docker/podman for sharing development environments.

I just can't live without just, the task runner. I always use it to run tasks in the project, but not everyone has it installed.

# This command will output the path of the just command if you have it installedwhich just
/Users/luka/.nix-profile/bin/just

You can see I already have it installed system-wide, but I always try to include all project dependencies in my development shell, just in case anyone else wants to interact with the project.

Let's modify the flake.nix to add it.

diff --git a/flake.nix b/flake.nix
index 58dadad..fffb25c 100644
--- a/flake.nix
+++ b/flake.nix
@@ -18,7 +18,9 @@
       in
       {
         devShells.default = pkgs.mkShell {
-          buildInputs = [];
+          buildInputs = with pkgs; [
+            just
+          ];
           shellHook = "";
         };
       }

Now, we have to activate the development shell. We can do that by running nix develop.

❯ nix develop
[...]which just
/nix/store/w0s0f89zcpwl9ipygxiihba0j6fgwnyq-just-1.40.0/bin/just
❯ #press CTRL+D to exit this development shell or run exit command
exitwhich just
/Users/luka/.nix-profile/bin/just

As you can see above, we get a just binary, that is exactly defined by the nixpkgs commit our flake.lock is pinned to. When you update the flake (nix flake update) your project binaries will also update. In my case the binary provided by the system and the one provided by the development shell one are the same, but that will not always hold.

One thing you might've noticed if you're using zsh is that the new shell is pure bash. Use nix develop -c $SHELL to retain your shell instead.

Anyway, now, when you want to work on your blog/project, you need to cd into the project root and run nix develop. If you're using the excellent direnv, you can also paste use flake in your .envrc, run direnv allow and now whenever you cd into this repo, you'll get dropped into the development shell automatically.

Now let's run just --init to create a justfile and edit to something like:

# The default command to run when ran with just 'just'
[group('General')]
default: help

# Print the available commands
[group('General')]
help:
    @just --list

# Update project dependencies
[group('General')]
update:
    nix flake update

Now when you run just you get a nice list of available recipes you can choose from.

❯ just
Available recipes:
    [General]
    default # The default command to run when ran with just 'just'
    help    # Print the available commands
    update  # Update project dependencies

I think it's time we commit these changes and work on getting Eleventy into our development shell.

Commit: Add just and justfile

Add Eleventy

Now here comes the tricky part. We need to add Eleventy to our project dependencies.

As mentioned earlier, I don't really want to install Node.js to my machine, so let's see if we can find Eleventy in Nixpkgs repository and use that instead. Searching nixpkgs for the Eleventy package, we find nothing (as of 2025-06-22 at least). What now?

Well, we can package it ourselves and add it to our repository as a dependency that way. This being Nix, it took me quite a while to figure out how to do all of this. But I persevered, because that is what I do and I like doing things the hard way I guess. Anyway after a few hours of searching the web and scouring the nixpkgs source code here is what I came up with.

I found the pkgs.buildNpmPackage function (source). It seems like a perfect candidate for us to use. To build the NPM package it needs its source code, so let's get that first.

You do that by adding it as an input to our flake. All inputs are git repositories and Nix manages them for us.

diff --git a/flake.nix b/flake.nix
index fffb25c..18aea66 100644
--- a/flake.nix
+++ b/flake.nix
@@ -2,6 +2,11 @@
   inputs = {
     nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
     flake-utils.url = "github:numtide/flake-utils";
+    eleventy-src = {
+      url = "github:11ty/eleventy";
+      flake = false;
+    };
+
   };

   outputs =

Since the Eleventy repository does not contain a flake, you need to set flake = false;.

Now let's pass that to buildNpmPackage to build it for us. Let's create a pkgs folder and place a 11ty-eleventy.nix file there with the following contents:

# We'll pass in the pkgs and `src`
{ pkgs, src }:
pkgs.buildNpmPackage {
  # Package name
  pname = "eleventy";
  version = "1.0.0";
  src = src;
  npmDepsHash = pkgs.lib.fakeHash; # We'll replace this soon
  dontNpmBuild = true;
  meta = with pkgs.lib; {
    description = "A simpler static site generator";
    homepage = "https://www.11ty.dev/";
    license = licenses.mit;
  };
}

We set dontNpmBuild to true because the package.json in the Eleventy repository does not contain a build script.

And use it in our flake.nix:

diff --git a/flake.nix b/flake.nix
index 18aea66..58dd8c7 100644
--- a/flake.nix
+++ b/flake.nix
@@ -20,6 +20,10 @@
       system:
       let
         pkgs = nixpkgs.legacyPackages.${system};
+        eleventy = import ./pkgs/11ty-eleventy.nix {
+          inherit pkgs;
+          src = inputs.eleventy-src;
+        };
       in
       {
         devShells.default = pkgs.mkShell {

Make sure that you stage the changes with git (git add .), otherwise you'll get an error that [...]source/pkgs/pkgs/11ty-eleventy.nix' does not exist when Nix tries to use it.

Cool, now we have everything set up, all we need is to expose it in our outputs.

diff --git a/flake.nix b/flake.nix
index 58dd8c7..e45810f 100644
--- a/flake.nix
+++ b/flake.nix
@@ -26,6 +26,7 @@
         };
       in
       {
+        packages.eleventy = eleventy;
         devShells.default = pkgs.mkShell {
           buildInputs = with pkgs; [
             just

nix flake show command shows us the outputs of our flake. I've edited it for brevity a bit, but yours should look similar. We can see that we have one devShell (default) and one package (eleventy).

❯ nix flake show
warning: Git tree '[...]/create-a-static-blog-with-nix' is dirty
git+file://[...]/create-a-static-blog-with-nix
├───devShells
│   ├───aarch64-darwin
│   │   └───default: development environment 'nix-shell'
[...]
└───packages
    ├───aarch64-darwin
    │   └───eleventy: package 'eleventy-1.0.0'
[...]

Now let's try and build it.

❯ nix build .#eleventy
warning: Git tree '[...]/create-a-static-blog-with-nix' is dirty
error: hash mismatch in fixed-output derivation '/nix/store/kx4k4fh0hh7v0apnxc9xrnca6g71nwrh-eleventy-1.0.0-npm-deps.drv':
         specified: sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
            got:    sha256-LGdCM1gjt3hRn7BiIlbA4e2HOiQ6e/qkAtWp0Qwn+PE=
error: 1 dependencies of derivation '/nix/store/cbc08ym28sgzz123gjf06q0k26j2sxc1-eleventy-1.0.0.drv' failed to build

If you've followed along this is what you should see, just the hashes will be different. We expected this error to happen, since we used a fakeHash. If we replace the fake hash with the correct one the package should build.

IMPORTANT: You should use the hash that you got, don't copy mine. As you'll get another hash mismatch.

diff --git a/pkgs/11ty-eleventy.nix b/pkgs/11ty-eleventy.nix
index c0692bf..c6adbfa 100644
--- a/pkgs/11ty-eleventy.nix
+++ b/pkgs/11ty-eleventy.nix
@@ -5,7 +5,7 @@ pkgs.buildNpmPackage {
   pname = "eleventy";
   version = "1.0.0";
   src = src;
-  npmDepsHash = pkgs.lib.fakeHash; # We'll replace this soon
+  npmDepsHash = "sha256-LGdCM1gjt3hRn7BiIlbA4e2HOiQ6e/qkAtWp0Qwn+PE=";
   dontNpmBuild = true;
   meta = with pkgs.lib; {
     description = "A simpler static site generator";

Running nix build .#eleventy now completes successfully and gives us a result file in the root of our repo. If we take a look at what it points to, we can see it points at /nix/store/[...]-eleventy-1.0.0.

❯ readlink result
/nix/store/hqa8c57nbl7rlimi1fa611r64lp0mp4w-eleventy-1.0.0

And taking a deeper look at the result, we can see that it contains a binary (result/bin/eleventy) and node_modules.

❯ tree result | head
result
├── bin
│   └── eleventy
└── lib
    └── node_modules
        └── @11ty
            └── eleventy
                ├── cmd.cjs
                ├── CODE_OF_CONDUCT.md
                ├── LICENSE

Let's try executing that binary:

❯ ./result/bin/eleventy --version
3.1.2-beta.2

❯ ./result/bin/eleventy --help | head
Usage: eleventy
       eleventy --input=. --output=./_site
       eleventy --serve

Arguments:

     --version

     --input=.
       Input template files (default: `.`)

Works like a charm! What a great moment.

To get that binary available in our development shell, we need to add the derivation (what Nix calls packages) as a build input. Nix will then add the bin folder of that derivation to our path.

diff --git a/flake.nix b/flake.nix
index e45810f..29adafc 100644
--- a/flake.nix
+++ b/flake.nix
@@ -30,6 +30,7 @@
         devShells.default = pkgs.mkShell {
           buildInputs = with pkgs; [
             just
+            eleventy
           ];
           shellHook = "";
         };

Let's enter the shell and try it out:

❯ nix develop

❯ eleventy --version
3.1.2-beta.2

❯ which eleventy
/nix/store/hqa8c57nbl7rlimi1fa611r64lp0mp4w-eleventy-1.0.0/bin/eleventy

We can see it's exactly the same Nix store path as the one we got when we ran nix build .#eleventy.

We could call it quits at this point, but I don't like that the version we specified in the package does not match the actual eleventy version. Another thing that bothers me is that when the eleventy repository updates (and we run nix flake update to get that update), we'll have to manually update the hash to the new one.

Ugh, manual work! I would much rather invest a few more hours into this right now, then ever have to worry about manual work.

Relevant XKCD as always: Is it worth the time?. In my case I think the answer would be a no, but hopefully for you it might be a yes.

Anyway, I had to figure out how this hash is calculated. Turns out there is a package called prefetch-npm-deps that given a package-lock.json file outputs the desired hash. So let's use it to calculate the hash.

Let's create a new justfile recipe called update-eleventy and let's not forget to add the binaries we use in the recipe to our development shell.

diff --git a/flake.nix b/flake.nix
index 29adafc..dd6cc6d 100644
--- a/flake.nix
+++ b/flake.nix
@@ -31,6 +31,9 @@
           buildInputs = with pkgs; [
             just
             eleventy
+            prefetch-npm-deps
+            jq
+            curlMinimal
           ];
           shellHook = "";
         };
diff --git a/justfile b/justfile
index 6776929..c7450cb 100644
--- a/justfile
+++ b/justfile
@@ -11,3 +11,13 @@ help:
 [group('General')]
 update:
     nix flake update
+
+# Update packages sha256 and version
+update-eleventy:
+    #!/bin/bash
+    pkgs=$(pwd)/pkgs
+    cd $(mktemp -d)
+    curl -f https://raw.githubusercontent.com/11ty/eleventy/refs/heads/main/package-lock.json -o package-lock.json
+    prefetch-npm-deps package-lock.json > $pkgs/11ty-eleventy.sha256
+    cat package-lock.json | jq --raw-output ".version" > $pkgs/11ty-eleventy.version
+

Let me explain the update-eleventy recipe, line by line:

To get the new dependencies in your development shell, make sure you leave (CTRL+D) and re-enter the development shell (nix develop). Now we can run just update-eleventy. You'll see that two new files appeared in the pkgs folder, a 11ty-eleventy.sha256 and a 11ty-eleventy.version. If you open them, you'll see what you expect to see from the filename, a hash and a version.

Let's use these two new files in our pkgs/11ty-eleventy.nix file. I used a suffix of main for the version, since we're using that branch of the Eleventy repo. A better approach would be to use a tagged release instead.

diff --git a/pkgs/11ty-eleventy.nix b/pkgs/11ty-eleventy.nix
index c6adbfa..240d557 100644
--- a/pkgs/11ty-eleventy.nix
+++ b/pkgs/11ty-eleventy.nix
@@ -3,9 +3,9 @@
 pkgs.buildNpmPackage {
   # Package name
   pname = "eleventy";
-  version = "1.0.0";
+  version = (builtins.readFile ./11ty-eleventy.version) + "main";
   src = src;
-  npmDepsHash = "sha256-LGdCM1gjt3hRn7BiIlbA4e2HOiQ6e/qkAtWp0Qwn+PE=";
+  npmDepsHash = builtins.readFile ./11ty-eleventy.sha256;
   dontNpmBuild = true;
   meta = with pkgs.lib; {
     description = "A simpler static site generator";

And after adding both new files to our git (git add pkgs), we can try and build eleventy again with nix build .#eleventy. Success!

Taking a look at the nix store, we can see it matches the version reported by eleventy.

❯ readlink result
/nix/store/j7kr145ws70in0sbj91yn2ba0m64kmih-eleventy-3.1.2-beta.2-main

❯ result/bin/eleventy --version
3.1.2-beta.2

One thing to note here is that if you only run just update-eleventy and don't run just update (nix flake update alias), the hash will at some point go out of sync, since flake.lock will be frozen in time and so will the src that we're passing into pkgs/11ty-eleventy.nix, but the package-lock.json that we're fetching will not be. You'll get a hash mismatch and you'll have to update the flake.lock file with just update.

So let's fix that and make sure both are run at the same time.

diff --git a/justfile b/justfile
index c7450cb..8de6672 100644
--- a/justfile
+++ b/justfile
@@ -9,11 +9,11 @@ help:

 # Update project dependencies
 [group('General')]
-update:
+update: _update-eleventy
     nix flake update

 # Update packages sha256 and version
-update-eleventy:
+_update-eleventy:
     #!/bin/bash
     pkgs=$(pwd)/pkgs
     cd $(mktemp -d)

So now whenever you run just update, it will also run _update-eleventy, thus they will never be out of sync. I've also made the update-eleventy recipe a private one by prepending _ in front of it.

One last thing before we git commit, let's ignore the result in our .gitignore file (echo "result" > .gitignore).

Commit: Add @11ty/eleventy package to dev shell

Phew! That was quite some work we put in. Now let's build our static site.

Build with Eleventy

Creating an Eleventy generated static site is pretty straightforward, they really do live up to their slogan.

Let's create an index.html file in our repo root.

<!DOCTYPE html>
<html>
<head>
    <title>Hello World</title>
</head>
<body>
    <h1>Hello, World!</h1>
</body>
</html>

That should do it.

Now let's enter our development shell and run eleventy to build the static site.

(I also gitignored _site at this point echo "_site" >> .gitignore).

❯ eleventy
[11ty] Writing ./_site/index.html from ./index.html (liquid)
[11ty] Wrote 1 file in 0.04 seconds (v3.1.2-beta.2)

Looks good! Let's also try if live development works.

❯ eleventy --serve
[11ty] Writing ./_site/index.html from ./index.html (liquid)
[11ty] Wrote 1 file in 0.05 seconds (v3.1.2-beta.2)
[11ty] Watching…
[11ty] Server at http://localhost:8080/
[11ty] File changed: ./index.html
[11ty] Writing ./_site/index.html from ./index.html (liquid)
[11ty] Wrote 1 file in 0.01 seconds (v3.1.2-beta.2)
[11ty] Watching…

I edited the index.html and the page updated by itself, awesome!

I think we're done here. git commit

Commit: Add index.html

Now let's build the site, with Nix.

Build with Nix

You might be wondering why build with Nix, didn't we just build with Eleventy? Can't I just take the files in _site, copy them to some hosting site and serve them?

Of course. That would be perfectly fine and I won't hold it against you if you want to stop here and do that. Congrats on your new static site!

But if you have NixOS as your server and you're like me and dislike manual work, then continue reading and I will show you how Nix wants you to do this. And I promise it will be easier this time!

Alrighty!

diff --git a/flake.nix b/flake.nix
index dd6cc6d..b31a478 100644
--- a/flake.nix
+++ b/flake.nix
@@ -24,9 +24,24 @@
           inherit pkgs;
           src = inputs.eleventy-src;
         };
+
+        # The static site
+        site = pkgs.stdenv.mkDerivation {
+          pname = "my-static-site";
+          version = "1.0.0";
+          src = ./.;
+          buildInputs = [ eleventy ];
+          buildPhase = "eleventy";
+          installPhase = ''
+            mkdir -p $out/
+            cp -r _site/* $out/
+          '';
+        };
       in
       {
         packages.eleventy = eleventy;
+        packages.site = site;
+        packages.default = site;
         devShells.default = pkgs.mkShell {
           buildInputs = with pkgs; [
             just

There is a couple of things going on here.

First, we created a new derivation/package by calling mkDerivation and assigned to a variable called site. In Nix-land almost everything is a derivation, you get used to it.

We set a few things for this derivation:

And then we make the package available as an output of our flake. Let's take a look.

❯ nix flake show
[...]
git+file://[...]/create-a-static-blog-with-nix
├───devShells
│   ├───aarch64-darwin
│   │   └───default: development environment 'nix-shell'
[...]
└───packages
    ├───aarch64-darwin
    │   ├───default: package 'my-static-site-1.0.0'
    │   ├───eleventy: package 'eleventy-3.1.2-beta.2-main'
    │   └───site: package 'my-static-site-1.0.0'
[...]

We've made it available under two different names site and default. The default package is a bit special in the sense that you can build it simply with nix build. Let's try it!

❯ nix build
❯ readlink result
/nix/store/25m04s0xmfz2cwlbj1nq7iw9fas9h3jy-my-static-site-1.0.0
❯ cat result/index.html
<!DOCTYPE html>
<html>
<head>
    <title>Hello World</title>
</head>
[...]

And there you have it! You've built the site with Nix! That wasn't so bad, was it?

Let's git commit and continue on to deploying the site.

Commit: Build the site with nix

Deploy!

Now we can deploy it! Woo!

So what we'll do is:

In this section I will just present the end result, as there are many ways to achieve the same thing and a lot of it will depend on your existing/desired setup.

Before we proceed, we'll need to push the static site repository to a remote. Instructions on how to do this will again vary depending on the forge you're using. I use a self-hosted git forge (Forgejo), so I:

For convenience I created a sub-directory nixos-configuration in our static site repository to contain the NixOS configuration, but you should create a new repository to contain the configuration for your NixOS server.

Here are the files that I added to that directory:

❯ tree nixos-configuration
nixos-configuration
├── flake.lock
├── flake.nix
├── justfile
└── my-static-site.nix

1 directory, 4 files

Here is the flake.nix. flake.lock will be created for you once you run some nix command (or if you want to mirror my example 100%, you can copy it from the post repository).

I created a separate module file for our static site, just to demonstrate how to pass the flake inputs to modules as an argument (see specialArgs below).

There is also one (inline) module that handles things needed to run this configuration as a VM. Keep in mind the configuration as-is only works on x86_64-linux systems.

{
  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
    our-site = {
      url = "git+ssh://git@git.kalu.blue/tech-blog/create-a-static-blog-with-nix.git?ref=main&shallow=1";
      inputs.nixpkgs.follows = "nixpkgs"; # Will use the same Nixpkgs as the NixOS system
    };
  };

  outputs =
    { self, nixpkgs, ... }@inputs:
    {
      nixosConfigurations.my-server-name = nixpkgs.lib.nixosSystem {
        specialArgs = {
          # These will be passed as arguments to the modules
          inherit inputs;
        };

        modules = [
          # Essential for test VM
          {
            virtualisation.vmVariant = {
              virtualisation = {
                memorySize = 2048;
                cores = 2;
                # Disable graphics to avoid gtk error
                graphics = false;
              };
            };

            # Create a user, so we can login and test
            users.users.test = {
              isNormalUser = true;
              # Never use this, use `hashedPassword` instead
              initialPassword = nixpkgs.lib.mkForce "123123";
              # So we can use `sudo` mainly for `sudo shutdown now` and to be able to debug `caddy`
              group = "wheel";
            };
          }
          # Some configuration
          {
            nixpkgs.hostPlatform = "x86_64-linux";
            system.stateVersion = "25.11";
          }
          ./my-static-site.nix
        ];
      };
    };
}

Then the module my-static-site.nix.

We have to enable the caddy service and configure one virtual host (example.com) with some configuration that will set it to serve files from our static site package. We also add some gzip compression.

As a bonus, I also included a curl-site shell alias, which you can use in the VM to view our static site main page. I think it also nicely demonstrates how simple it is to use one variable to configure two vastly different systems (web server and shell aliases).

{ inputs, pkgs, ... }:
let
  site-url = "example.com";
in
{
  services.caddy = {
    enable = true;
    # Needs `http://` prefix so that it does not try to request TLS certificates and redirect to 443
    virtualHosts."http://${site-url}".extraConfig = ''
      file_server
      root * ${inputs.our-site.packages."${pkgs.system}".default}
      encode gzip
    '';
  };

  environment.shellAliases = {
    # Test our site
    curl-site = "curl -H \"Host: ${site-url}\" localhost";
  };
}

This configuration will start Caddy and set it serve our static site. It will only be available on localhost, since we have not opened any firewall ports (e.g. 80 and 443). But it was enough for me to start the VM and verify that everything works as intended.

I've also added a just vm recipe (runs nix run ".#nixosConfigurations.my-server-name.config.system.build.vm"). I think its really neat how simple it is to start a full-blown virtual machine with Nix.

Anyway enter the nixos-configuration folder and run just vm. It will build and start the virtual machine and after a while a login screen will be presented. We have configured a user test with a plain-text password 123123. Use that to log in.

Now we can inspect the system:

cd nixos-configuration
❯ just vm
nix run ".#nixosConfigurations.my-server-name.config.system.build.vm"

[...]
<<< Welcome to NixOS 25.11.20250618.5395fb3 (x86_64) - ttyS0 >>>

Run 'nixos-help' for the NixOS manual.

nixos login: test
Password:

[test@nixos:~]$ systemctl status caddy
● caddy.service - Caddy
     Loaded: loaded (/etc/systemd/system/caddy.service; enabled; preset: ignored)
    Drop-In: /nix/store/bg47546cl8k10w06wg3i002frn2c422f-system-units/caddy.service.d
             └─overrides.conf
     Active: active (running) since Sun 2025-06-22 18:06:27 UTC; 1min 31s ago
 Invocation: 82109b58487c4df48a9d15ba2a958d7e
       Docs: https://caddyserver.com/docs/
   Main PID: 873 (caddy)
         IP: 0B in, 0B out
         IO: 40K read, 8K written
      Tasks: 8 (limit: 2343)
     Memory: 32.6M (peak: 33M)
        CPU: 992ms
     CGroup: /system.slice/caddy.service
             └─873 /nix/store/9820hkh6vc96hjmbqpi9xzfjdrgsw17i-caddy-2.10.0/bin/caddy run --config /etc/caddy/caddy_config --adapter ca>

Jun 22 18:06:25 nixos systemd[1]: Starting Caddy...
Jun 22 18:06:27 nixos caddy[873]: {"level":"info","ts":1750615587.5026293,"msg":"maxprocs: Leaving GOMAXPROCS=2: CPU quota undefined"}
Jun 22 18:06:27 nixos caddy[873]: {"level":"info","ts":1750615587.5131006,"msg":"GOMEMLIMIT is updated","package":"github.com/KimMachin>
Jun 22 18:06:27 nixos caddy[873]: {"level":"info","ts":1750615587.5194187,"msg":"using config from file","file":"/etc/caddy/caddy_confi>
Jun 22 18:06:27 nixos caddy[873]: {"level":"info","ts":1750615587.5768142,"msg":"adapted config to JSON","adapter":"caddyfile"}
Jun 22 18:06:27 nixos caddy[873]: {"level":"info","ts":1750615587.6890154,"msg":"serving initial configuration"}
Jun 22 18:06:27 nixos systemd[1]: Started Caddy.

[test@nixos:~]$ cat /etc/caddy/caddy_config
{
        log {
                level ERROR
        }
}

http://example.com {
        log {
                output file /var/log/caddy/access-http:__example.com.log
        }

        file_server
        root * /nix/store/cr5wkgbijfhhp0dy1qm2k30wx8j83bzh-my-static-site-1.0.0
        encode gzip
}

[test@nixos:~]$ cat /nix/store/cr5wkgbijfhhp0dy1qm2k30wx8j83bzh-my-static-site-1.0.0/index.html
<!DOCTYPE html>
<html>
<head>
    <title>Hello World</title>
</head>
<body>
    <h1>Hello, World!</h1>
    <p>Eleventy rocks</p>
</body>
</html>

[test@nixos:~]$ curl-site
<!DOCTYPE html>
<html>
<head>
    <title>Hello World</title>
</head>
<body>
    <h1>Hello, World!</h1>
    <p>Eleventy rocks</p>
</body>
</html>

[test@nixos:~] sudo shutdown now

Everything is working as expected. Hooray!

Commit: Add an example nixos config using this site

Conclusion

We've successfully created a fully reproducible static blog setup using Nix and Eleventy. What started as avoiding a simple Node.js installation turned into a powerful development workflow that demonstrates the strength of Nix's approach to package management and deployment.

While this approach required more upfront investment than a traditional Node.js setup, we now have a system that "just works" across different machines and time. No more "works on my machine" problems, no dependency conflicts, and deployments are as simple as pointing to a git commit.

Next steps

Here are a few things you might want to do next: