Table of Contents
Introduction
This tutorial is will help you set up a Hakyll blog similar to mine. The reason I’m writing this is to have a simple, linear, step-by-step tutorial with all the things I find essential on a blog, namely a RSS feed and tags.
One of the advantages of using Hakyll to statically generate websites is its native integration with Pandoc, so we can create files in any format supported by Pandoc and they will be automatically converted to HTML. In my case, for an example, I write documents using org-mode inside Emacs, then I just let Hakyll do the rest of the work converting files and adding tags. It’s so simple and enjoyable to do.
Enough talk, let’s begin.
Installation
You can install Hakyll using two different methods, the one I chose was using cabal. The other one is by using stack. Both methods are similar, so there isn’t much difference between each other. You can install cabal using ghcup, or using your Linux distro standard repositories. After installing cabal on your system, you can install Hakyll by typing:
$ cabal new-install hakyll
Building the website
You can create the default website by typing:
$ hakyll-init site-name
where site-name
is the name you will give to the site’s directory. Make sure that ~/.ghcup/env
is in your $PATH
. Inside the site-name
directory, you will find the default website files, execute cabal new-run site
to set up the default website. You can check it out buy running cabal new-run site watch
, then it will be available on your local host. Essentially, the generated website is located in the _site
subdirectory, all the other files, except the site.hs
file, are files to be converted to HTML or Templates. After making changes to some file, or adding new files to the default website, you can use cabal new-run site build
to incrementally build your website. Although, if you make changes to site.hs
, you need to run cabal new-run site rebuild
instead. The site.hs
file is where all the magic happens, it seems frightening at first, by it’s actually simple to work with.
You can now create files in any format supported by Pandoc and, after running cabal new-run site build
, Hakyll will create the corresponding HTML files inside the _site
subdirectory. You might have noticed that all files that already exist start with a block like this one:
---
title: Something
author: Some Name
---
This is the Metadata of the file, and it will be useful to automatically generate Names, Dates and Tags. For example, the Metadata of this post that you are reading right now is:
---
title: Setting up a static website with Hakyll
description: Building a Hakyll website with tags and a RSS feed
tags: Haskell, Computers
---
Configuration
We can now start to configure the website. Configuration is done by editing the site.hs
file. Inside the file you will find some imported modules, Hakyll
and Data.Monoid (mapped)
, and you see the hakyll
function being called.
main :: IO ()
= hakyll $ do main
After that you will see some Hakyll Rules
like these:
"images/*" $ do
match
route idRoute compile copyFileCompiler
Here, match
will look for all the files inside the images/
directory and use the route
and compile
functions with the idRoute
and copyFileCompiler
arguments respectively.
The route
function is used to determine the output file inside _site
. For example, if you have a markdown file named article.md
inside the posts/
directory, then you will probably want Hakyll to place the converted HTML file _site/posts/article.html
directory. That can be accomplished by using the setExtension "html"
route. But if you don’t want to convert the file? For example, you have an image inside your images/
directory and you just want to have a copy of that image inside the _site/image/
, how do you do it? You just need to use the identity route idRoute
and it’s done.
The compile function determines the way you want your files to be compiled. The route function just determines the place and name for the output, but to convert the file you want to compile it with something. There are different ways to do it, but if you want to Pandoc to convert your files to HTML, you should use the pandocCompiler
argument. If you want to give Pandoc more options, then you should use pandocCompilerWith
instead. But if you want to do nothing to the file, then just use copyFileCompiler
.
Notice that there are other functions and arguments inside site.hs
that I haven’t mentioned, you can learn more by reading the documentation for them. I just wanted to explain the ones that are most commonly used.
Another Rule worth mentioning is create
, while match
looks for a file that already exists, create
will generate a file for you, you can find an example inside the site.hs
file:
"archive.html"] $ do
create [
route idRoute$ do compile
RSS feed
Setting up a RSS feed using Hakyll is easy, since Hakyll has a built-in support for it. First you will need to define a FeedConfiguration
as the following example:
myFeedConfiguration :: FeedConfiguration
= FeedConfiguration
myFeedConfiguration = "My Blog feed"
{ feedTitle = "This is the RSS feed for my blog"
, feedDescription = "John Doe"
, feedAuthorName = "john@mail.com"
, feedAuthorEmail = "https://mywebsite.com"
, feedRoot }
You don’t need to use all the fields above. Now you probably want to the feed to contain the content inside your posts, to do that you will need to use Snapshots to save the content inside your posts during compilation and to load then to your feed later. To do that you will need to add
>>= saveSnapshot "content"
To the match "posts/*"
section. It will look something like this:
"posts/*" $ do
match $ setExtension "html"
route $ pandocCompiler
compile >>= loadAndApplyTemplate "templates/post.html" postCtx
>>= saveSnapshot "content"
>>= loadAndApplyTemplate "templates/default.html" postCtx
>>= relativizeUrls
Now you just need to load the content saved by the Snapshot and to create the RSS feed, to do that you will need to add the following block inside site.hs:
"rss.xml"] $ do
create [
route idRoute$ do
compile let feedCtx = postCtx `mappend` bodyField "description"
<- fmap (take 15) . recentFirst =<<
posts "posts/*" "content"
loadAllSnapshots renderRss myFeedConfiguration feedCtx posts
Adding Tags
As mentioned above, this file has the following metadata:
---
title: Setting up a static website with Hakyll
description: Building a Hakyll website with tags and a RSS feed
tags: Haskell, Computers
---
You can see that I added the Haskell
and Computer
tags to it. To create tags we need first to fetch them inside our posts. To do that we need to add the following line to site.hs
:
<- buildTags "posts/*" fromCapture "tags/*.html" tags
The line above will create the tags under "tags/*.html"
format by fetching them inside the "posts/*"
directory. Now all we need to do is to add those tags to the their respective post and to list all the tags in the archive page. To add the tags to their posts we need to change the post Context (I know I haven’t mentioned what a Context is, but we don’t need to understand them to add tags to our posts). To do that, need to remove the current postCtx
:
postCtx :: Context String
=
postCtx "date" "%B %e, %Y" `mappend`
dateField defaultContext
and add the following block in its place site.hs
:
postCtxWithTags :: Tags -> Context String
= mconcat
postCtxWithTags tags "date" "%Y-%m-%d"
[ dateField "tags" tags
, tagsField , defaultContext
Then change all the occurrences of postsCtx
inside site.hs
to postsCtxWithTags
. Now to add the list of tags to the archive.html
file, we need to add the following line to the create ["archive.html"]
rule:
"taglist" (\_ -> renderTagList tags) `mappend` field
In the end, it should look to something like this:
"archive.html"] $ do
create [
route idRoute$ do
compile <- recentFirst =<< loadAll "posts/*"
posts let archiveCtx =
"posts" (postCtxWithTags tags) (return posts) `mappend`
listField "taglist" (\_ -> renderTagList tags) `mappend`
field "title" "Archives" `mappend`
constField defaultContext
Now add this rule anywhere after that:
$ \tag pattern -> do
tagsRules tags let title = "Posts tagged \"" ++ tag ++ "\""
route idRoute$ do
compile <- recentFirst =<< loadAll pattern
posts let ctx = constField "title" title
`mappend` listField "posts" (postCtxWithTags tags) (return posts)
`mappend` defaultContext
""
makeItem >>= loadAndApplyTemplate "templates/tag.html" ctx
>>= loadAndApplyTemplate "templates/default.html" ctx
>>= relativizeUrls
Great, now we just need to make our tags visible. To do that we need to edit the Templates inside the templates/
directory. Your templates/posts.html
should look like this"
>
<article>
<section class="header"
Posted on $date$
$if(author)$
by $author$
$endif$>
</section>
<section class="header"
$if(tags)$: $tags$
Tags
$endif$>
</section>
<section
$body$>
</section> </article
We added the tags to their respective posts. Now we’ll need to create a tag template to list the posts inside a tag, you can just copy the code inside template/post-list.html
to a new file: template/tag.html
. This what it should look like:
.html")$ $partial("templates/post-list
Finally, change your templates/archive.html
file to something like this:
>
<section>Tags:</b> $taglist$
<b>
</section>Here is a list of all my previous posts:
<br.html")$ $partial("templates/post-list
Now you just need to rebuild site.hs
(~cabal new-run site rebuild) and it’s done!
Resources
To learn more about Hakyll, you can read these tutorials, and the Hackage page.