Build a blog with Nix

Post on September 13, 2020 Updated on Sep 26, 2020
nix, haskell, hakyll

1 Vision

I have been learning nix for a while. Overall, I am really happy with Nix ecosystem. With Nix, home-manager, cachix, you have setup a reliable and efficient development environment and CI. For demonstration, I want to show you how I use nix to publish my blog:

If you are new to Nix, I hope you can find something useful in this blog.

1.1 Why Nix ecosystem

Hopefully, at this point you are already sold on nix. If not, maybe checkout build with nix or nix.dev. For me, the biggest selling point of is: you can use a declarative language to set up a reproducible environment for your local development and CI.

1.2 Why Hakyll

This is probably a tough sell if you don’t care about Haskell. Nowadays, you use tools like Hugo, WordPress, or Jekyll to effortless setup a static site. I love Haskell, and Hakyll gives me an opportunity to use Haskell.

1.3 Why Firebase

There are lots of decent alternative to Firebase for hosting a static site. Just to list few

2 Required Tools

3 Consider using nix project template

Both templates are very opinionated. getting-started-nix-template is a more generic nix template. hakyll-nix-template is Hakyll specified, it has sitemap, tag support.

.
├── content
|    ├── about.org
|    ├── contact.org
|    ├── css
|    ├── images
|    ├── index.html
|    ├── posts
|    └── templates
├── default.nix
├── nix
|   ├── default.nix
|   ├── sources.json
|   └── sources.nix
├── shell.nix
└── src

4 scaffold hakyll project using hakyll-init

Hakyll comes with a command line too haskyll-init to scaffold a Hakyll project. Since we only need haskell-init once, normal nix workflow would be

nix-shell -p pkgs.haskellPackages.hakyll

but depend on which nix channel is your default channel, you might get an error about pkgs.haskellPackages.hakyll is broken. We could use -I flag to pin our one-time shell package to stable version. The pattern is -I nixpkgs=https://github.com/NixOS/nixpkgs-channels/archive/{rev}.tar.gz~

~nix-shell -p pkgs.haskellPackages.hakyll -I nixpkgs=https://github.com/NixOS/nixpkgs-channels/archive/14006b724f3d1f25ecf38238ee723d38b0c2f4ce.tar.gz~
hakyll-init . #scaffold at current directory
hakyll-init content  #scaffold at ./content directory

The project is just a plain cabal project. We can edit project or executable name. We can use normal Nix Haskell development workflow.

5 Nix Haskell Workflow

Currently, there are two major Nix Haskell workflows: iohk-haskell.nix and your traditional nixpgs. People usually recommend iohk one for complex project, I haven’t used it at all.

nix-shell -p cabal2nix # unless you already installed cabal2nix globally
cabal2nix . > blog.nix
{ project ? import ./nix {}
}:
let
  haskellPackages = project.pkgs.haskellPackages;
  packages = haskellPackages.callCabal2nix "blog" ./blog.cabal {};
in
haskellPackages.shellFor {
  withHoogle = true;
  packages = p: [ packages ];
  buildInputs = builtins.attrValues project.devTools;
  shellHook = ''
    ${project.ci.pre-commit-check.shellHook}
  '';
}

https://discourse.nixos.org/t/nix-haskell-development-2020/6170 hoogle server --local -p 3000 -n

5.1 How to find certain (Haskell) package’s version

nix repl
nix-repl> sources = import ./nix/sources.nix
nix-repl> pkgs = import sources.nixpkgs {}
nix-repl> pkgs.haskellPackages.hakyll.version
"4.13.0.1"
nix-repl> :q

6 How to customize Hakyll

This is probably beyond the scope of this blog, Robert Pearce has an on-going serial on the topic. https://robertwpearce.com/hakyll-pt-1-setup-and-initial-customization.html

Here is a list of Hakyll projects I often check

Hakyll website has a more comphersive list

7 GitHub Action

7.1 Build Step

Most of the YAML configuration is copied from getting-started-nix-template. My default.nix only build the Hakyll program, it doesn’t generate the site. So I added result/bin/site build in run command. (site is the name of my Hakyll executable). We need pass generated site directory as artifacts between build steps

- name: Archive Production Artifact
  uses: actions/upload-artifact@master
   with:
       name: dist
       path: dist

dist is the directory name for the generated site, by default Hakyll uses _site.

7.2 Publish to Firebase

I use w9jds firebase action to publish the generated static site directory to Firebase. There are publish actions for netlify and Github Page. Of course, we have to store our Firebase token as encrypted secret and pass them as environment variables into the build step.

7.3 Enable cachix cache (Optional)

Current version of GitHub Action YAML

name: CI

on:
  push:
    branches:
      - master

jobs:
  build:
    name: Build
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2.3.4
    - uses: cachix/install-nix-action@v12
    - uses: cachix/cachix-action@v8
      with:
        name: yuanw-blog
        signingKey: ${{ secrets.CACHIX_SIGNING_KEY }}
    - name: Nix build
      run: |
        nix-build
        result/bin/site build
    - name: Archive Production Artifact
      uses: actions/upload-artifact@master
      with:
          name: dist
          path: dist

  deploy:
    name: Deploy
    needs: build
    runs-on: ubuntu-latest
    steps:
      - name: Checkout Repo
        uses: actions/checkout@v2.3.2
      - name: Download Artifact
        uses: actions/download-artifact@master
        with:
          name: dist
          path: dist
      - name: Deploy to Firebase
        uses: w9jds/firebase-action@v1.5.0
        with:
          args: deploy --message '${{github.event.head_commit.message}}' --only hosting
        env:
          FIREBASE_TOKEN: ${{ secrets.FIREBASE_TOKEN }}
          PROJECT_ID: ${{secrets.FIREBASE_PROJECT_ID}}

8 Result

Right now, the whole CI steps averagely takes 4 min to run. I am pretty happy with the setup.

9 References

9.1 About Nix in general

9.2 Nix Haskell development

9.3 Hakyll

9.4 Github Action

Powered by Hakyll and tailwindcss