Upgraded to Hakyll 4

March 20, 2013 / Mad Coding, Hakyll, Haskell

Hakyll 4 had been released for a while and I haven’t got around to upgrade until now. The old source code for my blog generated by Hakyll 3 will live in this branch. New code for Hakyll 4 will be on master.

Like previously I wanted to keep the same functionality that I had on WordPress. Luckily Hakyll 4 makes that easier. Below I will detail the changes I had to make coming from Hakyll 3.

Recent Posts On Every Page

My old WordPress blog showed 10 most recently added posts on every page. Although I might change this when I finally get a new design, for now I want to keep it exactly the same. That’s part of the challenge anyway.

With Hakyll 3 this wasn’t easy. I ran into dependency cycle issues and eventually used Rake to compile pages twice in order to make it work.

With Hakyll 4 this is much easier. With Hakyll 4, you can use getMatches to get all identifiers and produce the necessary HTML for recent posts.

recentPosts :: Compiler [Item String]
recentPosts = do
    identifiers <- getMatches ("posts/*" .&&. hasNoVersion)
    return [Item identifier "" | identifier <- identifiers]

recentPostList :: Compiler String
recentPostList = do
    posts   <- fmap (take 10) . recentFirst =<< recentPosts
    itemTpl <- loadBody "templates/indexpostitem.html"
    list    <- applyTemplateList itemTpl defaultContext posts
    return list

Then you can produce the necessary Context to supply to loadAndApplyTemplate. It’s all very easy now. For example:

let allCtx =
        field "recent" (\_ -> recentPostList) `mappend`

makeItem ""
    >>= loadAndApplyTemplate "templates/default.html" allCtx

Markdown Reference-Style Links

I like reference-style links. I think it’s easier to read. With Hakyll 4 this is how you would declare a custom write option:

-- Allow for reference style links in markdown
pandocWriteOptions = defaultHakyllWriterOptions
    { writerReferenceLinks = True

-- Render markdown by calling:
-- pandocCompilerWith defaultHakyllReaderOptions pandocWriteOptions

Paginated Post Listing

To generate paginated pages, I used to use code from Ian Ross’s blog. With Hakyll 4 the old code is no longer compatible, so I had to write my own. This is a great practice for me.

Initially I wasn’t quite sure how to begin until I saw Jasper’s suggestion on Google Groups to use getMatches. Below is what I ended up with. My implementation allows one to chop up all posts into equal size chunks per page.

paginate:: Int -> (Int -> Int -> [Identifier] -> Rules ()) -> Rules ()
paginate itemsPerPage rules = do
    identifiers <- getMatches "posts/*"

    let sorted = reverse $ sortBy byDate identifiers
        chunks = chunk itemsPerPage sorted
        maxIndex = length chunks
        pageNumbers = take maxIndex [1..]
        process i is = rules i maxIndex is
    zipWithM_ process pageNumbers chunks
            byDate id1 id2 =
                let fn1 = takeFileName $ toFilePath id1
                    fn2 = takeFileName $ toFilePath id2
                    parseTime' fn = parseTime defaultTimeLocale "%Y-%m-%d" $ intercalate "-" $ take 3 $ splitAll "-" fn
                in compare ((parseTime' fn1) :: Maybe UTCTime) ((parseTime' fn2) :: Maybe UTCTime)

In the code above, it’s basically doing the following:

The way I implemented pagination, you can basically do whatever you want with the page number and the truncated list of Identifier. For my own blog below is what I do.

paginate 2 $ \index maxIndex itemsForPage -> do
    let id = fromFilePath $ "blog/page/" ++ (show index) ++ "/index.html"
    create [id] $ do
        route idRoute
        compile $ do
            let allCtx =
                    field "recent" (\_ -> recentPostList) `mappend`
                loadTeaser id = loadSnapshot id "teaser"
                                    >>= loadAndApplyTemplate "templates/teaser.html" (teaserCtx tags)
                                    >>= wordpressifyUrls
            item1 <- loadTeaser (head itemsForPage)
            item2 <- loadTeaser (last itemsForPage)
            let body1 = itemBody item1
                body2 = if length itemsForPage == 1 then "" else itemBody item2
                postsCtx =
                    constField "posts" (body1 ++ body2) `mappend`
                    field "navlinkolder" (\_ -> return $ indexNavLink index 1 maxIndex) `mappend`
                    field "navlinknewer" (\_ -> return $ indexNavLink index (-1) maxIndex) `mappend`

            makeItem ""
                >>= loadAndApplyTemplate "templates/blogpage.html" postsCtx
                >>= loadAndApplyTemplate "templates/default.html" allCtx
                >>= wordpressifyUrls

Another very nice thing about Hakyll 4 is the ability to save snapshots. In generating paginated pages, I make use of “teaser” snapshot that I saved while processing individual blog entries in order to produce the snippet to show. See loadTeaser function above. For saving snapshot, see below:

pandocCompilerWith defaultHakyllReaderOptions pandocWriteOptions
    >>= saveSnapshot "teaser"

So there it is, my blog now powered by Hakyll 4. It’s definitely an improvement over the previous version. I also love that I got to write my own pagination code. Please do comment if you see ways I can improve my code. I’d love to learn.

The only remaining thing I need to work on at some point is fixing my RSS feed. It’s broken not because of the Hakyll 4 upgrade, but because there are certain cases not handled and produces an invalid feed.

UPDATE: I’ve now fixed my RSS feed too. Everything is working great now!