Hakyll Pt. 5 – Generating Custom Post Filenames From a Title Slug
source link: https://www.tuicool.com/articles/hit/Ev2EzqZ
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
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
- (wip) Pt. 6 – Customizing Markdown Compiler Options
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
route
Function -
Looking to
idRoute
,setExtension
and OtherRoutes
Functions for Clues -
Leveraging Hakyll’s
metadataRoute
to 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!
Alright! Now, what does each one do?
-
customRoute
: takes in a function that accepts anIdentifier
and returns aFilePath
and returns that. Sounds like it could be useful, somehow… Let’s keep going. -
constRoute
: takes in aFilePath
, wraps the value in aconst
function (which will always return the value it was passed) and then passes the function tocustomRoute
! Okay, so this basically means if we sayconstRoute "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"
totags/bar.xml
). Useful! But not for our task. -
metadataRoute
: takes in a function that acceptsMetadata
and returnsRoutes
, and then this function returnsRoutes
. Since we want to access ourtitle
metadata to create a route, something that gives us access toMetadata
and returnsRoutes
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 isRoutes
-
metadataRoute
ultimately returnsRoutes
(yay!), but it first takes in a function that acceptsMetadata
and needs to returnRoutes
.
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 . 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:
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:
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
’slookupString
function to search fortitle
and handle theMaybe String
return value by providing a fallback of"no title"
-
fileNameFromTitle
: once we get thetitle
String
, convert it to typeText
, pass that to the slugify function, append.html
to the slugifiedtitle
, then convert it back to aString
(FilePath
is a a type alias ofString
, so no worries here) -
titleRoute
: once we have aFilePath
value, we pass it toconstRoute
to get back ourRoutes
type thatmetadataRoute
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:
- (wip) Pt. 6 – Customizing Markdown Compiler Options
Thank you for reading!
Robert
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK