Index

Adding Mustache template support to Hakyll

Posted in Category • April 18, 2020

Because of the COVID-19 pandemic, I have been staying in for the last few weeks. Suddenly I had some free time so naturally I tried to reboot my blog That seems to be the case for many others as well, I have seen a lot of people restarting their blog and Youtube podcasts, for example Chris Coiyer and Remy Sharp, whom I have been following for a very long time.

. Since Haskell is my go-to programming language now, this time I decided to use Hakyll to build my blog with. In the past I have tried quite a few static site builders like Jekyll and Hugo, however they are not flexible enough to my use, hence I always seem hit their limitation as I want a lot of flexibility and customization, which with Hakyll I can achieve. Sure I will need work through Hakyll’s own rather complex source first but I would not mind it at all, especially compared to the similarly complex, if not more, of Jekyll.

I have been quite satisfied with the power Mainly because of Pandoc. It truly is the universal converter which support the most extensive number of formats as well as a huge number of plugins.

and flexibility of Hakyll Another valued feature of Hakyll is the flexibility with custom routing, instead of requiring the files to be laid out exactly like the resulting site. This feature seems to be inspired by Nanoc.

, while I still think the context mechanism and dependency management is overly complicated, the area where I felt most dissatisfying is Hakyll’s template system. It gets the job done but it feels rather complicated and not quite familiar. That is why last weekend I decided to sit down and give it ago trying to add Mustache template support to Hakyll.

The plan

In Haskyll, you build the final files using compilers, typically, a skeleton project generated by Haskyll will contain code snippet like this:

main :: IO ()
main = hakyll $ do

  match "templates/*" $ compile templateCompiler

  match "posts/*" $ do
    route   $ indexBasedRoute
    compile $ pandocCompiler
      >>= saveSnapshot "content"
      >>= loadAndApplyTemplate "templates/post.html"    postContext
      >>= loadAndApplyTemplate "templates/default.html" postContext

How it works is that in the first rule, templateCompiler will read the template files, parse it and build an internal Item Template, ready to be used later In Hakyll, everything is an Item. A file you read from disk to process will be an Item String, through compilers and transformers you can turn that initial Item String into Item a, where a can be anything to your liking. As long as there is a Writable instance for a, you will then be able to write it to form your final resource.

. The second rule does something similar, it first loads the posts as Item String, then through pandocCompiler turning into Item Text, with the text here is the rendered Markdown. What is interesting here is loadAndApplyTemplate, what it does is to build a “context” using the post’s Item, then take the pre-compiled template file "templates/post.html" and substitude the placeholders in the template with the value coming from the context. This seems very straight forward and is a nice API, so my plan is to provide a compiler and my own version of loadAndApplyTemplate In reality, there are a couple of other functions that Hakyll provide, which I also need to implement, for example applyAsTemplate. Nevertheless, they are similar enough and are built from the same core, so it is trivial to write one given the other.

that works with compiled Mustache templates.

The implementation

Turns out that can be done in less than 100 lines of code. You can take a look at the final module on Github. A couple of notes:

  • I used stache, however I could not support functionField context field with it, so that’s a lost in term of feature parity. It’s not a problem for me because I want my template to be logic-less anyway, any field value processing and transform can be done by adding a custom field with the transformed value. And if I really wanted to have functionField, I could easily switch to mustache, which does support Mustache lambda extension.

  • Because Hakyll use Binary to serialize items to disk, I am required to implement a Binary instance for Template. GHC was able to derive most of the instances for me, except for Pos from Megaparsec. But since Pos is just a newtype wrapper over Int, I was able to wrap/unwrap it to provide a Binary instance:

    instance Binary Pos where
      put = put . unPos
      get = mkPos <$> get
  • Context is a quite complicated type. Eventhough was able to look at the source of Hakyll and figure out how to lookup value from it I still am not sure what of its all the arguments does.

  • Speaking of Context, I could not find a way to list all the keys from a particular context. What I end up doing is to traverse the compiled template’s Node, looking for variable and section fragment and look them up from the Context. It turns out to be perfectly fine, it works as expected and when the template references some non-existing keys in the context, the original (and sensible) error message will show up.

Conclusion

It was a fun journey, I learned a ton. Looking forward to spending more time hacking Hakyll. I have a couple of features I want to implement for this blog, namely multilingual and search When researching static site builders, I came across Zola. It is a very nice static site generate, with tons of features some I did even imagine possible with static site generators. I choose to go with Hakyll because of Pandoc, and the pandoc-sidenote extension that turns footnotes into side-notes, displayed in a nice Tufte layout.

, so stay tuned!