Add tutorial about templates
This commit is contained in:
parent
d0a9d01062
commit
d3b87ba259
7 changed files with 598 additions and 555 deletions
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
title: How to write pages
|
||||
what: elaborates a little on writing pages and templates
|
||||
what: elaborates a little on writing pages
|
||||
---
|
||||
|
||||
## The structure of a Page
|
||||
|
|
|
@ -1,157 +1,60 @@
|
|||
---
|
||||
title: Creating a Blog
|
||||
what: creates a simple blog
|
||||
title: How to write templates
|
||||
what: more information on template writing
|
||||
---
|
||||
|
||||
## Creating a simple blog with Hakyll
|
||||
## Simple templates
|
||||
|
||||
After we created a simple brochure site, we're going to try something more
|
||||
advanced: we are going to create a simple blog system.
|
||||
|
||||
A [zip file] containing the source for this tutorial is also available.
|
||||
|
||||
[zip file]: $root/examples/simpleblog.zip
|
||||
|
||||
Blogs, as you probably know, are composed of posts. In Hakyll, we're going
|
||||
to use simple pages for posts. All posts are located in the `posts`
|
||||
directory. But we're not going to use the `directory` command here - you will
|
||||
see why later. First, some trivial things like css.
|
||||
|
||||
~~~~~{.haskell}
|
||||
main = hakyll "http://example.com" $ do
|
||||
directory css "css"
|
||||
~~~~~
|
||||
|
||||
## Finding the posts
|
||||
|
||||
`Text.Hakyll.File` contains a handy function `getRecursiveContents`, which will
|
||||
provide us with all the blog posts. The blog posts have a
|
||||
`yyyy-mm-dd-title.extension` naming scheme. This is just a simple trick so we
|
||||
can sort them easily (sorting on filename implies sorting on date). You could of
|
||||
course name them whatever you want, but it's always a good idea to stick to the
|
||||
conventions. They contain some metadata, too:
|
||||
|
||||
title: A first post
|
||||
author: Julius Caesar
|
||||
date: November 5, 2009
|
||||
---
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
|
||||
Vivamus pretium leo adipiscing lectus iaculis lobortis.
|
||||
Vivamus scelerisque velit dignissim metus...
|
||||
|
||||
Now, we find the posts and sort them reversed, so the most recent post will
|
||||
become the first item in the list:
|
||||
|
||||
~~~~~{.haskell}
|
||||
postPaths <- liftM (reverse . sort) $ getRecursiveContents "posts"
|
||||
~~~~~
|
||||
|
||||
Our `postPaths` value is now of the type `[FilePath]`. We want to be able to
|
||||
render all posts, so we pass them to the `createPage` function.
|
||||
|
||||
~~~~~{.haskell}
|
||||
let postPages = map createPage postPaths
|
||||
~~~~~
|
||||
|
||||
We have two templates we want to render our posts with: first we would like to
|
||||
render them using `templates/post.html`, and we want to render the result
|
||||
using `templates/default.html`. This can be done with the `renderChain`
|
||||
function:
|
||||
|
||||
~~~~~{.haskell}
|
||||
mapM_ (renderChain [ "templates/post.html"
|
||||
, "templates/default.html"
|
||||
]) postPages
|
||||
~~~~~
|
||||
|
||||
Remember that the `renderChain` works by rendering the item using the first
|
||||
template, creating a new page with the render result in the `$body` field, and
|
||||
so on until it has been rendered with all templates.
|
||||
|
||||
Now, we have the posts rendered. What is left is to generate some kind of index
|
||||
page with links to those posts. We want one general list showing all posts, and
|
||||
we want to show a few recent posts on the index page.
|
||||
|
||||
## Creating listings.
|
||||
|
||||
`createPage` is the easiest way of reading a `Context`. But in this case, we
|
||||
want something more custom, so we'll use the `createCustomPage` function. This
|
||||
allows us to create a more specific `Context`.
|
||||
|
||||
~~~~~{.haskell}
|
||||
createCustomPage :: FilePath
|
||||
-> [(String, Either String (HakyllAction () String)]
|
||||
-> HakyllAction () Context
|
||||
~~~~~
|
||||
|
||||
The first argument is the `url` of the page to generate. For our index page,
|
||||
this will be, `index.html`. The second argument is obviously our `key: value`
|
||||
mapping. But why the `Either`? This, once again, is about dependency handling.
|
||||
The idea is that you can choose which type to use for the value:
|
||||
|
||||
- `String`: Simply a `String`.
|
||||
- `HakyllAction () String`: Here, you can give an `HakyllAction` Arrow action
|
||||
that can produce a String. However - this action _will not be executed_ when
|
||||
the file in `_site` is up-to-date.
|
||||
|
||||
However, in this specific case - a list of posts - there is an easier, and more
|
||||
high-level approach than `createCustomPage`[^1]. Let's look at the type
|
||||
signature of `createListing`:
|
||||
|
||||
~~~~~{.haskell}
|
||||
createListing :: FilePath
|
||||
-> [FilePath]
|
||||
-> [HakyllAction () Context]
|
||||
-> [(String, Either String (HakyllAction () String)]
|
||||
-> HakyllAction () Context
|
||||
~~~~~
|
||||
|
||||
[^1]: Since Hakyll-1.3 onwards.
|
||||
|
||||
The first argument is the destination url. For our blog, this is of course
|
||||
`index.html`. The second argument is a list templates to render _each_ `Context`
|
||||
with. We use only `templates/postitem.html` here. This is, as you can see, a
|
||||
simple template:
|
||||
Simple templates are simply HTML files, with `$identifiers`. An example:
|
||||
|
||||
~~~~~{.html}
|
||||
<li>
|
||||
<a href="$$root/$url">$title</a>
|
||||
- <em>$date</em> - by <em>$author</em>
|
||||
</li>
|
||||
<html>
|
||||
<head>
|
||||
<title>$title</title>
|
||||
</head>
|
||||
<body>
|
||||
$body
|
||||
</body>
|
||||
</html>
|
||||
~~~~~
|
||||
|
||||
We then give a list of @Context@s to render. For our index, these are the 3 last
|
||||
posts. The last argument of the `createListing` functions lets you specify
|
||||
additional key-value pairs, like in `createCustomPage`. We use this to set the
|
||||
title of our page. So, we create our index page using:
|
||||
## Markup in templates
|
||||
|
||||
~~~~~{.haskell}
|
||||
let index = createListing "index.html"
|
||||
["templates/postitem.html"]
|
||||
(take 3 postPages)
|
||||
[("title", Left "Home")]
|
||||
~~~~~
|
||||
Most of the examples in these tutorials use HTML for templates. However, since
|
||||
Hakyll 2.2, it is possible use other markup languages in your templates. Simply
|
||||
use an appropriate extension, and Hakyll will pick it up. For example, you could
|
||||
write your `templates/post.markdown` template as:
|
||||
|
||||
The result of this will be a `HakyllAction () Context`. This `Context`'s `$body`
|
||||
will contain a concatenation of all the 3 posts, rendered with the
|
||||
`templates/postitem.html` template.
|
||||
# $title
|
||||
|
||||
Now, we only have to render it: first using the `index.html` template - which
|
||||
adds some more information to our index - then using the
|
||||
`templates/default.html` template.
|
||||
_On $date_
|
||||
|
||||
~~~~~{.haskell}
|
||||
renderChain ["index.html", "templates/default.html"] index
|
||||
~~~~~
|
||||
$body
|
||||
|
||||
Note that the `index.html` in the `renderChain` list is also a template. Now,
|
||||
you might want to take your time to read the `index.html` template and the other
|
||||
files in the zip so you understand what is going on here.
|
||||
__Warning__: you shouldn't use markdown for your "root" template, as these
|
||||
templates will never insert things like the doctype for you -- so you always
|
||||
need at least one top-level HTML template.
|
||||
|
||||
## The gist of it
|
||||
## Hamlet templates
|
||||
|
||||
- You can find blogposts using `getRecursiveContents`.
|
||||
- The convention is to call them `yyyy-mm-dd-rest-of-title.extension`. This
|
||||
allows us to sort them easily.
|
||||
- You can use `createCustomPage` or `createListing` to create custom pages and
|
||||
simple listings.
|
||||
From Hakyll 2.3 onwards, it is possible to use [hamlet] templates. You can find
|
||||
more information about hamlet on that website. Usage is fairly simple -- since
|
||||
pages are strictly key-value mappings, only `$variable$` control is supported in
|
||||
hamlet templates. As an example, here is the template that can be used for the
|
||||
brochure site, but in hamlet:
|
||||
|
||||
!!!
|
||||
%html
|
||||
%head
|
||||
%title MyAweSomeCompany - $$title$
|
||||
%body
|
||||
%h1 MyAweSomeCompany - $$title$
|
||||
#navigation
|
||||
%a!href="$$root$/index.html" Home
|
||||
%a!href="$$root$/about.html" About
|
||||
%a!href="$$root$/code.html" Code
|
||||
$body$
|
||||
|
||||
Hakyll will recognise hamlet templates automatically by the `.hamlet` extension.
|
||||
|
||||
[hamlet]: http://docs.yesodweb.com/hamlet/
|
||||
|
|
|
@ -1,84 +1,157 @@
|
|||
---
|
||||
title: Creating feeds
|
||||
what: adds an rss feed to the simple blog
|
||||
title: Creating a Blog
|
||||
what: creates a simple blog
|
||||
---
|
||||
|
||||
## Adding Feeds
|
||||
## Creating a simple blog with Hakyll
|
||||
|
||||
In this tutorial, we're going to add an RSS feed to the blog we wrote in the
|
||||
previous part. Here is a [zip file] containing the source.
|
||||
After we created a simple brochure site, we're going to try something more
|
||||
advanced: we are going to create a simple blog system.
|
||||
|
||||
[zip file]: $root/examples/feedblog.zip
|
||||
A [zip file] containing the source for this tutorial is also available.
|
||||
|
||||
You will be glad to hear that Hakyll has native support for RSS as well as Atom
|
||||
feeds[^1]. This simplifies our object a lot.
|
||||
[zip file]: $root/examples/simpleblog.zip
|
||||
|
||||
[^1]: Since Hakyll-2.0
|
||||
|
||||
This is the first time that the absolute URL of your site you have to give to
|
||||
the `hakyll` function actually matters. That's because the specifications of
|
||||
those feed formats dictate you need the full URL of them.
|
||||
|
||||
## Creating a configuration
|
||||
|
||||
The first thing to do is creating a configuration for your feed. You could
|
||||
place this code outside of your main function, as is done in the example.
|
||||
Blogs, as you probably know, are composed of posts. In Hakyll, we're going
|
||||
to use simple pages for posts. All posts are located in the `posts`
|
||||
directory. But we're not going to use the `directory` command here - you will
|
||||
see why later. First, some trivial things like css.
|
||||
|
||||
~~~~~{.haskell}
|
||||
myFeedConfiguration = FeedConfiguration
|
||||
{ feedUrl = "rss.xml"
|
||||
, feedTitle = "SimpleBlog RSS feed."
|
||||
, feedDescription = "Simple demo of a feed created with Hakyll."
|
||||
, feedAuthorName = "Jasper Van der Jeugt"
|
||||
}
|
||||
main = hakyll "http://example.com" $ do
|
||||
directory css "css"
|
||||
~~~~~
|
||||
|
||||
Note that we enter the url of the feed in our configuration. So the function
|
||||
to render our feed only takes two arguments, the configuration and a list of
|
||||
items to put in the feed. Let's put the three most recent posts in our feed.
|
||||
## Finding the posts
|
||||
|
||||
`Text.Hakyll.File` contains a handy function `getRecursiveContents`, which will
|
||||
provide us with all the blog posts. The blog posts have a
|
||||
`yyyy-mm-dd-title.extension` naming scheme. This is just a simple trick so we
|
||||
can sort them easily (sorting on filename implies sorting on date). You could of
|
||||
course name them whatever you want, but it's always a good idea to stick to the
|
||||
conventions. They contain some metadata, too:
|
||||
|
||||
title: A first post
|
||||
author: Julius Caesar
|
||||
date: November 5, 2009
|
||||
---
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
|
||||
Vivamus pretium leo adipiscing lectus iaculis lobortis.
|
||||
Vivamus scelerisque velit dignissim metus...
|
||||
|
||||
Now, we find the posts and sort them reversed, so the most recent post will
|
||||
become the first item in the list:
|
||||
|
||||
~~~~~{.haskell}
|
||||
renderRss myFeedConfiguration (take 3 postPages)
|
||||
postPaths <- liftM (reverse . sort) $ getRecursiveContents "posts"
|
||||
~~~~~
|
||||
|
||||
## But it's not that easy
|
||||
|
||||
If you look at our generated RSS feed (build the site), you will see
|
||||
`$description` tags appearing in our final render. We don't want that! How
|
||||
did they get there in the first place?
|
||||
|
||||
To render feeds, Hakyll expects a number of fields in the renderables you put
|
||||
in the feed. They are:
|
||||
|
||||
- `$title`: Title of the item. This is set in our posts, since we use a `title`
|
||||
metadata field.
|
||||
- `$url`: Url of the item. This is automatically set by Hakyll, so you shouldn't
|
||||
worry about it.
|
||||
- `$description`: A description of our item to appear in the feed reader.
|
||||
|
||||
The latter is obviously the problem: we don't have a description in our posts.
|
||||
In fact, we would like to copy the `$body` key to the `$description` key, so
|
||||
people can read the full post in their feed readers.
|
||||
|
||||
## Where arrows come in
|
||||
|
||||
The `Text.Hakyll.ContextManipulations` module contains a number of simple
|
||||
functions that create Arrows for us. One of these functions is `copyValue`,
|
||||
which takes a source and a destination key. So, we need to pass our
|
||||
items through this Arrow first.
|
||||
Our `postPaths` value is now of the type `[FilePath]`. We want to be able to
|
||||
render all posts, so we pass them to the `createPage` function.
|
||||
|
||||
~~~~~{.haskell}
|
||||
renderRss myFeedConfiguration $
|
||||
map (>>> copyValue "body" "description") (take 3 postPages)
|
||||
let postPages = map createPage postPaths
|
||||
~~~~~
|
||||
|
||||
And that's that, now our feed gets rendered properly. Exercise for the reader
|
||||
is to add a Atom feed[^2].
|
||||
We have two templates we want to render our posts with: first we would like to
|
||||
render them using `templates/post.html`, and we want to render the result
|
||||
using `templates/default.html`. This can be done with the `renderChain`
|
||||
function:
|
||||
|
||||
[^2]: Hint: look around in the [reference]($root/reference.html).
|
||||
~~~~~{.haskell}
|
||||
mapM_ (renderChain [ "templates/post.html"
|
||||
, "templates/default.html"
|
||||
]) postPages
|
||||
~~~~~
|
||||
|
||||
Remember that the `renderChain` works by rendering the item using the first
|
||||
template, creating a new page with the render result in the `$body` field, and
|
||||
so on until it has been rendered with all templates.
|
||||
|
||||
Now, we have the posts rendered. What is left is to generate some kind of index
|
||||
page with links to those posts. We want one general list showing all posts, and
|
||||
we want to show a few recent posts on the index page.
|
||||
|
||||
## Creating listings.
|
||||
|
||||
`createPage` is the easiest way of reading a `Context`. But in this case, we
|
||||
want something more custom, so we'll use the `createCustomPage` function. This
|
||||
allows us to create a more specific `Context`.
|
||||
|
||||
~~~~~{.haskell}
|
||||
createCustomPage :: FilePath
|
||||
-> [(String, Either String (HakyllAction () String)]
|
||||
-> HakyllAction () Context
|
||||
~~~~~
|
||||
|
||||
The first argument is the `url` of the page to generate. For our index page,
|
||||
this will be, `index.html`. The second argument is obviously our `key: value`
|
||||
mapping. But why the `Either`? This, once again, is about dependency handling.
|
||||
The idea is that you can choose which type to use for the value:
|
||||
|
||||
- `String`: Simply a `String`.
|
||||
- `HakyllAction () String`: Here, you can give an `HakyllAction` Arrow action
|
||||
that can produce a String. However - this action _will not be executed_ when
|
||||
the file in `_site` is up-to-date.
|
||||
|
||||
However, in this specific case - a list of posts - there is an easier, and more
|
||||
high-level approach than `createCustomPage`[^1]. Let's look at the type
|
||||
signature of `createListing`:
|
||||
|
||||
~~~~~{.haskell}
|
||||
createListing :: FilePath
|
||||
-> [FilePath]
|
||||
-> [HakyllAction () Context]
|
||||
-> [(String, Either String (HakyllAction () String)]
|
||||
-> HakyllAction () Context
|
||||
~~~~~
|
||||
|
||||
[^1]: Since Hakyll-1.3 onwards.
|
||||
|
||||
The first argument is the destination url. For our blog, this is of course
|
||||
`index.html`. The second argument is a list templates to render _each_ `Context`
|
||||
with. We use only `templates/postitem.html` here. This is, as you can see, a
|
||||
simple template:
|
||||
|
||||
~~~~~{.html}
|
||||
<li>
|
||||
<a href="$$root/$url">$title</a>
|
||||
- <em>$date</em> - by <em>$author</em>
|
||||
</li>
|
||||
~~~~~
|
||||
|
||||
We then give a list of @Context@s to render. For our index, these are the 3 last
|
||||
posts. The last argument of the `createListing` functions lets you specify
|
||||
additional key-value pairs, like in `createCustomPage`. We use this to set the
|
||||
title of our page. So, we create our index page using:
|
||||
|
||||
~~~~~{.haskell}
|
||||
let index = createListing "index.html"
|
||||
["templates/postitem.html"]
|
||||
(take 3 postPages)
|
||||
[("title", Left "Home")]
|
||||
~~~~~
|
||||
|
||||
The result of this will be a `HakyllAction () Context`. This `Context`'s `$body`
|
||||
will contain a concatenation of all the 3 posts, rendered with the
|
||||
`templates/postitem.html` template.
|
||||
|
||||
Now, we only have to render it: first using the `index.html` template - which
|
||||
adds some more information to our index - then using the
|
||||
`templates/default.html` template.
|
||||
|
||||
~~~~~{.haskell}
|
||||
renderChain ["index.html", "templates/default.html"] index
|
||||
~~~~~
|
||||
|
||||
Note that the `index.html` in the `renderChain` list is also a template. Now,
|
||||
you might want to take your time to read the `index.html` template and the other
|
||||
files in the zip so you understand what is going on here.
|
||||
|
||||
## The gist of it
|
||||
|
||||
- Hakyll has native support for RSS and Atom feeds.
|
||||
- The items must contain `$title` and `$description` fields.
|
||||
- Arrows can be used to copy values in a `Context`.
|
||||
- You can find blogposts using `getRecursiveContents`.
|
||||
- The convention is to call them `yyyy-mm-dd-rest-of-title.extension`. This
|
||||
allows us to sort them easily.
|
||||
- You can use `createCustomPage` or `createListing` to create custom pages and
|
||||
simple listings.
|
||||
|
|
|
@ -1,221 +1,84 @@
|
|||
---
|
||||
title: Tags and manipulations
|
||||
what: enhances our blog with tags and explains context manipulations.
|
||||
title: Creating feeds
|
||||
what: adds an rss feed to the simple blog
|
||||
---
|
||||
|
||||
## Context manipulations
|
||||
## Adding Feeds
|
||||
|
||||
Here, have [a zip file]($root/examples/tagblog.zip) for this tutorial.
|
||||
In this tutorial, we're going to add an RSS feed to the blog we wrote in the
|
||||
previous part. Here is a [zip file] containing the source.
|
||||
|
||||
You probably remember that `Context` objects are just key-value mappings. We can
|
||||
render those with templates, and then the `$key`'s in the template get
|
||||
substituted by the appropriate values. This is a rather flexible system, but
|
||||
there are limitations. Some of these limitations can be solved using
|
||||
_context manipulations_.
|
||||
[zip file]: $root/examples/feedblog.zip
|
||||
|
||||
Like rendering actions, _context manipulations_ are also simply
|
||||
`HakyllAction Context Context` arrows. The `Text.Hakyll.ContextManipulations`
|
||||
contains some functions to easily construct easy variants.
|
||||
You will be glad to hear that Hakyll has native support for RSS as well as Atom
|
||||
feeds[^1]. This simplifies our object a lot.
|
||||
|
||||
One of the most general functions is the `renderValue` function. Let's have a
|
||||
look at it's type.
|
||||
[^1]: Since Hakyll-2.0
|
||||
|
||||
This is the first time that the absolute URL of your site you have to give to
|
||||
the `hakyll` function actually matters. That's because the specifications of
|
||||
those feed formats dictate you need the full URL of them.
|
||||
|
||||
## Creating a configuration
|
||||
|
||||
The first thing to do is creating a configuration for your feed. You could
|
||||
place this code outside of your main function, as is done in the example.
|
||||
|
||||
~~~~~{.haskell}
|
||||
renderValue :: String
|
||||
-> String
|
||||
-> (String -> String)
|
||||
-> HakyllAction Context Context
|
||||
myFeedConfiguration = FeedConfiguration
|
||||
{ feedUrl = "rss.xml"
|
||||
, feedTitle = "SimpleBlog RSS feed."
|
||||
, feedDescription = "Simple demo of a feed created with Hakyll."
|
||||
, feedAuthorName = "Jasper Van der Jeugt"
|
||||
}
|
||||
~~~~~
|
||||
|
||||
This is the preferred way of creating context manipulations. The first argument
|
||||
is the `key` to manipulate. The second argument is the `key` where the new value
|
||||
should be placed. If this is the same as the first argument, it will be
|
||||
replaced. The third argument is the function to manipulate the `value` with.
|
||||
|
||||
As a simple example, let's write a function that puts the `$title` in uppercase.
|
||||
Note that we enter the url of the feed in our configuration. So the function
|
||||
to render our feed only takes two arguments, the configuration and a list of
|
||||
items to put in the feed. Let's put the three most recent posts in our feed.
|
||||
|
||||
~~~~~{.haskell}
|
||||
import Data.Char (toUpper)
|
||||
|
||||
titleUpper :: HakyllAction Context Context
|
||||
titleUpper = renderValue "title" "title" $ map toUpper
|
||||
renderRss myFeedConfiguration (take 3 postPages)
|
||||
~~~~~
|
||||
|
||||
Because the destination `key` is the same as the source `key`, we can also use
|
||||
the `changeValue` function here.
|
||||
## But it's not that easy
|
||||
|
||||
~~~~~{.haskell}
|
||||
titleUpper = changeValue "title" $ map toUpper
|
||||
~~~~~
|
||||
If you look at our generated RSS feed (build the site), you will see
|
||||
`$description` tags appearing in our final render. We don't want that! How
|
||||
did they get there in the first place?
|
||||
|
||||
For further reading, refer to the `Text.Hakyll.ContextManipulations`
|
||||
documentation.
|
||||
To render feeds, Hakyll expects a number of fields in the renderables you put
|
||||
in the feed. They are:
|
||||
|
||||
## Applying Context Manipulations
|
||||
|
||||
Because we're dealing with Arrows again, we can use `>>>` to apply our
|
||||
manipulations. For example, we could use or title manipulation like this:
|
||||
|
||||
~~~~~{.haskell}
|
||||
renderChain ["templates/default.html"]
|
||||
(createPage "index.markdown" >>> titleUpper)
|
||||
~~~~~
|
||||
|
||||
## Rendering dates
|
||||
|
||||
As you remember, in our previous blog, all posts had a file name like
|
||||
`posts/yyyy-mm-dd-title.extension`, as is the Hakyll convention. But they also
|
||||
had a metadata field `date`, containing a human-readable date. This is not very
|
||||
D.R.Y., of course! Hakyll has a specialized `renderValue` function to deal with
|
||||
dates encoded in paths: `renderDate`.
|
||||
|
||||
~~~~~{.haskell}
|
||||
postManipulation :: HakyllAction Context Context
|
||||
postManipulation = renderDate "date" "%B %e, %Y" "Unknown date"
|
||||
~~~~~
|
||||
|
||||
That manipulation will:
|
||||
- Read the date from the file name the post was loaded from.
|
||||
- Parse the date and render it in a `%B %e, %Y` format. This is a
|
||||
`Month day, Year` format.
|
||||
- Put the result in the `date` metadata field.
|
||||
- If the date could not be parsed, it will put `"Unknown date"` in the `date`
|
||||
- `$title`: Title of the item. This is set in our posts, since we use a `title`
|
||||
metadata field.
|
||||
- `$url`: Url of the item. This is automatically set by Hakyll, so you shouldn't
|
||||
worry about it.
|
||||
- `$description`: A description of our item to appear in the feed reader.
|
||||
|
||||
So, we can throw away our `date: ` lines from our posts, and still use `$date`
|
||||
in our templates.
|
||||
The latter is obviously the problem: we don't have a description in our posts.
|
||||
In fact, we would like to copy the `$body` key to the `$description` key, so
|
||||
people can read the full post in their feed readers.
|
||||
|
||||
## Abstracting the post list
|
||||
## Where arrows come in
|
||||
|
||||
Now, we're going to render tags. This is also done using context manipulations.
|
||||
Hakyll has a specialized module to deal with tags, provided by
|
||||
`Text.Hakyll.Tags`. This module assumes tags are comma separated, and placed in
|
||||
the `tags` metadata field.
|
||||
|
||||
---
|
||||
title: A third post
|
||||
author: Publius Ovidius Naso
|
||||
tags: epic fail, ovidius
|
||||
---
|
||||
Pellentesque tempor blandit elit, vel...
|
||||
|
||||
But first things first. We need to render a post list for every tag. We already
|
||||
had some code to render a list of all posts. We're just going to abstract this
|
||||
code into a more general function:
|
||||
|
||||
~~~~{.haskell}
|
||||
renderPostList url title posts = do
|
||||
let list = createListingWith url ["templates/postitem.html"]
|
||||
posts [("title", Left title)]
|
||||
renderChain ["posts.html", "templates/default.html"] list
|
||||
~~~~~
|
||||
|
||||
Our "render all posts" action can now be written as:
|
||||
The `Text.Hakyll.ContextManipulations` module contains a number of simple
|
||||
functions that create Arrows for us. One of these functions is `copyValue`,
|
||||
which takes a source and a destination key. So, we need to pass our
|
||||
items through this Arrow first.
|
||||
|
||||
~~~~~{.haskell}
|
||||
renderPostList "posts.html" "All posts" renderablePosts
|
||||
renderRss myFeedConfiguration $
|
||||
map (>>> copyValue "body" "description") (take 3 postPages)
|
||||
~~~~~
|
||||
|
||||
## Tag links
|
||||
And that's that, now our feed gets rendered properly. Exercise for the reader
|
||||
is to add a Atom feed[^2].
|
||||
|
||||
We want to display the tags for our post under the title. But if we use the
|
||||
`$tags` key in a template, we will just have the plain tags - they will not be
|
||||
clickable. We can again solve this with a `ContextManipulation`. We have a
|
||||
function that produces an url for a given tag:
|
||||
|
||||
~~~~~{.haskell}
|
||||
tagToUrl tag = "$root/tags/" ++ removeSpaces tag ++ ".html"
|
||||
~~~~~
|
||||
|
||||
`removeSpaces` is an auxiliary function from `Text.Hakyll.File`. Now, there is
|
||||
a specialized `renderValue` function for creating linked tags called
|
||||
`renderTagLinks`. This function simply takes a function that produces an url
|
||||
for a given tag - the function we just wrote. Let's extend our
|
||||
`postManipulation`.
|
||||
|
||||
~~~~~{.haskell}
|
||||
postManipulation :: HakyllAction Context Context
|
||||
postManipulation = renderDate "date" "%B %e, %Y" "Unknown date"
|
||||
>>> renderTagLinks tagToUrl
|
||||
~~~~~
|
||||
|
||||
We apply this manipulation when we load the tags.
|
||||
|
||||
~~~~~{.haskell}
|
||||
let renderablePosts =
|
||||
map ((>>> postManipulation) . createPage) postPaths
|
||||
~~~~~
|
||||
|
||||
So, the `renderTagLinks` function replaces the `$tags` value from
|
||||
`epic fail, random` to `<a href="$root/tags/epic-fail.html">epic fail</a>, ...`.
|
||||
If we click a tag, we get a `404`. That's because we haven't generated the
|
||||
post lists for every tag.
|
||||
|
||||
## The Tag Map
|
||||
|
||||
Hakyll provides a function called `readTagMap`. Let's inspect it's type.
|
||||
|
||||
~~~~~{.haskell}
|
||||
type TagMap = Map String [HakyllAction () Context]
|
||||
readTagMap String [FilePath] -> HakyllAction () TagMap
|
||||
~~~~~
|
||||
|
||||
You give it a list of paths, and it creates a map that, for every tag, holds
|
||||
a number of posts. We can easily use this to render a post list for every tag.
|
||||
The first argument given is an "identifier", unique to this tag map. Hakyll
|
||||
needs this so it can cache the tags.
|
||||
|
||||
~~~~~{.haskell}
|
||||
let tagMap = readTagMap "postTags" postPaths
|
||||
~~~~~
|
||||
|
||||
When we have the `TagMap`, we can need to render a post list for every tag.
|
||||
There is a function in Hakyll designed more or less for this purpose:
|
||||
`withTagMap`. This takes a `TagMap` and an action to execute for every tag and
|
||||
it's associated posts. We pass a small function to it we create ourselves[^1]:
|
||||
|
||||
[^1]: Exercise for the reader: why do we use `>>> postManipulation` again here?
|
||||
|
||||
~~~~~{.haskell}
|
||||
let renderListForTag tag posts =
|
||||
renderPostList (tagToUrl tag)
|
||||
("Posts tagged " ++ tag)
|
||||
(map (>>> postManipulation) posts)
|
||||
withTagMap tagMap renderPostList
|
||||
~~~~~
|
||||
|
||||
There we go. We now have clickable tags, and a post list for every tag.
|
||||
|
||||
## A Tag Cloud
|
||||
|
||||
A tag cloud is a commonly found thing on blogs. Hakyll also provides code to
|
||||
generate a tag cloud. Let's have a look at the `renderTagCloud` function.
|
||||
|
||||
~~~~~{.haskell}
|
||||
renderTagCloud :: (String -> String)
|
||||
-> Float
|
||||
-> Float
|
||||
-> HakyllAction TagMap String
|
||||
~~~~~
|
||||
|
||||
The first argument is, once again, a function to create an url for a given tag.
|
||||
Then, we give a minimum and a maximum font size in percent, and we get a tag
|
||||
cloud Arrow back. We can add this to our index:
|
||||
|
||||
~~~~~{.haskell}
|
||||
let tagCloud = tagMap >>> renderTagCloud tagToUrl 100 200
|
||||
index = createListing "index.html"
|
||||
["templates/postitem.html"]
|
||||
(take 3 renderablePosts)
|
||||
[ ("title", Left "Home")
|
||||
, ("tagcloud", Right tagCloud)
|
||||
]
|
||||
renderChain ["index.html", "templates/default.html"] index
|
||||
~~~~~
|
||||
[^2]: Hint: look around in the [reference]($root/reference.html).
|
||||
|
||||
## The gist of it
|
||||
|
||||
- There's some handy, simple functions in `Text.Hakyll.ContextManipulations`.
|
||||
- Seperate tags by commas and put them in the `$tags` field.
|
||||
- Use `withTagMap` to render a list for every tag.
|
||||
- Hakyll can also create tag clouds.
|
||||
- Hakyll has native support for RSS and Atom feeds.
|
||||
- The items must contain `$title` and `$description` fields.
|
||||
- Arrows can be used to copy values in a `Context`.
|
||||
|
|
|
@ -1,102 +1,221 @@
|
|||
---
|
||||
title: Interlude
|
||||
what: gives some various tips and tricks about Hakyll (quite handy, read this!)
|
||||
title: Tags and manipulations
|
||||
what: enhances our blog with tags and explains context manipulations.
|
||||
---
|
||||
|
||||
## Syntax-highlighting
|
||||
## Context manipulations
|
||||
|
||||
Pandoc (which Hakyll uses as a backend) offers powerful syntax highlighting.
|
||||
To enable this, Pandoc needs to be compiled with highlighting support. If this
|
||||
is not the case, you can fix this using:
|
||||
Here, have [a zip file]($root/examples/tagblog.zip) for this tutorial.
|
||||
|
||||
~~~~~
|
||||
[jasper@alice ~]$ cabal install --reinstall -fhighlighting pandoc
|
||||
~~~~~
|
||||
You probably remember that `Context` objects are just key-value mappings. We can
|
||||
render those with templates, and then the `$key`'s in the template get
|
||||
substituted by the appropriate values. This is a rather flexible system, but
|
||||
there are limitations. Some of these limitations can be solved using
|
||||
_context manipulations_.
|
||||
|
||||
## Auto-compilation
|
||||
Like rendering actions, _context manipulations_ are also simply
|
||||
`HakyllAction Context Context` arrows. The `Text.Hakyll.ContextManipulations`
|
||||
contains some functions to easily construct easy variants.
|
||||
|
||||
Hakyll features a simple _auto-compilation_ mode. This is invoked by running
|
||||
|
||||
~~~~~
|
||||
[jasper@alice ~]$ ./hakyll preview
|
||||
Starting hakyll server on port 8000...
|
||||
~~~~~
|
||||
|
||||
Now, Hakyll will recompile your site when you change files, so you can just
|
||||
refresh in your browser. There is one more thing to note: this will not update
|
||||
your site automatically when `hakyll.hs` changes. So if you make any changes to
|
||||
the configuration file, you'll have to compile it again, and then you can enter
|
||||
`preview` mode again.
|
||||
|
||||
## When to rebuild
|
||||
|
||||
If you execute a `./hakyll build`, Hakyll will build your site incrementally.
|
||||
This means it will be very fast, but it will not pick up _all_ changes.
|
||||
|
||||
- In case you edited `hakyll.hs`, you first want to compile it again.
|
||||
- It is generally recommended to do a `./hakyll rebuild` before you deploy your
|
||||
site.
|
||||
|
||||
## Pretty URL's
|
||||
|
||||
There is an option in Hakyll to produce pretty URL's, which is disabled by
|
||||
default because it can be confusing when you're first introduced to Hakyll.
|
||||
|
||||
It can be enabled this way:
|
||||
One of the most general functions is the `renderValue` function. Let's have a
|
||||
look at it's type.
|
||||
|
||||
~~~~~{.haskell}
|
||||
import Text.Hakyll
|
||||
import Text.Hakyll.Hakyll
|
||||
|
||||
myConfig :: HakyllConfiguration
|
||||
myConfig = (defaultHakyllConfiguration "http://jaspervdj.be")
|
||||
{ enableIndexUrl = True
|
||||
}
|
||||
|
||||
main = hakyllWithConfiguration myConfig $ do
|
||||
-- Further code here
|
||||
renderValue :: String
|
||||
-> String
|
||||
-> (String -> String)
|
||||
-> HakyllAction Context Context
|
||||
~~~~~
|
||||
|
||||
The effect will be that the internal `toUrl` function will behave differently.
|
||||
A few examples:
|
||||
This is the preferred way of creating context manipulations. The first argument
|
||||
is the `key` to manipulate. The second argument is the `key` where the new value
|
||||
should be placed. If this is the same as the first argument, it will be
|
||||
replaced. The third argument is the function to manipulate the `value` with.
|
||||
|
||||
- `about.html` will be rendered to `about/index.html`.
|
||||
- `posts/2010-02-16-a-post.markdown` will be rendered to
|
||||
`posts/2010-02-16-a-post/index.html`.
|
||||
- However, `index.markdown` will still be rendered to `index.html`. Likewise,
|
||||
`posts/index.html` would be rendered to `posts.index.html`.
|
||||
As a simple example, let's write a function that puts the `$title` in uppercase.
|
||||
|
||||
The benefit of this is simply prettier URL's. That is, if you consider
|
||||
`example.com/about` prettier than `example.com/about.html`.
|
||||
~~~~~{.haskell}
|
||||
import Data.Char (toUpper)
|
||||
|
||||
## Default values
|
||||
titleUpper :: HakyllAction Context Context
|
||||
titleUpper = renderValue "title" "title" $ map toUpper
|
||||
~~~~~
|
||||
|
||||
At some point, you might want to use a number of global key-value pairs, for
|
||||
example, `$author`. There are two possible ways to achieve this.
|
||||
Because the destination `key` is the same as the source `key`, we can also use
|
||||
the `changeValue` function here.
|
||||
|
||||
- There is an option in `HakyllConfiguration` supporting this, called
|
||||
`additionalContext`. For an example on how to use `HakyllConfiguration`, see
|
||||
the pretty URL's section above.
|
||||
~~~~~{.haskell}
|
||||
titleUpper = changeValue "title" $ map toUpper
|
||||
~~~~~
|
||||
|
||||
- Another option is to use a `defaults.markdown` file, simply containing some
|
||||
metadata, and then `combine` this file with other pages. The advantage is
|
||||
that autocompilation mode will pick up changes in this file[^1].
|
||||
For further reading, refer to the `Text.Hakyll.ContextManipulations`
|
||||
documentation.
|
||||
|
||||
[^1]: Original idea by zenzike.
|
||||
## Applying Context Manipulations
|
||||
|
||||
## Markup in templates
|
||||
Because we're dealing with Arrows again, we can use `>>>` to apply our
|
||||
manipulations. For example, we could use or title manipulation like this:
|
||||
|
||||
Most of the examples in these tutorials use HTML for templates. However, since
|
||||
Hakyll 2.2, it is possible use other markup languages in your templates. Simply
|
||||
use an appropriate extension, and Hakyll will pick it up. For example, you could
|
||||
write your `templates/post.markdown` template as:
|
||||
~~~~~{.haskell}
|
||||
renderChain ["templates/default.html"]
|
||||
(createPage "index.markdown" >>> titleUpper)
|
||||
~~~~~
|
||||
|
||||
# $title
|
||||
## Rendering dates
|
||||
|
||||
_On $date_
|
||||
As you remember, in our previous blog, all posts had a file name like
|
||||
`posts/yyyy-mm-dd-title.extension`, as is the Hakyll convention. But they also
|
||||
had a metadata field `date`, containing a human-readable date. This is not very
|
||||
D.R.Y., of course! Hakyll has a specialized `renderValue` function to deal with
|
||||
dates encoded in paths: `renderDate`.
|
||||
|
||||
$body
|
||||
~~~~~{.haskell}
|
||||
postManipulation :: HakyllAction Context Context
|
||||
postManipulation = renderDate "date" "%B %e, %Y" "Unknown date"
|
||||
~~~~~
|
||||
|
||||
__Warning__: you shouldn't use markdown for your "root" template, as these
|
||||
templates will never insert things like the doctype for you -- so you always
|
||||
need at least one top-level HTML template.
|
||||
That manipulation will:
|
||||
- Read the date from the file name the post was loaded from.
|
||||
- Parse the date and render it in a `%B %e, %Y` format. This is a
|
||||
`Month day, Year` format.
|
||||
- Put the result in the `date` metadata field.
|
||||
- If the date could not be parsed, it will put `"Unknown date"` in the `date`
|
||||
metadata field.
|
||||
|
||||
So, we can throw away our `date: ` lines from our posts, and still use `$date`
|
||||
in our templates.
|
||||
|
||||
## Abstracting the post list
|
||||
|
||||
Now, we're going to render tags. This is also done using context manipulations.
|
||||
Hakyll has a specialized module to deal with tags, provided by
|
||||
`Text.Hakyll.Tags`. This module assumes tags are comma separated, and placed in
|
||||
the `tags` metadata field.
|
||||
|
||||
---
|
||||
title: A third post
|
||||
author: Publius Ovidius Naso
|
||||
tags: epic fail, ovidius
|
||||
---
|
||||
Pellentesque tempor blandit elit, vel...
|
||||
|
||||
But first things first. We need to render a post list for every tag. We already
|
||||
had some code to render a list of all posts. We're just going to abstract this
|
||||
code into a more general function:
|
||||
|
||||
~~~~{.haskell}
|
||||
renderPostList url title posts = do
|
||||
let list = createListingWith url ["templates/postitem.html"]
|
||||
posts [("title", Left title)]
|
||||
renderChain ["posts.html", "templates/default.html"] list
|
||||
~~~~~
|
||||
|
||||
Our "render all posts" action can now be written as:
|
||||
|
||||
~~~~~{.haskell}
|
||||
renderPostList "posts.html" "All posts" renderablePosts
|
||||
~~~~~
|
||||
|
||||
## Tag links
|
||||
|
||||
We want to display the tags for our post under the title. But if we use the
|
||||
`$tags` key in a template, we will just have the plain tags - they will not be
|
||||
clickable. We can again solve this with a `ContextManipulation`. We have a
|
||||
function that produces an url for a given tag:
|
||||
|
||||
~~~~~{.haskell}
|
||||
tagToUrl tag = "$root/tags/" ++ removeSpaces tag ++ ".html"
|
||||
~~~~~
|
||||
|
||||
`removeSpaces` is an auxiliary function from `Text.Hakyll.File`. Now, there is
|
||||
a specialized `renderValue` function for creating linked tags called
|
||||
`renderTagLinks`. This function simply takes a function that produces an url
|
||||
for a given tag - the function we just wrote. Let's extend our
|
||||
`postManipulation`.
|
||||
|
||||
~~~~~{.haskell}
|
||||
postManipulation :: HakyllAction Context Context
|
||||
postManipulation = renderDate "date" "%B %e, %Y" "Unknown date"
|
||||
>>> renderTagLinks tagToUrl
|
||||
~~~~~
|
||||
|
||||
We apply this manipulation when we load the tags.
|
||||
|
||||
~~~~~{.haskell}
|
||||
let renderablePosts =
|
||||
map ((>>> postManipulation) . createPage) postPaths
|
||||
~~~~~
|
||||
|
||||
So, the `renderTagLinks` function replaces the `$tags` value from
|
||||
`epic fail, random` to `<a href="$root/tags/epic-fail.html">epic fail</a>, ...`.
|
||||
If we click a tag, we get a `404`. That's because we haven't generated the
|
||||
post lists for every tag.
|
||||
|
||||
## The Tag Map
|
||||
|
||||
Hakyll provides a function called `readTagMap`. Let's inspect it's type.
|
||||
|
||||
~~~~~{.haskell}
|
||||
type TagMap = Map String [HakyllAction () Context]
|
||||
readTagMap String [FilePath] -> HakyllAction () TagMap
|
||||
~~~~~
|
||||
|
||||
You give it a list of paths, and it creates a map that, for every tag, holds
|
||||
a number of posts. We can easily use this to render a post list for every tag.
|
||||
The first argument given is an "identifier", unique to this tag map. Hakyll
|
||||
needs this so it can cache the tags.
|
||||
|
||||
~~~~~{.haskell}
|
||||
let tagMap = readTagMap "postTags" postPaths
|
||||
~~~~~
|
||||
|
||||
When we have the `TagMap`, we can need to render a post list for every tag.
|
||||
There is a function in Hakyll designed more or less for this purpose:
|
||||
`withTagMap`. This takes a `TagMap` and an action to execute for every tag and
|
||||
it's associated posts. We pass a small function to it we create ourselves[^1]:
|
||||
|
||||
[^1]: Exercise for the reader: why do we use `>>> postManipulation` again here?
|
||||
|
||||
~~~~~{.haskell}
|
||||
let renderListForTag tag posts =
|
||||
renderPostList (tagToUrl tag)
|
||||
("Posts tagged " ++ tag)
|
||||
(map (>>> postManipulation) posts)
|
||||
withTagMap tagMap renderPostList
|
||||
~~~~~
|
||||
|
||||
There we go. We now have clickable tags, and a post list for every tag.
|
||||
|
||||
## A Tag Cloud
|
||||
|
||||
A tag cloud is a commonly found thing on blogs. Hakyll also provides code to
|
||||
generate a tag cloud. Let's have a look at the `renderTagCloud` function.
|
||||
|
||||
~~~~~{.haskell}
|
||||
renderTagCloud :: (String -> String)
|
||||
-> Float
|
||||
-> Float
|
||||
-> HakyllAction TagMap String
|
||||
~~~~~
|
||||
|
||||
The first argument is, once again, a function to create an url for a given tag.
|
||||
Then, we give a minimum and a maximum font size in percent, and we get a tag
|
||||
cloud Arrow back. We can add this to our index:
|
||||
|
||||
~~~~~{.haskell}
|
||||
let tagCloud = tagMap >>> renderTagCloud tagToUrl 100 200
|
||||
index = createListing "index.html"
|
||||
["templates/postitem.html"]
|
||||
(take 3 renderablePosts)
|
||||
[ ("title", Left "Home")
|
||||
, ("tagcloud", Right tagCloud)
|
||||
]
|
||||
renderChain ["index.html", "templates/default.html"] index
|
||||
~~~~~
|
||||
|
||||
## The gist of it
|
||||
|
||||
- There's some handy, simple functions in `Text.Hakyll.ContextManipulations`.
|
||||
- Seperate tags by commas and put them in the `$tags` field.
|
||||
- Use `withTagMap` to render a list for every tag.
|
||||
- Hakyll can also create tag clouds.
|
||||
|
|
|
@ -1,104 +1,85 @@
|
|||
---
|
||||
title: CategoryBlog
|
||||
what: explains how to use categories instead of tags
|
||||
title: Interlude
|
||||
what: gives some various tips and tricks about Hakyll (quite handy, read this!)
|
||||
---
|
||||
|
||||
## Categories
|
||||
## Syntax-highlighting
|
||||
|
||||
Most people familiar with "tags" will also know the concept "categories".
|
||||
Pandoc (which Hakyll uses as a backend) offers powerful syntax highlighting.
|
||||
To enable this, Pandoc needs to be compiled with highlighting support. If this
|
||||
is not the case, you can fix this using:
|
||||
|
||||
![Tags illustration]($root/images/tutorial8-tags.png)
|
||||
|
||||
In fact, tags are harder to implement because they have to be represented as a
|
||||
many-to-many relation, and categories are a simple 1-to-many relation.
|
||||
|
||||
![Tags illustration]($root/images/tutorial8-categories.png)
|
||||
|
||||
This is also the reason you can "simulate" categories using tags. In this
|
||||
tutorial we will adapt the blog to use categories instead of tags. Here is
|
||||
[a zip file]($root/examples/categoryblog.zip) containing the files used in this
|
||||
tutorial.
|
||||
|
||||
## About category support
|
||||
|
||||
Categories are simpler, but they are usually used in custom ways. That's why
|
||||
Hakyll provides less "standard" functions to deal with them. But this gives us
|
||||
another chance to learn some of the things we can do with Hakyll.
|
||||
|
||||
## Reading Categories
|
||||
|
||||
Tags are located in the `tags` metadata field. Since one post can only belong
|
||||
in one category, a different approach was chosen here. The category of a post
|
||||
is determined by the subfolder it is in. Here you see the directory layout for
|
||||
our posts using categories:
|
||||
|
||||
posts
|
||||
|-- coding
|
||||
| |-- 2009-11-05-a-first-post.markdown
|
||||
| |-- 2009-11-28-a-third-post.markdown
|
||||
| `-- 2009-12-04-this-blog-aint-dead.markdown
|
||||
`-- random
|
||||
|-- 2009-11-10-another-post.markdown
|
||||
`-- 2009-12-23-almost-christmas.markdown
|
||||
|
||||
Because we find all our posts in different subdirectories, sorting them is a
|
||||
little harder: we still want them sorted by date, so it boils down to sorting
|
||||
them by "base name". I hope it does not surprise you Hakyll provides a function
|
||||
for that:
|
||||
|
||||
~~~~~{.haskell}
|
||||
postPaths <- liftM (reverse . sortByBaseName)
|
||||
(getRecursiveContents "posts")
|
||||
~~~~~
|
||||
[jasper@alice ~]$ cabal install --reinstall -fhighlighting pandoc
|
||||
~~~~~
|
||||
|
||||
We reverse them again, because we want the most recent posts first. Now, we can
|
||||
use the `readCategoryMap` function instead of `readTagMap`, which has the same
|
||||
signature, but assigns categories based on the folders the posts are in.
|
||||
## Auto-compilation
|
||||
|
||||
~~~~~{.haskell}
|
||||
categoryMap <- readCategoryMap "categoryMap" renderablePosts
|
||||
Hakyll features a simple _auto-compilation_ mode. This is invoked by running
|
||||
|
||||
~~~~~
|
||||
[jasper@alice ~]$ ./hakyll preview
|
||||
Starting hakyll server on port 8000...
|
||||
~~~~~
|
||||
|
||||
The rest of the `hakyll.hs` is very similar to the one in the previous
|
||||
tutorial, except we want to render a category list instead of a tag cloud.
|
||||
Now, Hakyll will recompile your site when you change files, so you can just
|
||||
refresh in your browser. There is one more thing to note: this will not update
|
||||
your site automatically when `hakyll.hs` changes. So if you make any changes to
|
||||
the configuration file, you'll have to compile it again, and then you can enter
|
||||
`preview` mode again.
|
||||
|
||||
## Rendering a category list
|
||||
## When to rebuild
|
||||
|
||||
Because rendering a category list is quite easy, and it would be hard to
|
||||
write a "general" function for this, hakyll does not provide such a function --
|
||||
but it is not hard to write. First, we write an auxiliary function that produces
|
||||
a list item for one category:
|
||||
If you execute a `./hakyll build`, Hakyll will build your site incrementally.
|
||||
This means it will be very fast, but it will not pick up _all_ changes.
|
||||
|
||||
- In case you edited `hakyll.hs`, you first want to compile it again.
|
||||
- It is generally recommended to do a `./hakyll rebuild` before you deploy your
|
||||
site.
|
||||
|
||||
## Pretty URL's
|
||||
|
||||
There is an option in Hakyll to produce pretty URL's, which is disabled by
|
||||
default because it can be confusing when you're first introduced to Hakyll.
|
||||
|
||||
It can be enabled this way:
|
||||
|
||||
~~~~~{.haskell}
|
||||
categoryListItem category posts =
|
||||
"<li>" ++ link category (categoryToUrl category)
|
||||
++ " - " ++ show (length posts) ++ " items.</li>"
|
||||
import Text.Hakyll
|
||||
import Text.Hakyll.Hakyll
|
||||
|
||||
myConfig :: HakyllConfiguration
|
||||
myConfig = (defaultHakyllConfiguration "http://jaspervdj.be")
|
||||
{ enableIndexUrl = True
|
||||
}
|
||||
|
||||
main = hakyllWithConfiguration myConfig $ do
|
||||
-- Further code here
|
||||
~~~~~
|
||||
|
||||
This is nothing more that some basic string concatenation to create a `li` HTML
|
||||
element. The function that applies this on every element in the `TagMap` is more
|
||||
interesting:
|
||||
The effect will be that the internal `toUrl` function will behave differently.
|
||||
A few examples:
|
||||
|
||||
~~~~~{.haskell}
|
||||
categoryList :: HakyllAction TagMap String
|
||||
categoryList = arr $ uncurry categoryListItem <=< toList
|
||||
~~~~~
|
||||
- `about.html` will be rendered to `about/index.html`.
|
||||
- `posts/2010-02-16-a-post.markdown` will be rendered to
|
||||
`posts/2010-02-16-a-post/index.html`.
|
||||
- However, `index.markdown` will still be rendered to `index.html`. Likewise,
|
||||
`posts/index.html` would be rendered to `posts.index.html`.
|
||||
|
||||
This function might seem a little harder to understand if you are not familiar
|
||||
with the `<=<` operator -- but it's just right-to-left monad composition in the
|
||||
list monad. `uncurry categoryListItem <=< toList` is a pure function we want to
|
||||
execute on the `TagMap`. But this is not possible in Hakyll[^1]. We need to make
|
||||
an arrow of this function. The `arr` function solves this problem easily.
|
||||
The benefit of this is simply prettier URL's. That is, if you consider
|
||||
`example.com/about` prettier than `example.com/about.html`.
|
||||
|
||||
[^1]: This is a feature, not a bug. It helps dependency handling.
|
||||
## Default values
|
||||
|
||||
We then add this to our index page, and we are done. Feel free to hack around
|
||||
with the source code. If you still have questions, feel free to ask them at the
|
||||
[google discussion group](http://groups.google.com/group/hakyll).
|
||||
At some point, you might want to use a number of global key-value pairs, for
|
||||
example, `$author`. There are two possible ways to achieve this.
|
||||
|
||||
## The gist of it
|
||||
- There is an option in `HakyllConfiguration` supporting this, called
|
||||
`additionalContext`. For an example on how to use `HakyllConfiguration`, see
|
||||
the pretty URL's section above.
|
||||
|
||||
- Hakyll supports categories as well as tags.
|
||||
- Tags are actually a generalization of categories.
|
||||
- Use `readCategoryMap` to read categories.
|
||||
- You need to write some custom functions to render category lists etc.
|
||||
- Another option is to use a `defaults.markdown` file, simply containing some
|
||||
metadata, and then `combine` this file with other pages. The advantage is
|
||||
that autocompilation mode will pick up changes in this file[^1].
|
||||
|
||||
[^1]: Original idea by zenzike.
|
||||
|
|
104
examples/hakyll/tutorials/part09.markdown
Normal file
104
examples/hakyll/tutorials/part09.markdown
Normal file
|
@ -0,0 +1,104 @@
|
|||
---
|
||||
title: CategoryBlog
|
||||
what: explains how to use categories instead of tags
|
||||
---
|
||||
|
||||
## Categories
|
||||
|
||||
Most people familiar with "tags" will also know the concept "categories".
|
||||
|
||||
![Tags illustration]($root/images/tutorial8-tags.png)
|
||||
|
||||
In fact, tags are harder to implement because they have to be represented as a
|
||||
many-to-many relation, and categories are a simple 1-to-many relation.
|
||||
|
||||
![Tags illustration]($root/images/tutorial8-categories.png)
|
||||
|
||||
This is also the reason you can "simulate" categories using tags. In this
|
||||
tutorial we will adapt the blog to use categories instead of tags. Here is
|
||||
[a zip file]($root/examples/categoryblog.zip) containing the files used in this
|
||||
tutorial.
|
||||
|
||||
## About category support
|
||||
|
||||
Categories are simpler, but they are usually used in custom ways. That's why
|
||||
Hakyll provides less "standard" functions to deal with them. But this gives us
|
||||
another chance to learn some of the things we can do with Hakyll.
|
||||
|
||||
## Reading Categories
|
||||
|
||||
Tags are located in the `tags` metadata field. Since one post can only belong
|
||||
in one category, a different approach was chosen here. The category of a post
|
||||
is determined by the subfolder it is in. Here you see the directory layout for
|
||||
our posts using categories:
|
||||
|
||||
posts
|
||||
|-- coding
|
||||
| |-- 2009-11-05-a-first-post.markdown
|
||||
| |-- 2009-11-28-a-third-post.markdown
|
||||
| `-- 2009-12-04-this-blog-aint-dead.markdown
|
||||
`-- random
|
||||
|-- 2009-11-10-another-post.markdown
|
||||
`-- 2009-12-23-almost-christmas.markdown
|
||||
|
||||
Because we find all our posts in different subdirectories, sorting them is a
|
||||
little harder: we still want them sorted by date, so it boils down to sorting
|
||||
them by "base name". I hope it does not surprise you Hakyll provides a function
|
||||
for that:
|
||||
|
||||
~~~~~{.haskell}
|
||||
postPaths <- liftM (reverse . sortByBaseName)
|
||||
(getRecursiveContents "posts")
|
||||
~~~~~
|
||||
|
||||
We reverse them again, because we want the most recent posts first. Now, we can
|
||||
use the `readCategoryMap` function instead of `readTagMap`, which has the same
|
||||
signature, but assigns categories based on the folders the posts are in.
|
||||
|
||||
~~~~~{.haskell}
|
||||
categoryMap <- readCategoryMap "categoryMap" renderablePosts
|
||||
~~~~~
|
||||
|
||||
The rest of the `hakyll.hs` is very similar to the one in the previous
|
||||
tutorial, except we want to render a category list instead of a tag cloud.
|
||||
|
||||
## Rendering a category list
|
||||
|
||||
Because rendering a category list is quite easy, and it would be hard to
|
||||
write a "general" function for this, hakyll does not provide such a function --
|
||||
but it is not hard to write. First, we write an auxiliary function that produces
|
||||
a list item for one category:
|
||||
|
||||
~~~~~{.haskell}
|
||||
categoryListItem category posts =
|
||||
"<li>" ++ link category (categoryToUrl category)
|
||||
++ " - " ++ show (length posts) ++ " items.</li>"
|
||||
~~~~~
|
||||
|
||||
This is nothing more that some basic string concatenation to create a `li` HTML
|
||||
element. The function that applies this on every element in the `TagMap` is more
|
||||
interesting:
|
||||
|
||||
~~~~~{.haskell}
|
||||
categoryList :: HakyllAction TagMap String
|
||||
categoryList = arr $ uncurry categoryListItem <=< toList
|
||||
~~~~~
|
||||
|
||||
This function might seem a little harder to understand if you are not familiar
|
||||
with the `<=<` operator -- but it's just right-to-left monad composition in the
|
||||
list monad. `uncurry categoryListItem <=< toList` is a pure function we want to
|
||||
execute on the `TagMap`. But this is not possible in Hakyll[^1]. We need to make
|
||||
an arrow of this function. The `arr` function solves this problem easily.
|
||||
|
||||
[^1]: This is a feature, not a bug. It helps dependency handling.
|
||||
|
||||
We then add this to our index page, and we are done. Feel free to hack around
|
||||
with the source code. If you still have questions, feel free to ask them at the
|
||||
[google discussion group](http://groups.google.com/group/hakyll).
|
||||
|
||||
## The gist of it
|
||||
|
||||
- Hakyll supports categories as well as tags.
|
||||
- Tags are actually a generalization of categories.
|
||||
- Use `readCategoryMap` to read categories.
|
||||
- You need to write some custom functions to render category lists etc.
|
Loading…
Reference in a new issue