7.7 KiB
Loading items
The compiler Monad is a complex beast, but this is nicely hidden for the user of the Hakyll library.
Suppose that you're generating index.html
which shows your latest brilliant
blogpost. This requires posts/foo.markdown
to be generated before
index.html
(so we don't have to generate it twice). But you don't have to care
about any of that: Hakyll will sort this out for you automatically!
Let's see some quick examples. We can load a specific item:
load "posts/foo.markdown" :: Compiler (Item String)
Or a whole bunch of them:
loadAll "posts/*" :: Compiler [Item String]
Sometimes you just want the contents and not the Item
:
loadBody "posts/foo.markdown" :: Compiler String
This is all useful if we want to use Hakyll's templating system.
Templates
Basic templates
Let's have a look at a simple template:
<h1>$title$</h1>
<div class="info">Posted on $date$</div>
$body$
As you can probably guess, template files just contain text and only the $
character has special meaning: text between dollar signs ("fields") is replaced
when the template is applied. If you want an actual dollar sign in the output,
use $$
.
You usually compile the templates from disk using the aptly named
templateBodyCompiler
:
match "templates/*" $ compile templateBodyCompiler
Notice the lack of route
here: this is because we don't need to write the
templates to your _site
folder, we just want to use them elsewhere.
Templates: Context
We can easily guess the meaning of $title$
, $date$
, and $body$
, but these
are not hard-coded fields: they belong to a certain Context. A Context
determines how the fields are interpreted. It's a Monoid and therefore very
composable.
field
allows us to create a Context
for a single field:
field :: String -> (Item a -> Compiler String) -> Context a
Let's try this out. Note that this is for illustration purposes only: you
shouldn't have to write complicated fields often. We can implement the $body$
field like this:
field "body" $ \item -> return (itemBody item) :: Context String
And $title$
like this:
titleContext :: Context a
titleContext = field "title" $ \item -> do
metadata <- getMetadata (itemIdentifier item)
return $ fromMaybe "No title" $ M.lookup "title" metadata
And compose them using the Monoid
instance:
context :: Context String
context = mconcat
[ titleContext
, field "body" $ return . itemBody
]
Obviously, it would be tedious to implement things like titleContext
over and
over again for different websites and different fields. This is why hakyll
provides defaultContext
. defaultContext
is a composed Context
and allows
you to use:
$body$
for the body of the page;$url$
for the destination URL of the page;$path$
for the original filepath of the page;$foo$
where foo is specified in the metadata.
$date$
is not provided by default. In the scaffold, we use the convenience
context function dateField
, which will parse an Item
's filename to check if
it begins with a date. You can see how we add it in the definition of postCtx
in site.hs
:
postCtx :: Context String
postCtx =
dateField "date" "%B %e, %Y" `mappend`
defaultContext
Loading and applying templates
Now we know about templates, context and how to load arbitrary items. This gives us enough background information in order to understand you can apply a template:
compile $ do
tpl <- loadBody "templates/post.html"
pandocCompiler >>= applyTemplate tpl postCtx
Loading and then immediately applying a template is so common there's a shorthand function:
compile $
pandocCompiler >>= loadAndApplyTemplate "templates/post.html" postCtx
Control flow in templates
Sometimes string interpolation does not suffice, and you want a little more control over how your templates are layed out. Hakyll provides a few control structures for this. The syntax for these structures was based on the syntax used in pandoc templates, since Hakyll already has tight integration with pandoc.
Conditionals
In templates/post.html
of the example site we generated using hakyll-init
,
we see an example of a conditional:
<div class="info">
Posted on $date$
$if(author)$
by $author$
$endif$
</div>
This example should be pretty straightforward. One important thing to notice is
that $if(foo)$
does not check the truth value of $foo$
: it merely checks
if such a key is present.
Note that an if-else form is supported as well:
<div class="info">
Posted on $date$
$if(author)$
by $author$
$else$
by some unknown author
$endif$
</div>
Partials
Partials allow you to DRY up your templates by putting repetitive actions into
separate template files. You can then include them using
$partial("filename.html")$
.
An example can be found in templates/archive.html
:
Here you can find all my previous posts:
$partial("templates/post-list.html")$
This partial is just another template and uses the same syntax. Note that in
order to use something like this, we also need to load the partial template in
our site.hs
:
match "templates/post-list.html" $ compile templateCompiler
Fortunately, we usually don't need to add this since we already have:
match "templates/*" $ compile templateCompiler
Producing a list of items: for
At this point, everything in the example site we generated should be clear to
you, except for how we produce the list of posts in archive.html
and
index.html
. Let's look at the templates/post-list.html
template:
<ul>
$for(posts)$
<li>
<a href="$url$">$title$</a> - $date$
</li>
$endfor$
</ul>
This uses the $for(foo)$
construct. This construct allows you loop over a
list, in this case, $posts$
. Inside the body of this for loop, all fields
refer to the current post, e.g.: $url$
, $title$
and $date$
.
Of course, posts
does not magically appear. We have to specify this in
site.hs
. Let's look at how archive.html
is generated:
posts <- recentFirst =<< loadAll "posts/*"
let archiveCtx =
listField "posts" postCtx (return posts) `mappend`
constField "title" "Archives" `mappend`
defaultContext
We discussed loadAll
earlier in this tutorial.
recentFirst
sorts items by date. This relies on the convention that posts are
always named YYYY-MM-DD-title.extension
in Hakyll -- or that the date must be
present in the metadata.
recentFirst :: [Item a] -> Compiler [Item a]
After loading and sorting the items, we use listField
to create the $posts$
key.
listField :: String -> Context a -> Compiler [Item a] -> Context b
The first parameter is simply the name of the key ("posts"
). Secondly we have
a Context
with which all items should be rendered -- for our example site, we
already wrote such a Context
for posts: postCtx
. Lastly, we have a
Compiler
which loads the items. We already loaded the items so we can just use
return posts
.
The following snippet would produce the same result:
let archiveCtx =
listField "posts" postCtx (recentFirst =<< loadAll "posts/*") `mappend`
constField "title" "Archives" `mappend`
defaultContext