Links to previous and next post

Currently the landing page contains all the posts I’ve written so far. That works for now, but it won’t for very long if I keep on writing posts. It certainly won’t work when I get around to importing all the posts from my old blog. Clearly I need to limit the posts that appear in index.html at some point, but before that two things need to be in place:

  1. The post titles on the landing page should be links to the single-post page.
  2. The single-post pages should be linked together with links to the previous and next posts.

I found another hakyll-based site that already implemented such previous-next links, Richard Goulter’s blog. He even has a post about its implemantation. It was a rather straigth-forward implementation and it served well as inspiration.

Code changes

I want the links to have the post titles, not just general text like “previous post” and “next post”. That meant putting a total of four values into the context used to render the individual post pages, two URLs and two titles. That means postCtx will look like this:

let postCtx =
        field "previousPostUrl" (previousPostUrl "posts/*") <>
        field "previousPostTitle" (previousPostTitle "posts/*") <>
        field "nextPostUrl" (nextPostUrl "posts/*") <>
        field "nextPostTitle" (nextPostTitle "posts/*") <>

In implementing the functions to extract the URLs and titles I first tried working on a snapshot but that only resulted in circular dependencies, so I had to follow Richard’s lead and used Identifier to drive the whole thing. When done with functions for extracting both URL and title there was an obvious pattern so I generalised a little and refactored into a single function to do the grunt work:

withRelatedPost:: (MonadMetadata m, Alternative m) =>
    (Identifier -> [Identifier] -> Maybe t) -> (t -> m b) -> Pattern -> Item a -> m b
withRelatedPost r f pattern item = do
    idents <- getMatches pattern >>= sortRecentFirst
    let id = itemIdentifier item
        prevId = r id idents
    case prevId of
        Just i -> f i
        Nothing -> empty

The r argument is a function that given the ID of the current item, and a list of the IDs of all items (sorted with the most recent first) returns the ID of a single item. With that it’s possible to create two more functions, one that operates on the previous post and one on the next post:

withPreviousPost :: (MonadMetadata m, Alternative m) => (Identifier -> m b) -> Pattern -> Item a -> m b
withPreviousPost = withRelatedPost itemAfter
        itemAfter x xs = lookup x $ zip xs (tail xs)

withNextPost :: (MonadMetadata m, Alternative m) => (Identifier -> m b) -> Pattern -> Item a -> m b
withNextPost = withRelatedPost itemBefore
        itemBefore x xs = lookup x $ zip (tail xs) xs

Now getting the URLs and titles becomes a few single-line functions:

previousPostUrl :: Pattern -> Item String -> Compiler String
previousPostUrl = withPreviousPost (fmap (maybe empty toUrl) . getRoute)

previousPostTitle :: Pattern -> Item String -> Compiler String
previousPostTitle = withPreviousPost (\ i -> getMetadataField' i "title")

nextPostUrl :: Pattern -> Item String -> Compiler String
nextPostUrl = withNextPost (fmap (maybe empty toUrl) . getRoute)

nextPostTitle :: Pattern -> Item String -> Compiler String
nextPostTitle = withNextPost (flip getMetadataField' "title")

What’s left is putting the fields to use in a template. The template itself is in the following section. The compilation step is modified to use the new context, and the new template is put in after the snapshot so the added links don’t appear in the index.html.

    >>= loadAndApplyTemplate "templates/single-post.html" postCtx
    >>= saveSnapshot "posts-content"
    >>= loadAndApplyTemplate "templates/single-post-prev-next.html" postCtx
    >>= loadAndApplyTemplate "templates/default.html" postCtx
    >>= relativizeUrls

Template changes

The added template is short and rather short and obvious:


<a href="$previousPostUrl$">&#10232; $previousPostTitle$</a>

<a href="$nextPostUrl$">$nextPostTitle$ &#10233;</a>
Leave a comment