Post Navigation Links in Hakyll

Posted on December 18, 2014
Tags: [ meta, hakyll ]

I wrote an abstraction for generating page navigation links in Hakyll that supports both web comic-style and blog post navigation.

On my site, I have both a Comics and a Posts section: the first is a collection of comic strips, and the section a collection of blog posts. I wanted them to behave slightly differently with respect to naviation: comics should have the common first/prev/next/last navigation links, whereas for posts I wanted to have less intrusive navigation links that only linked to the previous and next post.

For the comics, I came up with five navigation links, which are placed both above and below the strip, so that you can avoid scrolling down in order to zap through the strips. Additionally, a click on the strip itself takes you to the next comic.

Posts are read differently than comic strips: most users will come there from a feed aggregator (did I mention that this site provides an RSS feed linkin the top right corner?) or from results in a search engine, will then read an article and maybe start to browse around the site. I expect linear reading to be less common than for comic strips. Therefore I wanted more subdued navigation links. My favorite style for these links is to simply have a link to the previous and next posts, and to display the title of these posts.

The function below calculates both comic-style first/prev/next/last links and the titles of the previous and next page:

getNavigationContext :: [Identifier] -> (String -> String) ->
                        Compiler (Context String)
getNavigationContext pageIdents modifyLink = do

  ident <- getUnderlying

  let findIdent ident = find (\ (a, _) ->
                                toFilePath a == toFilePath ident)

  let nextItem = fmap snd $ findIdent ident $
                 zip pageIdents (tail pageIdents)
  let prevItem = fmap snd $ findIdent ident $
                 zip (tail pageIdents) pageIdents

  let firstItem = case pageIdents of
        x:_ | toFilePath ident /= toFilePath x -> Just x
        _ -> Nothing
  let lastItem = case reverse pageIdents of
        x:_ | toFilePath ident /= toFilePath x -> Just x
        _ -> Nothing

  let maybeGetTitle = maybe (return Nothing)
                      (flip getMetadataField "title")

  prevTitle <- maybeGetTitle prevItem
  nextTitle <- maybeGetTitle nextItem

  let maybeGetRoute = maybe (return Nothing) getRoute

  nextUrl  <- maybeGetRoute nextItem
  prevUrl  <- maybeGetRoute prevItem
  firstUrl <- maybeGetRoute firstItem
  lastUrl  <- maybeGetRoute lastItem

  let maybeUrlField   name = maybe mempty
                             (constField name . toUrl . modifyLink)
      maybeConstField name = maybe mempty
                             (constField name)

  let ctx = mconcat
              maybeUrlField "nextUrl"  nextUrl,
              maybeUrlField "prevUrl"  prevUrl,
              maybeUrlField "firstUrl" firstUrl,
              maybeUrlField "lastUrl"  lastUrl,

              maybeConstField "nextTitle" nextTitle,
              maybeConstField "prevTitle" prevTitle
  return ctx

The function returns a context that contains the following fields:

  • firstUrl
  • prevUrl
  • nextUrl
  • lastUrl
  • prevTitle
  • nextTitle

Note that all fields that cannot be calculated (e.g. the prevUrl field on the first page) are not defined in the returned context.

One additional feature is that you can pass in a function to modify the file path of the first/prev/next/last links before they are converted to an URL. The example below shows how to make use of that. If you don’t need any modification, you can just pass in the id function.

I use the function in the following way:

            pages <- getMatches (comicsPattern .&&. hasNoVersion)

            let htmlLink = flip replaceExtension "html"
            navCtx <- getNavigationContext pages htmlLink

The resulting navCtx is then used to render individual comic post pages. Note the use of htmlLink in the call to getNavigationContext in order to replace the .png or .jpg extensions my comic files have with .html.

For blog posts, the function is used like this:

        posts <- getMatches postsPattern
        navCtx <- getNavigationContext posts id

No modification is done on the links. Because they are already correct.

Feel free to try out this little function in your Hakyll site – for me, it realizes exactly what I wanted!