Hakyll From Scratch Pt 1

Posted on May 10, 2026 by Jarvis Cochrane · Tagged ,

When I decided to rebuild this site, I chose to make it a static site using Hakyll. The previous version was built on the Wagtail CMS engine. I really like Wagtail, but it’s far too complex for a simple personal site like this. I was spending more time – so much time – customising the CMS and automating the infrastructure than on the actual content.

Why Hakyll?

There are literally hundreds of static site generators to choose from. I chose Hakyll because I was just starting to learn Haskell and I thought it would be good to have a small, real project to work on. It would give me some motivation to keep learning the language; help me get to grips with the broader ecosystem; and give me a little of the real project experience that you don’t get from working through tutorials.

Unlike, say, Pelican, Hakyll isn’t a program that you configure and run to assemble your content into a site. It’s a toolbox for writing that program. There isn’t a separate configuration file – the generator program is the configuration.

There are tradeoffs. On the one hand, Pelican makes it easy to get started quickly. On the other hand, Hakyll makes it easier to build your site exactly the way you want it. I prefer driving manual (‘stick shift’) to automatic cars, so you can guess which approach I find more appealing.

Hakyll Hello World

To get this new site set up I worked through some Hakyll tutorials, puzzled over some examples, and had quite a bit of help from Claude. I got it running, and I’m happy with it, but I can’t honestly say I understand how everything in site.hs fits together.

Much as the classic “Hello World” is a way to start unwrapping all the myriad, layered complexities of a programming language, I thought it would be an interesting exercise to create the most minimal Hakyll site possible.

Once I understood that, I could add features gradually, extending my understanding of both Haskell and Hakyll at each step.

Minimal Site Generator

(Download the example)

This is the most minimal and unmagical Hakyll site builder I’ve been able to write. It’s functionally equivalent to the Unix shell command mkdir _site; cp index.html _site. I’ve excised most of the syntactic sugar for the sake of my own understanding.

-- Minimal Hakyll site

import Hakyll

main :: IO ()
main = hakyll $
    match (fromGlob "index.html") (
        route idRoute >>
        compile copyFileCompiler)

So what’s going on here?

hakyll processes its parameter – a Rules type – to build an internal description of the processing required. Then, it uses that internal description to perform the required processing and build the site. I’ve tried reading the source code to try and understand what hakyll is doing under the hood, but I just don’t read Haskell well enough yet. One day…

match links a Pattern of files to be processed with the processing to be performed on them, and returns this as a Rules instance.

The Pattern in this case is obviously a ‘glob’ style search for any filenames matching the pattern index.html.

The processing Rules are to first set the destination filename to the name of the source (route idRoute) and then perform a file copy as the ‘compilation’ step (compile copyFileCompiler).

The >> operator sequences expressions so route idRoute is executed first, then compile copyFileCompiler. It’s more idiomatic to use do notation, but I wanted to check my understanding by removing that. With do notation it’s:

match (fromGlob "index.html") $ do
    route idRoute
    compile copyFileCompiler

Which, honestly, is much nicer to read!

Cabal Build Configuration

GHC is complex enough that using Cabal to manage the build is pretty much a requirement.

Here’s a cabal file with the minimum required configuration to build and run the minimal Hakyll site generator above.

I needed the if os(osx) section to resolve the conflict between the system libiconv version and the version provided by MacPorts. YMMV.

cabal-version:      3.14

name:               site
version:            0.1.0.0
build-type:         Simple

executable site
  main-is:          site.hs
  build-depends:    base == 4.*
                  , hakyll == 4.16.*
  ghc-options:      -threaded
  default-language: Haskell2010

-- This is required on MacOS if another copy of libiconv has been installed
-- with macports or similar. This specifies the correct (System default)
-- iconv library.
  if os(osx)
    extra-lib-dirs: /usr/lib
    include-dirs: /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include

Download the example and stay tuned for part 2!

Revised May 11, 2026. Minor edits for style. Added download of example files. Fixed incorrect post slug.