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 baseNodeCtxAdd 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)
>>= relativizeUrlsThe 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) keyThe 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)
>>= relativizeUrlsand 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.