Basics of the basics
What
To quote the creator :
Hakyll is a Haskell library for generating static sites, and uses a xmonad-DSL for configuration. Integration with pandoc gives us Markdown and TeX support
My blog (as the footer indicates) is (proudly !) generated using Hakyll.
Why
Hakyll has tons of functionalities, and those it doesn’t have, I can program. Not only do I get to focus on content by writing everything in Markdown (and TeX for the trees in the Orchard), I can automate most features in Haskell.
Also, I think writing HTML is a pain, I like static websites, and pages like posts list, RSS feed, sitemap, tags list are generated automatically.
How
Write compilations rules, write your CSS and your templates, use whatever format you want for you content as long as Pandoc supports it, and voilà, you have a site.
And in practice ?
The idea
The same as for XMonad ! We’ll write our own program using Hakyll as a library (but to be clear, Hakyll does most of the job by giving us nice abstractions and a DSL).
But, simpler than XMonad, when creating our site, using hakyll-init [-f] <directory>
(-f
is to force the creation of files), we get a nice example project, on top of which we can build on.
Get the example project, and let us analyze the code…
The simple part
main :: IO ()
= hakyll $ do
main "images/*" $ do
match
route idRoute
compile copyFileCompiler
"css/*" $ do
match
route idRoute compile compressCssCompiler
Just like XMonad, we define a main
function, that uses hakyll
(resp. xmonad
for XMonad) with some sort of configuration. However, more complex than Hakyll, we are not merely configuring a program. We are creating Rules
that define the compilation process of our site !
In the Rules
monad, each block define a compilation rule. Here, we have the two simplest: we just need to copy those files from our source directory (by default the directory where we execute our program) to the _site
directory (containing the compiled site).
To that effect, we match the correct files, we specify that the route will be the exact same, and that the compiling is just a matter of copying files for the images, and compressing the file for CSS.
The less simple part
"about.rst", "contact.markdown"]) $ do
match (fromList [$ setExtension "html"
route $ pandocCompiler
compile >>= loadAndApplyTemplate "templates/default.html" defaultContext
>>= relativizeUrls
"posts/*" $ do
match $ setExtension "html"
route $ pandocCompiler
compile >>= loadAndApplyTemplate "templates/post.html" postCtx
>>= loadAndApplyTemplate "templates/default.html" postCtx
>>= relativizeUrls
postCtx :: Context String
=
postCtx "date" "%B %e, %Y" `mappend`
dateField defaultContext
Now we have to compile some files, here a reStructuredText and a Markdown file. The route is just a matter of changing the extension. However, we need to compile the files, and get a full webpage. That’s why we use…
Context
A context is a number of informations to be recorded about our page:
- Body
- Metadata fields
- URL
- Path
- Title
Everything after (and including) 3. can be altered in metadata fields. If you ever used Markdown, especially R Markdown, you know what those are: the little optional YAML header at the start of a file. Here’s the one of this post
---
title: The basics of Hakyll
date: 2024-08-05
tags: hakyll, haskell
---
We can also append (there’s a Monoid
instance for Context
:D) fields to the default context, for example dateField
. And the Context
is used in…
Templates
"templates/*" $ compile templateBodyCompiler match
Templates are a really powerful tool: they do exactly what you think they do, especially the “being cool” part !
There’s no need to specify a route, since we don’t need them as they are in the final site. However, we still “compile” them, to use them elsewhere…
We use a very small set of identifiers:
$fieldName$
: Simply outputs the content offieldName
$for(items)$
: Iterates over items and in the context of the loop, each$fieldName$
refers to the current item. Ends with$endfor$
$if(fieldName)$
: If the field namedfieldName
exist, do whatever is in the context of theif
block. Ends with$endif$
$partial(filePath)$
: Includes the content of the file located atfilePath
Then here’s how it’s processed:
file.md template.html /---------\ /---------------------\ ⎸ # Title ⎹ ⎸ <html> ⎹ ⎸ ⎹ ⎸ <head>...</head> ⎹ ⎸ Text ⎹ ⎸ <body> ⎹ \---------/ ⎸ $body$ ⎹ | ⎸ </body> ⎹ | Pandoc ⎸ </html> ⎹ | \---------------------/ | | | | loadAndApplyTemplate v | /----------------\ | file.html ⎸ <h1>Title</h1> ⎹ | /---------------------\ ⎸ ⎹----+---->⎸ <html> ⎹ ⎸ <p>Text</p> ⎹ ⎸ <head>...</head> ⎹ \----------------/ ⎸ <body> ⎹ ⎸ <h1>Title</h1> ⎹ ⎸ ⎹ ⎸ <p>Text</p> ⎹ ⎸ </body> ⎹ ⎸ </html> ⎹ \---------------------/
The difference being, in the example project, that we use two templates ! The first template shows the date, the author (if the field exists) and the body of the post, and the second formats everything into a complete page. The result of the first apply becomes the new $body$
for the second template.
We finish with relativizeUrls
, transforming every URL in a relative form.
The not very simple part
Let’s finish with this block:
"archive.html"] $ do
create [
route idRoute$ do
compile <- recentFirst =<< loadAll "posts/*"
posts let archiveCtx =
"posts" postCtx (return posts) `mappend`
listField "title" "Archives" `mappend`
constField
defaultContext
""
makeItem >>= loadAndApplyTemplate "templates/archive.html" archiveCtx
>>= loadAndApplyTemplate "templates/default.html" archiveCtx
>>= relativizeUrls
The next block is more or less the same, so there’s no need to explain it.
In that case, we do not match anything, we are creating a file from scratch. We already know about the root part, the interesting part is the compilation:
- We first load all the posts, and sort them by date
- We create a new context, containing the title and the list of posts (to be used in a
$for(posts)$
loop) - We create a new item, with an empty body, and it goes through both templates
This process can be generalized for a lot of files, but you’ll have to wait for the next post :)