In this post I’ll show how to add tags (keyword strings) to a blog powered by Hakyll. This post assumes Hakyll 4.5.1.0.
Although not particular well-documented, Hakyll has built-in support for tags. A common feature of many blogs is the ability to list all the posts that have been tagged by a given tag (e.g., all the posts that have the tag hakyll might be listed at tags/hakyll.html).
Hakyll supplies the buildTags
and tagRules
functions for extracting tags from posts that define a tags
metadata element, then building output pages for each tag. Let’s look at buildTags
first:
buildTags :: MonadMetadata m => Pattern -> (String -> Identifier) -> m Tags
Let’s dissect the types.
m
is restricted to be an instance of MonadMetadata
, (e.g., Rules
).- The first argument is of type
Pattern
. This pattern specifies the location of posts in which to detect tags (e.g., "posts/*"
). - The second argument is a function of type
String -> Identifier
. This function takes a tag and returns the Identifier
for the output page that should contain the listing of posts with that tag. In other words, you supply this function to tell Hakyll where to place tag pages.
The return type is m Tags
, but what is the Tags
type?
data Tags = Tags
{ tagsMap :: [(String, [Identifier])]
, tagsMakeId :: String -> Identifier
, tagsDependency :: Dependency
} deriving (Show)
Tags
is just your basic product type, of which we’re only concerned with the first element: a map defined as a list of (String, [Identifier])
tuples. The first element of each tuple is a tag, and the second element is a list of identifiers corresponding to the posts with that tag.
To make use of a value of type Tags
, we’ll have to take a look at the tagRules
function.
tagsRules :: Tags -> (String -> Pattern -> Rules ()) -> Rules ()
Let’s dissect these types now.
- The first argument is the
Tags
value we construct with buildTags
. - The second argument is a function that defines the rule for creating a single tag page. In particular, the first argument of type
String
is the tag, and the second argument of type Pattern
is a pattern that matches all posts with that tag. The return value should be a rule for creating a single page for the corresponding tag. - The return value of
tagRules
is a rule for building all tag pages essentially by aggregating the per-page rules created via the second argument.
With that in mind, here’s how we might put it all together:
tags <- buildTags postsPattern (fromCapture "tags/*.html")
tagsRules tags $ \tagStr tagsPattern -> do
route $ idRoute
compile $ do
posts <- recentFirst =<< loadAll tagsPattern
postItemTpl <- loadBody "templates/post-list-item.html"
postList <- applyTemplateList postItemTpl postCtx posts
let tagCtx = constField "title" tagStr `mappend`
constField "postlist" postList `mappend`
constField "tag" tagStr `mappend`
defaultContext
makeItem ""
>>= loadAndApplyTemplate "templates/tags.html" tagCtx
>>= loadAndApplyTemplate "templates/default.html" tagCtx
>>= relativizeUrls