Hakyll Pt. 5 – Generating Custom Post Filenames From a Title Slug
Info
| Summary | Leverage hakyll's own internals to output custom webpage URI routes using any metdata field – in this case, our post title. |
|---|---|
| Shared | 2019-03-16 |
| Revised | 2023-02-11 @ 16:00 UTC |
2023-02-03 update: I published the 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 and customized a fair bit.
- Pt. 1 – Setup & Initial Customization
- Pt. 2 – Generating a Sitemap XML File
- Pt. 3 – Generating RSS and Atom XML Feeds
- Pt. 4 – Copying Static Files For Your Build
- Pt. 5 – Generating Custom Post Filenames From a Title Slug
- Pt. 6 – Pure Builds With Nix
- The hakyll-nix-template Tutorial
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".
- Where Do We Start? Hakyll’s
routeFunction - Looking to
idRoute,setExtensionand OtherRoutesFunctions for Clues - Leveraging Hakyll’s
metadataRouteto Access Title Metadata - Writing Our Own URI Slug Function
- Retrieving and Slugifying our Titles
Where Do We Start? Hakyll’s route Function
In the hakyll tutorial on basic routing, as well as other posts in this series, we have come across hakyll’s route function used in conjunction with functions like idRoute and setExtension. Given these functions live in the Hakyll.Core.Routes 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, we can see that idRoute and setExtension, which we know are used with route, 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 * constRoute * gsubRoute * metadataRoute
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 :
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".
{-# 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 . cleanOnce you do this, don’t forget to open up your project’s .cabal file, add in this line and run stack build eventually:
executable site
-- ...
other-modules: SlugNow 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:
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"getTitleFromMeta: useMetadata’slookupStringfunction to search fortitleand handle theMaybe Stringreturn value by providing a fallback of"no title"fileNameFromTitle: once we get thetitleString, convert it to typeText, pass that to the slugify function, append.htmlto the slugifiedtitle, then convert it back to aString(FilePathis a type alias ofString, so no worries here)titleRoute: once we have aFilePathvalue, we pass it toconstRouteto get back ourRoutestype thatmetadataRouterequires, 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
Thank you for reading!
Robert