Tag-based Navigation using Hakyll
For this website, I wanted to use Hakyll to generate a navigation sidebar listing the Tags of all available posts. Hakyll is not only a static site generator but also a build system with dependency tracking. The navigation sidebar is a bit tricky to implement from this perspective, as it causes every page to depend on every post. Consequently, changing any post would rebuild the entire side.
Until recently, Hakyll’s dependency tracking only supported depending on the content of an underlying file.
Similar to make(1), it would consider a resource to be modified if the mtime of this file changed.
That is, it wasn’t possible to only depend on the Metadata of a post.
In order to resolve this limitation, I wrote a patch that allows depending exclusively on the metadata of a post.
Thereby, for my use case, the navigation sidebar will not be rebuilt unless the YAML metadata block of a post changed.
The patch got merged recently and starting with Hakyll >4.16 metadata-only dependencies will be fully supported.
This post will explain how to make use of this feature, using the navigation sidebar and tag pages as an example.
New Dependency Type
Unfortunately, the aforementioned patch requires a backwards-incompatible change to Hakyll’s Dependency representation. Prior to this change, the algebraic data type was defined as follows:
data Dependency
= PatternDependency Pattern (Set Identifier)
| IdentifierDependency Identifier
| AlwaysOutOfDateUsing this type, we could declare a dependency on a single Identifier or a set of them.
Further, it was possible to signify that a dependency was always out-of-date, causing it to be rebuilt every time.
It would have been possible to extend this type in a backwards compatible way by introducing two new constructors: IdentifierMetadataDependency and PatternMetadataDependency.
However, since this would duplicate the existing constructors, I instead introduced a new DependencySelector type.
Further, to distinguish dependencies on content and dependencies on metadata of a post, I also introduced a new sum type called DependencyKind.
These two types are defined as follows:
data DependencySelector
= PatternDependency Pattern (Set Identifier)
| IdentifierDependency Identifier
data DependencyKind = KindContent | KindMetadataUsing DependencySelector and DependencyKind we can now redefine the Dependency type itself to allow distinguishing different kinds of dependencies.
The new definition looks as follows:
data Dependency
= Dependency DependencyKind DependencySelector
| AlwaysOutOfDateThis is hopefully largely self-explanatory.
We can still depend on a single Identifier or a set of them through the DependencySelector.
However, through DependencyKind we can now also express if we want to depend on the entire content or only the metadata of this Identifier.
Based on the new Dependency type, existing Hakyll functions (such as makePatternDependency) have been refined to also expect a DependencyKind as a function argument.
Generating a Sidebar
We can now use these new types in order to generate a tag-based navigation sidebar with Hakyll. For this purpose, we define a sidebar compilation rule that only depends on post metadata. For my own website, the definition of this compilation rule looks as follows:
tags <- buildTags "posts/*" (fromCapture "tags/*.html")
create ["sidebar"] $ do
deps <- makePatternDependency KindMetadata (fromGlob "posts/*")
compile $ do
-- renderTagList/renderTags does not tell dependencies itself.
compilerTellDependencies [deps]
allTagsCtx <- constField "allTags" <$> renderTagList tags
makeItem []
>>= loadAndApplyTemplate "templates/sidebar.html" allTagsCtxThe sidebar compilation rule creates a dependency on a set of identifiers as obtained from the pattern posts/*.
Through the new DependencyKind function argument for makePatternDependency we declare that we only depend on the metadata of those posts (their tags).
Because our sidebar compilation rule depends only on the post’s metadata, the sidebar will not be rebuilt if only the content of a post changes.
This drastically reduces the overall build time for larger websites.
Generating Tag Pages
Metadata-only dependencies are also useful for generating tag pages, which can be referenced in the sidebar and list all posts with a given tag.
Hakyll already supports generating such pages via tagsRules.
The Rules for tag pages are obtained through a function provided as an argument to tagsRules:
String -> Pattern -> Rules ()Using the Pattern, which matches all posts associated with the tag, we can obtain the corresponding metadata using getAllMetadata.
Facilitating a second upstream patch, this function will implicitly add metadata dependencies for us.
If we want to sort the posts by recency, we need a list of Items that we can then pass to recentFirst.
For this, we write a custom function:
getMetadataItems :: Pattern -> Compiler [Item Metadata]
getMetadataItems p =
map (uncurry Item) <$> getAllMetadata pAdditionally, to render the metadata in a template, we need a Context.
Ideally, we want to create a listField to iterate over the posts in the template, as illustrated in the Hakyll tutorial.
For this, we also need some custom code:
postsField :: String -> [Item Metadata] -> Context String
postsField name posts =
let fields = dateCtx <> urlField "url" <> metadataField
in listField name fields (pure posts)Finally, we can now generate our tag pages in the Rules monad:
tagsRules tags $ \tagStr tagsPattern -> do
route idRoute
compile $ do
posts <- getMetadataItems tagsPattern >>= recentFirst
let listCtx = postsField "posts" posts <> defaultContext
makeItem []
>>= loadAndApplyTemplate "templates/posts.html" listCtx
>>= loadAndApplyTemplate "templates/default.html" defaultContext
>>= relativizeUrlsDue to the metadata-only dependencies introduced indirectly in getMetadataItems through getAllMetadata, each tag page will only be rebuilt if the metadata of any post listed on the tag page changes.
Similar to the sidebar, it will not be rebuilt if only the content of the post changed.
See Also
- Tutorial: Rules, routes and compilers
- Add tags to your Hakyll blog by Javran Cheng
- Opinionated Hakyll Tutorial by Troels Henriksen