Publishing a pelican website using nix

Posted on Sun 19 January 2025 in nix

This website is statically generated using pelican and development on the website is done via nix flakes. Here is how :

Overview

I use pelican to publish this website. Pelican is a python based command line tool to generate a static website. Pelican requires writing the articles in reStructuredText or Markdown and placing these files in a folder which I call content. To generate the website I simply run a command

pelican -r content -o output -s pelicanconf.py

I will not cover the details on how to install Pelican. In the above command I specify the source folder content, the output destination folder and finally a configuration file called pelicanconf.py that contain various website information like the name of website, the locale or the theming.

While pelican is simple, it still requires a lot of small steps every time you want to publish an article. Which is why I use nix to automate as much as possible to leave as little friction.

I will explain how I use Nix flakes to manage dependencies, build the website and run it locally. I will leave publishing for possible future article.

Creating a development environment (nix-shell)

The purpose of a nix shell is to have a shell environment where all the tooling is installed and one can develop various helper scripts (e.g. script to run the website locally).

First I create the flake.nix file:

{
    description = "Simple blog project";
    inputs = {
        nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11";
    }
    outputs = { self, nixpkgs }: let
        system = "x86_64-linux";
        pkgs = import nixpkgs { inherit system; }
        deps = with pkgs; [
            (python3.withPackages (ps: [ ps.pelican ps.markdown ]))
        ];
    in {
        devShells.${system} = pkgs.mkShell {
            buildInputs = deps;
        };
    };
}

Basically this tells how to make a shell environment with pelican installed and markdown support. It can be invoked like this:

nix develop
pelican -r ./content -o ./dev-output -s pelicanconf.py

However, this may not work for everyone because I use an external theme.

When it is not a flake project (i.e the project root does not have a flake.nix) we turn the external dependency into a derivation from scratch. This can be done using fetchFromGitHub:

{
    description = "Simple blog project";
    inputs = {
        nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11";
    }
    outputs = { self, nixpkgs }: let
        system = "x86_64-linux";
        pkgs = import nixpkgs { inherit system; }
        pelican-flex-theme = pkgs.fetchFromGitHub {
            owner = "alexandrevicenzi";
            repo = "Flex";
            rev = "master";
            sha256 = "sha256-I/WEqFMZbnJvX4NMeqB+o9LzD4im1BwOwgaqqDqbL2s=";
        };
        deps = with pkgs; [
            pelican-flex-theme
            (python3.withPackages (ps: [ ps.pelican ps.markdown ]))
        ];
    in {
        devShells.${system} = pkgs.mkShell {
            buildInputs = deps;
            shellHook = ''
              export PELICAN_THEME=${pelican-flex-theme}
            '';
        };
    };
}

Now after running nix develop I can access the path to the theme via the environment variable PELICAN_THEME (which happens to be what I use in pelicanconf.py).

Building the website

Next step is to implement the build step. This is done by specifying the packages.${system}.default output derivation. Now
by building the website, I mean generating the static files, not running a local instance. Running the website locally will be explained later.

{
    description = "Simple blog project";
    inputs = {
        nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11";
    }
    outputs = { self, nixpkgs }: let
        system = "x86_64-linux";
        pkgs = import nixpkgs { inherit system; }
        pelican-flex-theme = pkgs.fetchFromGitHub {
            owner = "alexandrevicenzi";
            repo = "Flex";
            rev = "master";
            sha256 = "sha256-I/WEqFMZbnJvX4NMeqB+o9LzD4im1BwOwgaqqDqbL2s=";
        };
        deps = with pkgs; [
            pelican-flex-theme
            (python3.withPackages (ps: [ ps.pelican ps.markdown ]))
        ];
    in {
        devShells.${system} = pkgs.mkShell {
            buildInputs = deps;
            shellHook = ''
              export PELICAN_THEME=${pelican-flex-theme}
            '';
        };
        packages.${system}.default = pkgs.stdenv.mkDerivation {
            name = "my-website";
            src = ./.;
            nativeBuildInputs = deps;
            buildPhase = ''
                export PELICAN_THEME=${pelican-flex-theme}
                pelican content -o output -s pelicanconf.py
            '';
            installPhase = ''
                mkdir -p $out
                cp -r output/* $out
            '';
        };
    };
}

Just like a Makefile we specify how to build the project using the buildPhase and how to install using the installPhase; all of this within stdenv.mkDerivation.

There is however one problem with this derivation. It fetches all the files instead of only the files it needs. To fix this I use gitignore.nix which filters the source folder using the .gitignore file.

{
    description = "Simple blog project";
    inputs = {
        nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11";
        gitignore = {
            url = "github:hercules-ci/gitignore.nix";
            inputs.nixpkgs.follows = "nixpkgs";
        };
    }
    outputs = { self, nixpkgs, gitignore }: let
        # ... previous code omitted ...
        deps = with pkgs; [
            pelican-flex-theme
            (python3.withPackages (ps: [ ps.pelican ps.markdown ]))
        ];
        inherit (gitignore.lib) gitignoreSource;
    in {
        # ... previous code omitted ...
        packages.${system}.default = pkgs.stdenv.mkDerivation {
            name = "my-website";
            src = gitignoreSource ./.;
            nativeBuildInputs = deps;
            buildPhase = ''
                export PELICAN_THEME=${pelican-flex-theme}
                pelican content -o output -s pelicanconf.py
            '';
            installPhase = ''
                mkdir -p $out
                cp -r output/* $out
            '';
        };
    };
}

Now you can build the website by running the command

nix build

You can access the website in the result folder (which is a link to the created derivation in the nix store).

Launching the website locally

To run the locally we define an application in the Nix flake file. To run the website locally will be done in three steps : 1. Create a shell script to run the website (using nix of course) 2. Define an application in flake.nix 3. Make the shell script and the package a dependency of the application

Here is how it is done:

{
    # ... inputs ...
    outputs = { self, nixpkgs, gitignore }: let
        # ... previous code omitted ...
        deps = with pkgs; [
            pelican-flex-theme
            (python3.withPackages (ps: [ ps.pelican ps.markdown ]))
        ];
        inherit (gitignore.lib) gitignoreSource;
        static-website = pkgs.stdenv.mkDerivation {
            name = "my-website";
            src = gitignoreSource ./.;
            nativeBuildInputs = deps;
            buildPhase = ''
                export PELICAN_THEME=${pelican-flex-theme}
                pelican content -o output -s pelicanconf.py
            '';
            installPhase = ''
                mkdir -p $out
                cp -r output/* $out
            '';
        };
    in {
        # ... previous code omitted ...
        packages.${system}.default = static-website;
        apps.${system}.default = let
            serv = pkgs.writeShellApplication {
                name = "serve";
                runtimeInputs = [ static-website ] ++ deps;
                text = ''
                    pelican --relative-urls -p 8080 -l ${static-website}
                '';
            };
        in {
            type = "app";
            program =" ${serv}/bin/serve";
        }
    };
}

In the above code we refactor the code by moving the package definition into the static-website variable in the let ... in ... clause. We also create a subcomponent as a shell application derivation called serv which simply run the pelican local server.

To run the website locally simply do

nix run

resulting_image

Updating the website

Now if you make changes to your website (e.g. add a new article markdown file) you may not see the changes after running nix run or nix build, the most likely reason for this is that flakes track the latest state of the git repository; meaning you may need to git add files for changes to be reflected.

Summary

In this article I show how to use Nix flakes to manage the development life-cycle of a statically generated website using pelican.

Nix flakes are used to manage the development environment (like virtual environments in Python), build the website (like in make build) and run the website locally (like a docker run).

For more articles about Nix and NixOS the following RSS feed is available over here.