I’ve been using Hakyll for a long time – ever since I’ve had this blog (as opposed to any of my old WordPress etc blogs), and I like it a lot, although it’s taken a long time for me to feel like I actually understand anything it’s doing. I have known for a long time that Hakyll uses pandoc to render markdown files as html, and I have read that pandoc can automatically generate tables of contents from markdown headings. Since some of my posts are very long, I thought this would be a really useful thing to enable for this blog.
However, I also sometimes write short posts without many headings, so I wanted to automatically generate tables of contents only on posts where I needed them and not on all posts by default. It was relatively easy, with some googling for help, of course, to figure out how to enable the TOC generation, but it took me some time to figure out how to enable it only on certain posts. I thought I should write it down for future me or for anyone else who might want to do the same thing.
Note: I’m on hakyll-4.10 and stack resolver lts-10.3. I am fairly certain this would have to be written slightly differently for older versions of hakyll. UPDATE: Have updated to hakyll-4.12 and lts-12.13 without needing to change this.
I knew from reading the Hakyll docs that what I wanted was a helper function like this to turn on some pandoc options:
That enables the TOC generation all right, but it isn’t conditional on having, oh, a certain length or certain types of headings that would generate the TOC, so on every post, even if there were no headings, that Contents heading was showing up. So the trouble was figuring out how to make it conditional.
In the general post html template, there’s a conditional that looks like this:
If an author is listed in the post metadata, then it will put a byline on the rendered post; if that field is missing from the metadata, it does nothing. My first efforts to make the appearance of the TOC were, therefore, centered around that: I added a
withtoc field to my metadata (that’s also where the title and tags go, in their own fields).
After playing around with it directly in the html template, I figured out what the problem was: I hadn’t told the post compiler to look for that field in the metadata and know what to do with it.
postCompiler :: Compiler (Item String) postCompiler = do tags <- buildTags postsGlob (fromCapture "tags/*.html") ident <- getUnderlying -- these are the five lines toc <- getMetadataField ident "withtoc" -- that I added to this let writerSettings = case toc of -- function today Just _ -> withToc -- in order to make my TOC Nothing -> defaultHakyllWriterOptions -- conditional pandocCompilerWith defaultHakyllReaderOptions writerSettings >>= saveSnapshot "content" >>= loadAndApplyTemplate "templates/post.html" (postCtxWithTags tags) >>= loadAndApplyTemplate "templates/default.html" (postCtxWithTags tags) >>= relativizeUrls
(Many Hakyll configurations, including the default initial configuration, I believe, will have this as part of the larger
main rather than split off into its own function. I have started decomposing that
main block in my own site code because I find it so much easier to think about the parts separately and then combine them at the end, but ymmv. If you have a more standard Hakyll
site.hs, then you’d need to add this to the post compiler in your
main, wherever you
match on the
postsGlob or something like that and specify the compiler instructions.)
When I had added the tags to my blog posts, I had to modify this
postCompiler function, as you can see in the first line after the
do, so it would know what to do with the data in the
tags field. I did basically the same thing to make a
writerSettings that can be conditional on the appearance of the
withtoc field: when that field is present now, it will compile the post with my special
withToc writer options; when that field isn’t present, it will just use the defaults. I suspect there are other ways to accomplish this same thing, but this all works and so we’re calling it good.
The final thing I changed was adding html directly into my Haskell file to tell it to add a header when it does generate a TOC and allow me to style it. Not everyone has a header on their TOCs (the Hakyll tutorials, for example, are bulleted but don’t have a header). I also wanted to add some
<div>s so I could style it. Anyway, so I had to change the last line of my
withToc function as below:
That gave me the heading “Contents” inside some
<div> classes so that I could spend the rest of my day messing with CSS.