Next and previous links on blog posts

Filed under: programming

Taking a break from presentations to work on this blog a bit. Finally added the links at the bottom of posts that link to the next and previous posts! Here’s how it looks like on the front end:

Design of 'Next Post' link, from the [cleanMagic-Hakyll][7] theme

This design was originally created by Lucas Gatsas for his Jekyll theme. Beautiful and clean, definitely wanted to include it for this blog!

My first step to figure out how to do this was check the Haykll Tutorials and the Mailing List. The tutorials had no information about building pagination, but someone was so helpful about providing links in the mailing list. Thank goodness :) I picked the example provided by Richard Goulter, which generates a nextPost and prevPost value for each post. These two strings are generated on the Haskell side and plugged into the post template. The string values change per post.

HTML Markup

I’m borrowing heavily from the original Jekyll template. Instead of ``, here we use Hakyll’s template tags which is defined in this example as prevPost. We want to have the conditional if(variable) statements for the edge casses of using this template for the very 1st post and very last post.

<ul class="pager">
    if(prevPost)
        <li class="previous">
            <a href="prevPost" data-toggle="tooltip" data-placement="top" title="Previous Post">&larr; Previous Post</a>
        </li>
    endif

    if(nextPost)
        <li class="next">
            <a href="nextPost" data-toggle="tooltip" data-placement="top" title="Next Post">Next Post &rarr;</a>
        </li>
    endif
</ul>

The data attributes shown above for data-toggle and data-placement are bootstrap specific, which defines the type of label that will show up when the mouse hovers over the link. The example shows a tooltip type that appears above the link.

Haskell

Using Richard’s example, my Haskell code for creating the two strings looked like this:

    match "posts/**.markdown"  do
        route  setExtension "html"
        compile  do
            let postContext =
                    field "nextPost" nextPostUrl `mappend`
                    field "prevPost" previousPostUrl `mappend`
                    defaultContext

            pandocCompiler
                >>= saveSnapshot "teaser"
                >>= loadAndApplyTemplate "templates/post.html"    postContext
                >>= loadAndApplyTemplate "templates/default.html" postContext
                >>= relativizeUrls

The code says that for all the markdown files in subdirectory posts, create an html file with metadata defined for the default context (i.e. title) and include nextPost and prevPost.

nextPost gets its value from the function nextPostUrl and similarly prevPost gets its value from the function previousPostUrl. The following functions are copied from his example.

previousPostUrl :: Item String -> Compiler String
previousPostUrl post = do
    posts <- getMatches "posts/**.markdown"
    let ident = itemIdentifier post
        sortedPosts = sortIdentifiersByDate posts
        ident' = itemBefore sortedPosts ident
    case ident' of
        Just i -> (fmap (maybe empty  toUrl) . getRoute) i
        Nothing -> empty


nextPostUrl :: Item String -> Compiler String
nextPostUrl post = do
    posts <- getMatches "posts/**.markdown"
    let ident = itemIdentifier post
        sortedPosts = sortIdentifiersByDate posts
        ident' = itemAfter sortedPosts ident
    case ident' of
        Just i -> (fmap (maybe empty  toUrl) . getRoute) i
        Nothing -> empty

we also need some functions for sorting and finding the adjacent item and url of the post.

sortIdentifiersByDate :: [Identifier] -> [Identifier]
sortIdentifiersByDate identifiers =
    reverse  sortBy byDate identifiers
        where
          byDate id1 id2 =
            let fn1 = takeFileName  toFilePath id1
                fn2 = takeFileName  toFilePath id2
                myTime fn = parseTime defaultTimeLocale "%Y-%m-%d" 
                        intercalate "-"  take 3  splitAll "-" fn
            in compare ((myTime fn1) :: Maybe UTCTime) ((myTime fn2) :: Maybe UTCTime)

itemAfter :: Eq a => [a] -> a -> Maybe a
itemAfter xs x =
    lookup x  zip xs (tail xs)

itemBefore :: Eq a => [a] -> a -> Maybe a
itemBefore xs x =
    lookup x  zip (tail xs) xs

urlOfPost :: Item String -> Compiler String
urlOfPost =
    fmap (maybe empty  toUrl) . getRoute . itemIdentifier

Don’t forget your imports =)

import Data.Time.Format (parseTime)
import System.Locale (defaultTimeLocale)
import Data.Time.Clock (UTCTime)

import System.FilePath (takeFileName)
import Data.List (intercalate, sortBy)
import Data.Maybe (fromMaybe)
import Control.Applicative (Alternative (..))

That’s it! Thanks for reading this post. Enjoy the convenient links below!