# Jekyll Style URLs with Hakyll

Posted on January 31, 2016

Re­cently, I switched from Jekyll to Hakyll for the gen­er­a­tion of this Blog. In this ar­ticle I want to talk about Hakyll’s routing mech­a­nism and how to get it to gen­erate the same URLs as Jekyll so that all the old links to your posts keep work­ing.

If you want to follow along you can find the source code here. Step through the com­mits in the repos­i­tory to see the steps pre­sented in this ar­ticle ap­plied one after the other.

First, a few brief words about what Jekyll and Hakyll are. Both are static web­site gen­er­a­tors. That is, they take a set of input files and set­tings, and gen­erate a com­plete web­site in a static folder struc­ture. This means that you don’t need any clever server-side soft­ware to serve your site. You can just up­load it to any hosting ser­vice that al­lows you to serve Html files and that’s all you need.

Be­fore switch­ing, I used Jekyll Boot­strap which is a Blog scaf­fold based on Jekyll, Twitter Boot­strap, and a few other com­po­nents that tries to make web­site gen­er­a­tion as quick and easy as pos­si­ble. When I refer to Jekyll in this ar­ti­cle, then I re­ally mean Jekyll Boot­strap.

For that reason I am not going to talk about the pros and cons of Hakyll vs. Jekyll. It would be a mis­leading com­par­i­son. I per­son­ally switched to Hakyll be­cause it seemed like a good op­por­tu­nity to learn a bit more about Haskell, and be­cause it seems to be very flex­ible when it comes to changing the de­tails of how your site should be gen­er­ated. The main mo­ti­va­tion, how­ever, was just cu­rios­ity.

## Jekyll’s Routing

Now, let’s talk about rout­ing. Jekyll ex­pects mark­down files (or some other markup) in a spe­cial folder called _posts. Their path should be of the fol­lowing form.

_posts/YYYY-MM-DD-some-title.ext

That is, it should con­tain the pub­lishing date, and then a ti­tle, all sep­a­rated by dashes. The title may con­tain dashes it­self, those will be left un­touched. Jekyll will then take that input path and gen­erate the fol­lowing output path.

_site/category/YYYY/MM/DD/some-title/index.html

The whole web­site will be placed into the _site di­rec­tory. That is the di­rec­tory that you want to serve to the web in the end. Next, there is a cat­e­gory di­rec­tory. Jekyll will read the cat­e­gory from the YAML front­matter of the mark­down files. The date is split into the com­po­nents year, month, day and each be­comes its own di­rec­tory. The title is made into a di­rec­tory as well, and the gen­er­ated Html file is stored as index.html un­der­neath.

When Jekyll gen­er­ates URLs to these files they will have the fol­lowing form.

/category/YYYY/MM/DD/some-title

That is, the file­name index.html is chopped off. In gen­eral, most con­sider that good prac­tice as it al­lows one to change the tech­nology be­hind how a web­site is gen­er­ated without in­val­i­dating old URLs.

## Hakyll’s Routing

When starting off with Hakyll’s ex­ample site, gen­er­ated by hakyll-init, it ex­pects to find mark­down files in the folder posts. Their path should look about the same as for Jekyll.

posts/YYYY-MM-DD-some-title.ext

The gen­er­ated output will be placed under the fol­lowing path.

_site/posts/YYYY-MM-DD-some-title.html

As with Jekyll the whole web­site is placed under _site. The mark­down file will be trans­lated to Html and be given the cor­re­sponding file-ex­ten­sion. Other than that the path is not changed at all. Hakyll also doesn’t apply any trans­for­ma­tion on the URLs by which it links to your con­tent. I.e. the gen­er­ated links will look like this:

/posts/YYYY-MM-DD-some-title.html

If we want Hakyll to gen­erate the same URLs as Jekyll would we need to ad­dress the fol­lowing four points.

• Files should be stored as index.html under a cer­tain path, and URLs should not in­clude that file­name.
• The date in the file­name should be split into one di­rec­tory for each com­po­nent.
• The prefix posts/ should be dropped.
• And we need to gen­erate a cat­e­gory di­rec­tory in front of the rest of the path.

## Ex­ten­sion-less URLs

Let’s first look at the gen­er­ated output path. Hakyll calls this routing. The tu­to­rial ex­plains the ba­sics be­hind the routing process. The de­fault setup looks like this.

match "posts/*" $do route$ setExtension "html"


This code finds all files in the posts di­rec­tory and ex­changes their file-ex­ten­sion by html. What we want to do now, is to slip a /index in be­tween the ex­ten­sion and the rest of the path. Hakyll’s routing mech­a­nism is easily ex­ten­sible through the pro­vided func­tion composeRoutes, which does what the name sug­gests.

composeRoutes :: Routes -> Routes -> Routes


Ad­di­tion­ally, Hakyll of­fers a func­tion to fully cus­tomize how we gen­erate a route:

customRoute :: (Identifier -> FilePath) -> Routes


For our pur­poses an Identifier is just some­thing that has a path. We can ex­tract it with toFilePath. The fol­lowing func­tion will per­form the trans­for­ma­tion that we’re looking for.

appendIndex :: Routes


## Drop­ping the Prefix

Next let’s get rid of that posts/ prefix in the gen­er­ated URL. This is ac­tu­ally very easy. We can use the func­tion gsubRoute again. Just, this time we want to re­place the matching part with the empty string.

dropPostsPrefix :: Routes
dropPostsPrefix = gsubRoute "posts/" $const ""  Now, we’re al­most there. ## Group Posts by Cat­e­gory Fi­nally, we want to group our posts by cat­e­gory. There are two ways to do this. The simple way is to just move the mark­down files into sub­folders ac­cording to their cat­e­gory, and then change all the routing pat­terns in your site.hs file to match only posts within a cat­e­gory in their path. In other words, re­place every oc­cur­rence of the pat­tern posts/* by posts/*/* in your site.hs, and move all the mark­down files into the cor­re­sponding sub­di­rec­tory. An­other way, which is the way in which Jekyll does it, is by a meta­data field which is in­serted into the URL. For that we first need to add cat­e­gory fields to the YAML headers of all Blog posts. E.g. --- title: S.P.Q.R. category: category_a ---  Then we need a route trans­former which adds the cat­e­gory into the route. For­tu­nately, Hakyll pro­vides a func­tion for that pur­pose. metadataRoute :: (Metadata -> Routes) -> Routes  We can com­bine it with the func­tion customRoute that we used be­fore and prepend the cat­e­gory to the path. prependCategory :: Routes prependCategory = metadataRoute$ \md -> customRoute \$
(md M.! "category" </>) . toFilePath


Hakyll rep­re­sents meta­data as a map from Data.Map in the containers pack­age. In this func­tion we use the (!) op­er­ator from the same module to ex­tract the cat­e­gory field. Note, that this func­tion will fail with a run-time error if a post does not have a cat­e­gory field. If that is not the be­hav­iour you want, then you should use the lookup func­tion from Data.Map in­stead and handle the Nothing case in whichever way you see fit.

## Con­clu­sion

With that we taught Hakyll how to gen­erate URLs just like Jekyll does. Based on this ex­pe­ri­ence I will dare one com­par­ison be­tween Jekyll and Hakyll. The former seems more like a static web­site gen­er­ator that you can con­fig­ure. The lat­ter, how­ever, re­ally seems like a li­brary that you use to write your own static web­site gen­er­a­tor. Nat­u­rally, it takes a little bit more ef­fort to get any­thing done with Hakyll. On the other hand you have im­me­diate ac­cess to every as­pect of the web­site gen­er­a­tion.

Please leave a com­ment if you no­ticed any mis­take, have any sug­ges­tions, or any other form of feed­back. Thanks for read­ing!