metadatalistfieldhakyll

Creating a list field from metadata in Hakyll


I'm trying to generate HTML for posts in Hakyll that have a versions entry in their metadata. For example, a post may have versions: Python 3.4, pytest 1.5.2 which would be formatted nicely at the bottom of the post.

To achieve this, I want to create a context which loads the metadata and creates a ListField. Something like the following stub:

versionsCtx :: Context String
versionsCtx = listFieldWith "versions" ctx (\item -> do
    versions <- getMetadataField (itemIdentifier item) "versions"

    return $ case versions of
      Just lst -> map (mkVersinoItem . trim) $ splitAll "," lst
      Nothing  -> [])
          where ctx = field "version" (return . itemBody)
                mkVersionItem version = Item {
                    itemIdentifier = fromString ("version/" ++ version),
                    itemBody = version
                }

In my post.html template, I have:

...
    <section>
        $body$

        $if(versions)$
        <hr />
        <ul>
            $for(versions)$
                <li>$version$</li>
            $endfor$
        </ul>
        $else$
            <p>Fail...</p>
        $endif$
    </section>
...

Yet I have tried many different definitions of versionsCtx and found similar attempts online. None seem to work and the post is always rendered with "Fail...". What am I doing wrong?

EDIT: Updated question with suggestions and clarifications.


Solution

  • There are multiple issues with your code:

    1. getMetadataField provides a Maybe type, which in Haskell has data constructors Just and Nothing, not Some and None.
    2. The makeItem function creates an Item already wrapped in a Compiler, resulting in the following error:
    • Couldn't match type ‘Compiler (Item String)’ with ‘Item String’
      Expected type: Compiler [Item String]
        Actual type: Compiler [Compiler (Item String)]
    

    While you could try to extract the item from it, it is probably cleaner to create an item from scratch using something like this:

    mkVersionItem version = Item {
        itemIdentifier = fromString ("version/" ++ version),
        itemBody = version
    }
    
    1. I do not see you adding the newly created context to the post context. Did you do that?
    2. As mentioned in the docs, the order Contexts are being appended is important. It is not evident from your question, but you are probably using defaultContext, which includes metadataField. You have versions field in the metadata block of your posts so when the defaultContext wins, it will make versions available as a string field in the template. $if(versions)$ for some reason jumps to the else branch when versions is a string field, which explains why “Fail” is shown. You can see a more informative error in the console when you move the for loop outside the conditional block:
    [ERROR] Hakyll.Web.Template.applyTemplateWith: expected ListField but got StringField for expr versions
    

    In full the code could look something like this:

    import           Data.String (fromString)
    
    postCtx :: Context String
    postCtx =
        versionsCtx `mappend`
        dateField "date" "%B %e, %Y" `mappend`
        defaultContext
    
    versionsCtx :: Context String
    versionsCtx = listFieldWith "versions" ctx (\item -> do
        versions <- getMetadataField (itemIdentifier item) "versions"
    
        return $ case versions of
          Just lst -> map (mkVersionItem . trim) $ splitAll "," lst
          Nothing     -> []
        )
      where
        ctx = field "version" (return . itemBody)
        mkVersionItem version = Item {
            itemIdentifier = fromString ("version/" ++ version),
            itemBody = version
        }