Upgraded to Hakyll 4
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`
defaultContext
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
where
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:
- Use
getMatches
to grab all posts’ identifier. i.e.[Identifier]
- Sort the
Identifier
list using individual identifier’sFilePath
which has the year-month-date format - Split up the
Identifier
list into groups of at mostitemsPerPage
each - Generate the page numbers for each group
- Call
rules
function for each group usingzipWithM_
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`
defaultContext
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`
defaultContext
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!