Dynamic Sidebar or Header Activation based on the Current Page in Hakyll
November 19, 2020, Posted by Heuna KimSituation
Hakyll is a haskell-based static site generator that is used to generate my blog. For the migration I ported Lanyon theme designed for Jekyll to lanyon-hakyll and here I describe one of the problems that I encountered.
The problem is an extension of what is described in this blog: Hakyll, where am I?
The page that you are navigating will be linked to one in your sidebar or your header depending on your layout unless it is one of posts. It is possible to statically link each of such pages manually. But if this list of pages is dynamically generated by loading all pages in some folder (e.g. in the pages
folder in lanyon-hakyll), it gets somewhat more complicated in Hakyll for the following reason.
An example based on liquid syntax in ruby for such activation will look like (the excerpt from Lanyon):
{% for node in pages_list %}
{% if node.title != null %}
<a class="sidebar-nav-item{% if page.url == node.url %} active{% endif %}" href="{{ node.url | absolute_url }}">{{ node.title }}</a>
{% endif %}
{% endfor %}
A direct translation of {% if page.url == node.url %}
in Hakyll is not possible, because the control flow of Hakyll $if(variable)$
does not evaluate the boolean value of variable
but merely checks whether the key variable
exists in the current context or not. Check out this tutorial for understanding the control flow of Hakyll templates.
Approaching the Solution
We will dynamically generate this constField
having the page title as a key in the context of listField
with a key pages_list
. First we add a snapshot to avoid a dependency cycle in compiling the pages
folder:
Define the context containing such listField
:
sidebarCtx :: Context String -> Context String
sidebarCtx nodeCtx =
listField "pages_list" nodeCtx (loadAllSnapshots "pages/*" "page-content") `mappend`
defaultContext
baseNodeCtx :: Context String
baseNodeCtx =
urlField "node-url" `mappend`
titleField "title" `mappend`
baseCtx
baseSidebarCtx = sidebarCtx baseNodeCtx
Add dynamically generated constField
with the current page title.
--- This is not enough.
import System.FilePath (takeBaseName)
match "pages/*" $ do
route $ setExtension "html"
compile $ do
pageName <- takeBaseName . toFilePath <$> getUnderlying
let pageCtx = constField pageName "" `mappend`
baseNodeCtx
let activeSidebarCtx = sidebarCtx pageCtx
pandocCompiler
>>= saveSnapshot "page-content"
...
>>= loadAndApplyTemplate "templates/default.html" (activeSidebarCtx <> siteCtx)
>>= relativizeUrls
The translation of the above html layout will be similar to:
<!-- THIS DOES NOT WORK -->
$for(pages_list)$
$if(title)$
<a class="sidebar-nav-item$if($title$)$ active$endif$" href="$baseurl$$node-url$">$title$</a>
$endif$
$endfor$
As you see in the comment, this is not enough because inside of $if(...)$
syntax, you cannot evaluate the key by surrounding them with $
.
Solution
We can add functionField
for evaluating a key for a given context. The functionField
needs a function with a type [String] -> Item String -> Compiler String
.
We define the following evalCtxKey
function for this purpose:
evalCtxKey :: Context String -> [String] -> Item String -> Compiler String
evalCtxKey context [key] item = (unContext context key [] item) >>= \cf ->
case cf of
StringField s -> return s
_ -> error $ "Internal error: StringField expected"
Just if you need, you can also access the meta data as follows:
getMetadataKey :: [String] -> Item String -> Compiler String
getMetadataKey [key] item = getMetadataField' (itemIdentifier item) key
The functions unContext
, getMetadataField'
, and data itemIdentifier
are already defined in Hakyll.
The following is the working version of compiling pages/*
:
match "pages/*" $ do
route $ setExtension "html"
compile $ do
pageName <- takeBaseName . toFilePath <$> getUnderlying
let pageCtx = constField pageName "" `mappend`
baseNodeCtx
let evalCtx = functionField "eval" (evalCtxKey pageCtx)
let activeSidebarCtx = sidebarCtx (evalCtx <> pageCtx)
pandocCompiler
>>= saveSnapshot "page-content"
...
>>= loadAndApplyTemplate "templates/default.html" (activeSidebarCtx <> siteCtx)
>>= relativizeUrls
and the sidebar layout:
$for(pages_list)$
$if(title)$
<a class="sidebar-nav-item$if(eval(title))$ active$endif$" href="$baseurl$$node-url$">$title$</a>
$endif$
$endfor$
If you want to look at an example code, please check out the codes of lanyon-hakyll.