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
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/includeDownload 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.