---
author: "Robert Pearce"
authorTwitter: "@RobertWPearce"
desc: "Leverage hakyll's own internals to output custom webpage URI routes using any metdata field – in this case, our post title."
keywords: "hakyll, slug, filename, hakyll tutorial, hakyll blog, hakyll blog tutorial, hakyll static site, static site generator"
title: "Hakyll Pt. 5 – Generating Custom Post Filenames From a Title Slug"
updated: "2023-02-11T16:00:00Z"
---

_2023-02-03 update: I published the [slugger](https://hackage.haskell.org/package/slugger)
package on Hackage back on 2021-07-28, so that is available for use if you'd
rather use a package._

This is part 5 of a multipart series where we will look at getting a website /
blog set up with [hakyll](https://jaspervdj.be/hakyll) and customized a fair
bit.

* [Pt. 1 – Setup & Initial Customization](/hakyll-pt-1-setup-initial-customization.html)
* [Pt. 2 – Generating a Sitemap XML File](/hakyll-pt-2-generating-a-sitemap-xml-file.html)
* [Pt. 3 – Generating RSS and Atom XML Feeds](/hakyll-pt-3-generating-rss-and-atom-xml-feeds.html)
* [Pt. 4 – Copying Static Files For Your Build](/hakyll-pt-4-copying-static-files-for-your-build.html)
* Pt. 5 – Generating Custom Post Filenames From a Title Slug
* [Pt. 6 – Pure Builds With Nix](/hakyll-pt-6-pure-builds-with-nix.html)
* [The hakyll-nix-template Tutorial](/the-hakyll-nix-template-tutorial.html)

## Overview

Out of the box, hakyll takes filenames and dates and outputs nice routes for
your webpages, but what if you want your routes to be based off of a metadata
field like `title`? In this post we'll take a title like `"Hakyll Pt. 5 –
Generating Custom Post Filenames From a Title Slug"` and have hakyll output
routes like `"hakyll-pt-5-generating-custom-post-filenames-from-a-title-slug"`.

1. [Where Do We Start? Hakyll's `route`
   Function](#where-do-we-start-hakylls-route-function)
1. [Looking to `idRoute`, `setExtension` and Other `Routes` Functions for
   Clues](#looking-to-idroute-setextension-and-other-routes-functions-for-clues)
1. [Leveraging Hakyll's `metadataRoute` to Access Title
   Metadata](#leveraging-hakylls-metadataroute-to-access-title-metadata)
1. [Writing Our Own URI Slug Function](#writing-our-own-uri-slug-function)
1. [Retrieving and Slugifying our Titles](#retrieving-and-slugifying-our-titles)

## Where Do We Start? Hakyll's `route` Function

In [the hakyll tutorial on basic
routing](https://jaspervdj.be/hakyll/tutorials/03-rules-routes-compilers.html#basic-routes),
as well as other posts in this series, we have come across hakyll's
[`route`](https://github.com/jaspervdj/hakyll/blob/1abdeee743d65d96c6f469213ca6e7ea823340a7/lib/Hakyll/Core/Rules.hs#L175)
function used in conjunction with functions like
[`idRoute`](https://github.com/jaspervdj/hakyll/blob/1abdeee743d65d96c6f469213ca6e7ea823340a7/lib/Hakyll/Core/Routes.hs#L114-L115)
and
[`setExtension`](https://github.com/jaspervdj/hakyll/blob/1abdeee743d65d96c6f469213ca6e7ea823340a7/lib/Hakyll/Core/Routes.hs#L136-L138).
Given these functions live in the
[`Hakyll.Core.Routes`](https://github.com/jaspervdj/hakyll/blob/1abdeee743d65d96c6f469213ca6e7ea823340a7/lib/Hakyll/Core/Routes.hs)
module, we can bet that other functions for customizing our outputted routes
will be found in there. Let's see what we can find!

## Looking to `idRoute`, `setExtension` and Other `Routes` Functions for Clues

When we look at
[`Hakyll.Core.Routes`](https://github.com/jaspervdj/hakyll/blob/1abdeee743d65d96c6f469213ca6e7ea823340a7/lib/Hakyll/Core/Routes.hs),
we can see that
[`idRoute`](https://github.com/jaspervdj/hakyll/blob/1abdeee743d65d96c6f469213ca6e7ea823340a7/lib/Hakyll/Core/Routes.hs#L114-L115)
and
[`setExtension`](https://github.com/jaspervdj/hakyll/blob/1abdeee743d65d96c6f469213ca6e7ea823340a7/lib/Hakyll/Core/Routes.hs#L136-L138),
which we know are used with
[`route`](https://github.com/jaspervdj/hakyll/blob/1abdeee743d65d96c6f469213ca6e7ea823340a7/lib/Hakyll/Core/Rules.hs#L175),
both return a type of `Routes`. The implementation of `Routes` is not important
for us here, for our job now is to see what _other_ functions return `Routes`,
as well, so that we can potentially leverage their functionality.

Doing a quick search in that module reveals to us some very interesting results!
* [`customRoute`](https://github.com/jaspervdj/hakyll/blob/1abdeee743d65d96c6f469213ca6e7ea823340a7/lib/Hakyll/Core/Routes.hs#L152-L153)
* [`constRoute`](https://github.com/jaspervdj/hakyll/blob/1abdeee743d65d96c6f469213ca6e7ea823340a7/lib/Hakyll/Core/Routes.hs#L159-L160)
* [`gsubRoute`](https://github.com/jaspervdj/hakyll/blob/1abdeee743d65d96c6f469213ca6e7ea823340a7/lib/Hakyll/Core/Routes.hs#L173-L177)
* [`metadataRoute`](https://github.com/jaspervdj/hakyll/blob/1abdeee743d65d96c6f469213ca6e7ea823340a7/lib/Hakyll/Core/Routes.hs#L182-L185)

Alright! Now, what does each one do?
* `customRoute`: takes in a function that accepts an `Identifier` and returns a
  `FilePath` and returns that. Sounds like it could be useful, somehow... Let's
  keep going.
* `constRoute`: takes in a `FilePath`, wraps the value in a `const` function
  (which will always return the value it was passed) and then passes the
  function to `customRoute`! Okay, so this basically means if we say
  `constRoute "foo.html"`, then that's what the route will come out as. Makes
  sense.
* `gsubRoute`: this one's purpose is to use patterns to replace parts of routes
  (like transforming `"tags/rss/bar.xml"` to `tags/bar.xml`). Useful! But not
  for our task.
* `metadataRoute`: takes in a function that accepts `Metadata` and returns
  `Routes`, and then this function returns `Routes`. Since we want to access our
  `title` metadata to create a route, something that gives us access to
  `Metadata` and returns `Routes` is exactly what we want!

## Leveraging Hakyll's `metadataRoute` to Access Title Metadata
As with most things in the Haskell world, let's allow the types to guide us.
What do we know?
* `route` accepts a function whose return value is `Routes`
* `metadataRoute` ultimately returns `Routes` (yay!), but it first takes in a
  function that accepts `Metadata` and needs to return `Routes`.

Therefore, our task is to write a function with the signature
`Metadata -> Routes` that finds the `title` field in the metadata, converts it
to a URI slug, and transforms that `FilePath` into a `Routes`. Perhaps we could
call it `titleRoute` and then extract the conversion from `Metadata` to
`FilePath` to something like `fileNameFromTitle`? Good enough.

Also, what did we see earlier that can take a `FilePath` and return `Routes`?
`constRoute` to the rescue! With these initial bits figured out, let's sketch
this out :

```haskell
main :: IO ()
main = hakyllWith config $ do
    match "posts/*" $ do
        let ctx = constField "type" "article" <> postCtx

        route $ metadataRoute titleRoute -- THIS LINE
        compile $ pandocCompilerCustom
            >>= loadAndApplyTemplate "templates/post.html"    ctx
            >>= saveSnapshot "content"
            >>= loadAndApplyTemplate "templates/default.html" ctx

    -- ...other rules


titleRoute :: Metadata -> Routes
titleRoute = constRoute . fileNameFromTitle


fileNameFromTitle :: Metadata -> FilePath
fileNameFromTitle = undefined -- ???
```

Great! This is progress! We have the outline of what we need to accomplish. The
next task is to find the `title`, convert it to a slug and return a `FilePath`.
But first, we need to take a detour and write a `toSlug` function that we can
work with.

## Writing Our Own URI Slug Function

Taking inspiration from the archived project https://github.com/mrkkrp/slug, we
can write a module, `Slug.hs`, with a main function, `toSlug` that takes in
`Text` from `Data.Text` and transforms it from normal text to a slug. For
example, `"This example isn't good"` would be transformed into
`"this-example-isnt-good"`.

```haskell
{-# LANGUAGE OverloadedStrings #-}


module Slug (toSlug) where


import           Data.Char (isAlphaNum)
import qualified Data.Text as T


keepAlphaNum :: Char -> Char
keepAlphaNum x
    | isAlphaNum x = x
    | otherwise    = ' '


clean :: T.Text -> T.Text
clean =
    T.map keepAlphaNum . T.replace "'" "" . T.replace "&" "and"


toSlug :: T.Text -> T.Text
toSlug =
    T.intercalate (T.singleton '-') . T.words . T.toLower . clean
```

Once you do this, don't forget to open up your project's `.cabal` file, add in
this line and run `stack build` eventually:

```haskell
executable site
  -- ...
  other-modules:    Slug
```

Now that this is taken care of, let's return to the remaining task!

## Retrieving and Slugifying our Titles

The last step in our journey is to look up the `title` in the `Metadata`,
convert it to a slug and return a `FilePath`. Let's look at the implementation
and then talk about it:

```haskell
titleRoute :: Metadata -> Routes
titleRoute =
    constRoute . fileNameFromTitle


fileNameFromTitle :: Metadata -> FilePath
fileNameFromTitle =
    T.unpack . (`T.append` ".html") . toSlug . T.pack . getTitleFromMeta


getTitleFromMeta :: Metadata -> String
getTitleFromMeta =
    fromMaybe "no title" . lookupString "title"
```

1. `getTitleFromMeta`: use `Metadata`'s
   [`lookupString`](https://github.com/jaspervdj/hakyll/blob/1abdeee743d65d96c6f469213ca6e7ea823340a7/lib/Hakyll/Core/Metadata.hs#L36-L37)
   function to search for `title` and handle the `Maybe String` return value by
   providing a fallback of `"no title"`
1. `fileNameFromTitle`: once we get the `title` `String`, convert it to type
   `Text`, pass that to the slugify function, append `.html` to the slugified
   `title`, then convert it back to a `String` (`FilePath` is a type alias of
   `String`, so no worries here)
1. `titleRoute`: once we have a `FilePath` value, we pass it to `constRoute` to
   get back our `Routes` type that `metadataRoute` requires, and we're done!

## Wrapping Up

While it would be awesome if this sort of thing were built in to hakyll, this
experience has shown me that in a way, the core of hakyll allows people to
customize their build to their heart's delight, and perhaps an implementation
such as this would be useful as a hakyll plugin. Maybe!

Next up: [Pt. 6 – Pure Builds With Nix](/hakyll-pt-6-pure-builds-with-nix.html)

* * *

Thank you for reading!
<br />
Robert
