Hakyll From Scratch Pt 1

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

When I started rebuilding this site over the New Year’s holiday, I decided to make it a static site and use Hakyll as the generator.

The previous version was built on the Wagtail CMS engine. I really like Wagtail, but I realised I was spending more time – so much time – customising the CMS and automating the infrastructure deployment than on writing and publishing actual content. Using Wagtail for a simple personal site is like asking Buffy to supervise a kindergarten Halloween dress-up.

But 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 but real project to work on. This would push me to keep learning Haskell and the tools and libraries. I’d also gain some of that invaluable real project experience that you never get from just working through tutorials.

Unlike some other static site generators – Pelican for example – Hakyll isn’t a program you configure and then run to assemble your content into a site. It’s better thought of as a toolbox for writing a program that does that. In effect, Hakyll extends Haskell to create a specialised vocabulary for describing how to assemble various assets into a complete static web site.

There are tradeoffs. Pelican makes it really easy to get started, and there’s a rich variety of themes and other extensions that you can more or less just drop into your Pelican installation to enhance it. 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 and puzzled over some Hakyll tutorials, borrowed code from some example sites, and had a lot of help from Claude. I got it running (obviously, since you’re reading this), and I’m pretty happy with it, but I can’t honestly say I properly understand what everything in site.hs is doing.

“Hello World” is the classic way to start unwrapping the myriad, layered complexities of a new programming language. In the same spirit, I thought it would be helpful to create the most minimal Hakyll site I could. Once I understood that, I could gradually introduce new features, extending my understanding of both Hakyll and Haskell 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 – it copies index.html into the _site directory unchanged. I’ve excised most of the syntactic sugar to help 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?

The scenic view is that hakyll reads a series of Rules that describe how to build a website, then performs the processing described by those rules, ensuring that artifacts are created in the right order and are only rebuilt if they’ve changed.

I’ve just described a two-line Makefile!

match (fromGlob "index.html") (...) specifies a rule that will be applied to any files which match index.html and how those files are to be processed.

The first processing step is route idRoute. This sets the path of the destination file – the ‘route’ – to that of the source file being processed, but relative to the output directory.

The second processing step is compile copyFileCompiler. In this case the ‘compiler’ merely copies the source file unchanged to the destination.

A couple of small notes on Haskell syntax – for my own benefit! f $ x y is equivalent to f (x y) – it saves having to wrap parameters in parentheses, which becomes really useful when composing a chain of functions.

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 written:

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

Which, honestly, is much nicer to read!

Cabal Build Configuration

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

This cabal file shows the minimum required configuration to build and run the Hakyll site generator above.

The command to build and run the generator is cabal run -- site build.

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 June 04, 2026. Added link to Part 2.

Revised May 14, 2026. Substantial rewrite.

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