Adding Tags to Hakyll

April 21, 2014 · 3 minute read

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.

  1. m is restricted to be an instance of MonadMetadata, (e.g., Rules).
  2. The first argument is of type Pattern. This pattern specifies the location of posts in which to detect tags (e.g., "posts/*").
  3. 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.

  1. The first argument is the Tags value we construct with buildTags.
  2. 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.
  3. 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