Short Attention Span Theatre

Leaving bread crumbs and playing with Hakyll

One of the things I wanted most in my conversion to Hakyll was a way to show the location bread­crumbs of the current page. For those unfamiliar with the concept, these do not show the actual path the user took to get to the page, but they do show the location of the current page in the website hierarchy. For example, if I were on a post called "Having my bread crumbs and eating them too!", then the bread crumb URLs on the page would be as follows.

sast :: posts :: Having my bread crumbs and eating them too!

Having bread crumbs like these allows users to have a better feel for "where" they are in a website, and they are neat. De­ter­min­ing how best to create those bread crumbs using hakyll was not easy, and I am certain my im­ple­men­ta­tion is overly com­pli­cat­ed, but I finally have something that works! The key insight which led to a working im­ple­men­ta­tion was the following:

An element of the page does not need to be visible.

That insight is not rocket science, and web designers are probably snickering under their breath, but I had been struggling with how to get various levels of the crumbs to appear and disappear depending on the visible level. The insight started me down a path that ended with the idea of combining templates, inline styles, and the CSS display property.

First, I assume that the website has three different levels: main (index and facets), category (indexes of posts and projects), and page (individual posts and projects). Changing the levels would require changing the code, but with this pattern in place adding levels should (for certain values of should) be trivial.

Next, I include the following HTML in the default template (default.html).

<div id="headercrumbs">
    <a class="headercrumb" id="headerpagetitle" href="$url$">$title$</a>
    <span class="headercrumb" style="display:$categoryIs$">::</span>
    <a class="headercrumb" style="display:$categoryIs$"
        href="/$category$">$category$</a>
    ::
    <a class="headercrumb" id="sitelink" href="/">sast</a>
</div>

Note that one of the crumb separators ("::") is bare, not surrounded by a span: this is because there will always be a "sitelink" (the link to the main page of this website), and there will always be a "head­er­pageti­tle". Thus, there will always be at least one crumb separator and two links in the bread crumbs.

Now comes the clever bit (or, at least I like to think it is the clever bit). Contexts are generated which contain two metadata fields. If the page is part of a category and not a category index, then the "category" field is set to the category name and the "cat­e­go­ry­Is" field is set to "inline". If the page is not part of a category or is a category index, then the "category" field is set to the empty string and the "cat­e­go­ry­Is" field is set to "none". All this magical ma­nip­u­la­tion happens with the help of three new contexts and two new functions, listed below.

defaultCtx :: Context String
defaultCtx = mconcat
    [ crumbCtx
    , crumbIsCtx
    , defaultContext
    ]

-- | Return String with the HTML code for bread crumbs of the current item.
-- This assumes three levels to the site: main (index, facets), category
-- (indices of posts, projects), page (individual posts, projects).
--
crumbCtx :: Context a
crumbCtx = field "category" $ \item -> do
    metadata <- getMetadata $ itemIdentifier item
    let crumb = findCategory (itemIdentifier item) categories
    return crumb

crumbIsCtx :: Context a
crumbIsCtx = field "categoryIs" $ \item -> do
    metadata <- getMetadata $ itemIdentifier item
    let crumbIs = findCategoryIs (itemIdentifier item) categories
    return crumbIs

findCategory :: Identifier -> [String] -> String
findCategory id cats
    | notElem '/' $ idPath                     = ""
    | hcat == []                               = ""
    | hcat == head (splitDirectories $ idPath) = hcat
    | otherwise                                = findCategory id $ tail cats
    where idPath = toFilePath id
          hcat = head cats

findCategoryIs :: Identifier -> [String] -> String
findCategoryIs id cats
    | notElem '/' $ idPath                     = "none"
    | hcat == []                               = "none"
    | hcat ++ "/index.md" == idPath            = "none"
    | hcat == head (splitDirectories $ idPath) = "inline"
    | otherwise                                = findCategoryIs id $ tail cats
    where idPath = toFilePath id
          hcat = head cats

The defaultCtx is only used when applying the default.html template, and thus should have minimal impact on other pages.

With the above code (and a bit of CSS) I have a happy little breadcrumb navigation section on the upper-right corner of my web site which updates based on the current page. It certainly is not the most elegant solution: the two functions have a lot of du­pli­ca­tion, as do the two contexts. For now though, until I figure out how to use the output of findCategory in findCategoryIs, it will do.

Changing the expiration date of my GPG key » « Setting hostname in FreeBSD when using dhclient
sast favicon