After several attempts to run and actually maintain some personal blog, I finally settled down on this one based on Hakyll. Main reason was that I decided to learn Haskell and the best way for me how to learn new technology is to use it for some real world application. After publishing several blog posts I realized that some of them are pretty long with many headings and having some kind of table of contents would definitely help to navigate them.
I spent some time by searching optimal solution and found that Pandoc, library used by Hakyll to convert Markdown to HTML, already contains pretty decent built-in support for this. In this blog post, I’ll demonstrate how to implement simple table of contents for blog posts, that can be easily customized to your specific needs.
1 Expected features
As first step, I wrote down main features I’d like to have in the ideal implementation:
- option to enable / disable table of contents per blog post
- automatic numbering for table of contents anchor links and also blog post headings
- render table of contents only for full blog posts, not for previews on landing page
Fortunately all of these can be implemented in pretty straightforward way, as shown in following chapters.
Pandoc already contains support for rendering table of contents and it can be enabled by setting the writerTableOfContents field from WriterOptions to
True. Hakyll implements support for this as well, so the rendered table of contents is then available as
$toc$ context field.
2.1 Integration with Hakyll
writerNumberSections option is worth mentioning, because it automatically adds numbering to both table of content links and the headings inside blog post (as you can also see on this page). These
WriterOptions can then be used for rendering blog posts like this:
Problem with this implementation is that table of contents is rendered always for each blog post, which may be unwanted, mainly for shorter ones. Let’s see how this can be fixed.
2.2 Enabling per blog post
One way how to implement enabling/disabling of table of contents per blog post is to detect presence of some custom field in YAML header of the markdown file. Let’s say we want to have it disabled by default and enable it by adding
tableOfContents field to YAML header:
Based on presence of this field, we would choose whether to render or not the table of contents (we aren’t checking the actual value, just whether the field is present or not):
match "posts/*" $ do route $ setExtension "html" compile $ do underlying <- getUnderlying toc <- getMetadataField underlying "tableOfContents" let writerOptions' = maybe defaultHakyllWriterOptions (const withTOC) toc pandocCompilerWith defaultHakyllReaderOptions writerOptions' >>= loadAndApplyTemplate "templates/default.html" defaultContext
2.3 Adding stylesheets
Although the above code renders the table of content for blog posts and adds automatic numbering to heading, it would be still nice to add some CSS to make things better looking.
2.3.1 Adding styles to table of contents
First thing we need to do is to wrap the rendered table of contents into some
<div> container with custom class, so we can refer it later in stylesheet. This can be done by changing the
Now we can add proper styling to the
.toc CSS class. If you want to change styles for the section numbers of table of contents (as used on this page), you can modify it using the
2.3.2 Adding styles to headings
Headings itself now contain the automatically generated section numbers, and it’s likely that you’d like to visually separate them from the rest of the heading. This can be done by adding styles to
2.4 Making headings clickable