I'm trying to do something like what's described in this tutorial, i.e., add tags to my Hakyll blog, but instead of generating a page for every tag, just have one page that lists all tags and their posts. So given a Post1
tagged Tag1
, and a Post2
tagged Tag1, Tag2
, and a Post3
tagged Tag2
, my tags.html
would look like this:
Tag1:
- Post1
- Post2
Tag2:
- Post2
- Post3
But I'm a Haskell beginner, and I don't fully understand all of Hakyll's monadic contexts. Here's what I have so far:
create ["tags.html"] $ do
route idRoute
tags <- buildTags "posts/*" (fromCapture "tags.html")
compile $
makeItem ""
>>= applyTemplate tagListTemplate defaultContext
>>= applyTemplate defaultTemplate defaultContext
>>= relativizeUrls
>>= cleanIndexUrls
The problem is, I don't really know what Tags
are, in the context of my blog. I can't seem to print them out for debugging. (I tried adding print tags
, but it doesn't work.) So I'm having a really hard time thinking about how to proceed with this.
The complete file is here on GitHub.
Any help is much appreciated.
Update: I'm still not much closer to figuring this out. Here's what I'm trying now:
create ["tags.html"] $ do
route idRoute
tags <- buildTags "posts/*" (fromCapture "tags.html#")
let tagList = tagsMap tags
compile $ do
makeItem ""
>>= applyTemplate tagListTemplate (defaultCtxWithTags tags)
along with:
-- Add tags to default context, for tag listing
defaultCtxWithTags :: Tags -> Context String
defaultCtxWithTags tags = listField "tags" defaultContext (return (tagsMap tags)) `mappend` defaultContext
The full code, as it currently stands, is up here.
Any help with this would be much appreciated. I'm aware of all the documentation, but I can't seem to translate that into working code.
I have modified your site.hs
to create a rudimentary tags list page which I believe has the structure required: a list of tags, each of which contains a list of posts with that tag.
Here's a summary of each of the things that I had to do to get it to work:
{-# LANGUAGE ViewPatterns #-}
Not strictly necessary, but a nice language extension which I use once. I thought I'd use/mention it since you mentioned that you're a beginner to Haskell, and it's nice to know about.
tags <- buildTags "posts/*" (fromCapture "tags/*.html")
There are two changes needed to this line, compared to the buildTags
in your initial site.hs
. One is that it should probably moved out of the individual match
clauses into the top level Rules
monad, so that we can create individual tag pages if required. The other is that the capture was similarly changed from "tags.html#"
to "tags/*.html"
. This is important because Hakyll wants every Item
to have a unique Identifier
, and not every tags page is the same.
Having the individual tag pages with unique identifiers may not be strictly necessary, but simplifies the rest of the setup since a lot of the Hakyll machinery assumes they exist. In particular, the Tags:
line in the individual post descriptions was not previously being rendered correctly either.
For the same reason, it's a good idea to actually make these individual tag pages routable: without this stanza in the top-level Rules
monad, the tags on each post won't render correctly with the default tagsField
that you use, since they can't figure out how to link to an individual tag page:
tagsRules tags $ \tag pat -> do
route idRoute
compile $ do
posts <- recentFirst =<< loadAll pat
let postCtx = postCtxWithTags tags
postsField = listField "posts" postCtx (pure posts)
titleField = constField "title" ("Posts tagged \""++tag++"\"")
indexCtx = postsField <> titleField <> defaultContext
makeItem "" >>= applyTemplate postListTemplate indexCtx
>>= applyTemplate defaultTemplate defaultContext
>>= relativizeUrls
>>= cleanIndexUrls
Alright, that's the preliminaries. Now onto the main attraction:
defaultCtxWithTags tags = listField "tags" tagsCtx getAllTags `mappend`
defaultContext
Alright, the important thing added here is some tags
field. It will contain one item for each thing returned by getAllTags
, and the fields on each item will be given by tagsCtx
.
where getAllTags :: Compiler [Item (String, [Identifier])]
getAllTags = pure . map mkItem $ tagsMap tags
where mkItem :: (String, [Identifier]) -> Item (String, [Identifier])
mkItem x@(t, _) = Item (tagsMakeId tags t) x
What's getAllTags
doing? Well, it starts with tagsMap tags
, just like your example. But Hakyll wants the result to be an Item
, so we have to wrap it up using mkItem
. What's in an Item
other than the body? Just an Identifier
, and the Tags
object happens to contain a field that tells us how to get this! So mkItem
just uses tagsMakeId
to get an identifier and wraps up the given body with that identifier.
What about tagsCtx?
tagsCtx :: Context (String, [Identifier])
tagsCtx = listFieldWith "posts" postsCtx getPosts `mappend`
metadataField `mappend`
urlField "url" `mappend`
pathField "path" `mappend`
titleField "title" `mappend`
missingField
Everything starting with metadataField
is just the usual stuff we expect to get from defaultContext
; we can't use defaultContext
here since it wants to add a bodyField
, but the body of this Item
isn't a string (but instead a much more useful for us Haskell structure representing a tag). The interesting bit of this is line which adds the posts
field, which should look a bit familiar. The big difference is that it uses listFieldWith
instead of listField
, which basically means that getPosts
gets an extra argument, which is the body of the Item
that this field is on. In this case, that's the tag record from tagsMap
.
where getPosts :: Item (String, [Identifier])
-> Compiler [Item String]
getPosts (itemBody -> (_, is)) = mapM load is
getPosts
mostly just uses the load
function to get ahold of the Item
for each post given its Identifier
---it's a lot like the loadAll
you do to get all the posts on the index page, but it just gives you one post. The weird-looking pattern-match on the left is ViewPatterns
in action: it's basically saying that for this pattern to match, the pattern on the right of the ->
(i.e. (_, is)
) should match the result of applying the function on the left (i.e. itemBody
) to the argument.
postsCtx :: Context String
postsCtx = postCtxWithTags tags
postsCtx
is very simple: just the same postCtxWithTags
used everywhere else we render a post.
That's everything necessary to get a Context
with everything that you want; all that's left is to actually make a template to render it!
tagListTemplateRaw :: Html
tagListTemplateRaw =
ul $ do
"$for(tags)$"
li ! A.class_ "" $ do
a ! href "$url$" $ "$title$"
ul $ do
"$for(posts)$"
li ! A.class_ "" $ do
a ! href "$url$" $ "$title$"
"$endfor$"
"$endfor$"
This is just a very simple template that renders nested lists; you could of course do various things to make it fancier/nicer-looking.
I have made a PR to your repo so that you can see these changes in context here.
Here is what we've done to achieve this behaviour on our webpage:
Kowainik webpage tags building
And the example of the tag page:
https://kowainik.github.io/tags/haskell
You can ask any questions about the code :)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With