How I build my website

  • 1st Oct 2024
  •  • 
  • 4 min read

My website is made with Zola and built with a Nix Flake. I also build a PDF version of my CV with Pandoc automatically. It's hosted in GitHub pages, and built with a GH action.

I've taken the Nix pill, and while it's sometimes hard to swallow, and the UX isn't great, the benefits are pretty amazing. It allows for everything to be declarative, so the builds are always reproducible.

Since it's a standard Zola project - which uses the tabi theme, it has content, static, templates, etc... directories, which the command zola build uses to build a website and put it in a public directory.

This is the flake.nix file. A flake, among other things, declares inputs and locks them, so you get reproducibility in your builds.

{
  description = "Personal website for praguevara";

  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05";
    utils.url = "github:numtide/flake-utils";
    theme = {
      url = "github:welpo/tabi";
      flake = false;
    };
  };

  outputs =
    inputs@{
      self,
      nixpkgs,
      utils,
      ...
    }:
    utils.lib.eachSystem
      [
        utils.lib.system.x86_64-darwin
        utils.lib.system.x86_64-linux
        utils.lib.system.aarch64-darwin
        utils.lib.system.aarch64-linux
      ]
      (
        system:
        let
          pkgs = import nixpkgs {
            inherit system;
          };
        in
        {
          packages.website = pkgs.stdenv.mkDerivation {
            name = "website";
            src = self; # 'self' refers to the flake's source

            buildInputs = [
              pkgs.nodePackages.prettier
              pkgs.pandoc
              pkgs.zola
              (pkgs.texlive.combine {
                inherit (pkgs.texlive)
                  scheme-basic
                  etoolbox
                  enumitem
                  underscore
                  parskip
                  xcolor
                  sectsty
                  ;
              })
            ];

            buildPhase = ''
              # Generate the PDF from cv.md
              pandoc ${self}/content/cv/cv.md -o static/pablo_ramon_guevara_cv.pdf --template ${self}/jb2resume.latex

              mkdir -p themes
              ln -s ${inputs.theme} themes/tabi

              # Copy config.toml directly from src
              cp ${self}/config.toml config.toml

              cp -r ${self}/content/ content/
              cp -r ${self}/templates/ templates/
              cp -r ${self}/static/ static/

              zola build
              prettier -w public '!**/*.{js,css}'
            '';

            installPhase = ''
              mkdir -p $out
              cp -r public/. $out/
            '';
          };

          defaultPackage = self.packages.${system}.website;

          apps.default = utils.lib.mkApp { drv = self.packages.${system}.website; };

          devShell = pkgs.mkShell {
            buildInputs = [
              pkgs.nixpkgs-fmt
              pkgs.zola
              pkgs.pandoc
              pkgs.nodePackages.prettier
            ];

            shellHook = ''
              mkdir -p themes
              ln -s ${inputs.theme} themes/tabi
            '';
          };
        }
      );
}

The relevant sections of the flake are:

  • Inputs: the whole nixpkgs version 24.05, from which I grab prettier, pandoc, zola and texlive. I also use flake-utils, and a custom input called theme, which points to the tabi repository even though it's not a flake.

  • Derivation: the part that's in between curly brackets after mkDerivation. In the buildInputs I define what packages are necessary to build the site. Then, the magic happens in the buildPhase, where I generate the CV using pandoc, copy the relevant files and directories and build the site. After it's built, the installPhase copies the public directory to the $out variable, which points to the output of the derivation.

  • Development shell: this section declares what packages are available and what actions are run when you run the nix develop command. This allows me to run zola serve to test changes. I also use direnv with use flake; in my .envrc, which runs the command automatically when I cd into it.

When I run a git push, the following GitHub action is triggered:

name: Build and Deploy to GitHub Pages

on:
  push:
    branches:
      - master
  workflow_dispatch:

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout Repository
        uses: actions/checkout@v4

      - name: Install Nix
        uses: cachix/install-nix-action@v27
        with:
          github_access_token: ${{ secrets.GITHUB_TOKEN }}
          nix_path: nixpkgs=channel:nixos-24.05
          extra_nix_config: |
            trusted-public-keys = hydra.iohk.io:f/Ea+s+dFdN+3Y/G+FDgSq+a5NEWhJGzdjvKNGv0/EQ= cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=
            substituters = https://hydra.iohk.io https://cache.nixos.org/

      - name: Build Site with Nix
        run: nix build .#website

      - name: Deploy to GitHub Pages
        uses: peaceiris/actions-gh-pages@v3
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          publish_dir: ./result
          publish_branch: gh-pages
          force_orphan: true

It takes 2 minutes to run.

Nix really shines here. Install, run nix build .#website and deploy. You always use the same command, regardless of the complexity of the build process.

By the way, I took inspiration from this post to create my flake originally. You should give it a look (it's quite short).