Single-page static website generation with Hakyll
by Clement Delafargue on January 24, 2013
Static sites generators
Static sites generators are quite the hype these days. They bring lots of advantages over CMSs: security, speed, easier versioning (contents are in the SCM, not in the DB, use a real text editor, not a crappy WYSIWYG)… With web servers like Nginx it only gets better.
I’ve been fond of static site generators for some time now. I’ve started
with rest2web which used
docutils to generate webpages. We used it to create the website for our Junior
Entreprise. Nelle Varoquaux managed to get a nice
site to build but I remember it to be non trivial. It involved putting some
custom python code im my /lib
and bugging rest2web’s creator with a few
emails. I liked the concept but it was a real pain to use and even more to
customize. Nelle did a fine job with it but our successors at the Junior
Entreprise dropped it altogether and replaced it by a hand-rolled PHP website.
Even though the tool support was less than ideal, I got hooked to the idea of static websites.
Some time later, Jekyll became widely used thanks to GitHub pages and I’ve rolled a few websites with it: my company’s website, http://eklaweb.com (I’ll come to that later), my company’s blog http://blog.eklaweb.com, and a few other projects. I’ve advocated its use for http://ordify.de.
Working with Jekyll gave quick results and easy-to-build websites but this way fell short quite quickly when trying to do less standard stuff.
Building http://eklaweb.com with Jekyll was not possible out-of-the-box, for Jekyll is designed to create one HTML file per input file. Being a single-page website, this led to all kinds of dirty hacks. Generally speaking, very few projects with Jekyll were achieved without hacking its blogpost mechanism in Lovecraftian abominations.
I’ve also tried to use Scalate, a scala templating library which was said to also support static website generation. Due to how it was packaged, I’ve never been able to make it work, but it gave me an interesting vision on static website generation. The key was to provide a library which lets you build your own generator. Unfortunately this is not how most static sites generators work. This way of thinking can more or less be linked to
More libraries, less frameworks
but that’s another story.
Enter Hakyll
Hakyll is a haskell library which lets you create your own site generator. For convenience, there are a few examples provided so you can have a blog running without coding everything; that’s what I did with this blog.
Hakyll uses pandoc, so it supports a huge number of formats.
After porting my blog to Hakyll 4, I felt comfortable enough to try to dive deeper and recreate my company’s website with more modularity, with the different parts cleanly separated into different templates and markdown files.
Here’s my attempt: http://github.com/divarvel/hakyll-single-page-test. Every section of the page has its own template and the blocks (whose numbers vary in each section) are in different files.
How it works
Hakyll is based on a few important types:
Compiler a
, produces a unit and tracks its dependencies. The types are very generic, I won’t be more specific.Compiler
is an instance ofMonad
so you can easily assemble compilers to create more powerful ones.Item a
pairs some content to anIdentifier
Context a
provides an immutable context allowing to inject data in a template. Typically, it will contain the file body and its metadata (title, date, tags). You can join contexts since they’re instances ofMonoid
For a regular multi-page website, you would write a compiler to turn every input file into an output file.
For a single page website, you have to proceed a bit differently. You can compile every input file, but you have to pipe different compilers to assemble the parts into one page.
In my example, each section contains a title, a description text and a variable number of blocks, each with a title, an image and some metadata. There is a template for the blocks, and a template for the whole section.
The content files follow this structure:
blocks
├── formations
│ ├── audit.md
│ └── formations.md
├── formations.md
├── index.md
├── people
│ ├── clement.md
│ ├── godefroy.md
│ └── lefu.md
├── people.md
├── refs
│ ├── alixio.md
│ ├── ecn.md
│ ├── lapompadour.md
│ └── rezoto.md
├── refs.md
├── technos
│ ├── html5.md
│ ├── nosql.md
│ ├── phonegap.md
│ └── scala.md
└── technos.md
The templates follow this one:
templates
├── blocks
│ ├── formations.html
│ ├── people.html
│ ├── refs.html
│ └── technos.html
├── default.html
├── formations.html
├── people.html
├── refs.html
└── technos.html
So, how do we assemble it back ?
First, compile all the blocks
"blocks/**.md" $ do
match compile pandocCompiler
I’ve not specified a route, so they won’t directly appear in the generated site. They’re just compiled so another compiler can use them.
Compiling a section (for instance, the refs section) is easy. To keep the code simple, I’ll give an example for only one section. The final code is not duplicated, I just pass an options record to keep the whole thing DRY.
First, we assemble the blocks
refsCompiler :: Compiler String
= do
refsCompiler <- loadAll "blocks/refs"
blocks <- loadBody "templates/blocks/refs.html"
blockTemplate <- applyTemplateList blockTemplate defaultContext blocks blockList
applyTemplateList
applies a template to every element of the list and joins
the result. defaultContext
is provided by Hakyll and contains the block’s
metadata and body.
Then, we inject the elements in the section and return its contents (we drop
its Identifier
since it won’t be a page on its own).
<- loadBody "templates/refs.html"
sectionTemplate <- load "blocks/refs.md"
sectionData <- (applyTemplate
section
sectionTemplate (sectionContext blockList) sectionData)return $ itemBody section
With sectionContext
I pass the blocks list to the section template, while
keeping the sections metadata (Context
is an instance of Monoid
).
sectionContext :: String -> Context String
=
sectionContext list "blocks" `mappend`
constField defaultContext
The last thing to do is to combine all those compilers into one page:
"index.html"] $ do
create [$ do
compile <- load "blocks/index.md"
pageData <- refsCompiler
r <- (loadAndApplyTemplate
page "templates/default.html" (indexContext r) pageData)
$ itemBody page makeItem
As with sectionContext
, indexContext
just adds the section contents to
defaultContext
.
indexContext :: String -> Context String
= constField "refs" r `mappend` defaultContext indexContext r
Here are the important parts. You also have to compile CSS, static files and this sort of stuff, but it’s quite boring.
Once you have everything, juste generate your generator (so meta) and build your site:
$ ghc --make hakyll.hs
$ ./hakyll build
You can also launch a preview server with automatic rebuild:
$ ./hakyll preview
Or check your links to find broken links:
$ ./hakyll check
Hakyll provides simple yet powerful building blocks, letting you build anything you want without resorting to ugly hacks, but keep in mind that you’re not forced to write all the code by yourself. There are examples for common use cases: a static site, a blog with tags, RSS / Atom support, multi-lang setups… The point of this post was that it’s possible to build something really custom with it.
Hakyll is fairly well documented and there are a few hakyll-based websites whose source is available:
Enjoy, and don’t forget to thank Jasper on #hakyll
(freenode).