Wrote another tutorial, and updated some tutorials.
This commit is contained in:
parent
db0eef8470
commit
0ba0eac8d9
12 changed files with 711 additions and 400 deletions
|
@ -66,12 +66,12 @@ metadata. The metadata is placed in the file header and surrouded by `---`
|
||||||
lines. Each line should contain a `key: value` pair. Let's have a look at the
|
lines. Each line should contain a `key: value` pair. Let's have a look at the
|
||||||
`about.markdown` page.
|
`about.markdown` page.
|
||||||
|
|
||||||
> ---
|
---
|
||||||
> title: About
|
title: About
|
||||||
> ---
|
---
|
||||||
> Nullam imperdiet sodales orci vitae molestie.
|
Nullam imperdiet sodales orci vitae molestie.
|
||||||
> Nunc quam orci, pharetra a rhoncus vitae,
|
Nunc quam orci, pharetra a rhoncus vitae,
|
||||||
> eleifend id felis. Suspendisse potenti...
|
eleifend id felis. Suspendisse potenti...
|
||||||
|
|
||||||
This contains one `key: value` pair, namely `title: About`. The rest of the
|
This contains one `key: value` pair, namely `title: About`. The rest of the
|
||||||
file is treated as markdown by pandoc. If you want to know more about
|
file is treated as markdown by pandoc. If you want to know more about
|
||||||
|
@ -209,4 +209,4 @@ questions, feel free to ask them on the
|
||||||
[google discussion group](http://groups.google.com/group/hakyll).
|
[google discussion group](http://groups.google.com/group/hakyll).
|
||||||
|
|
||||||
If you feel comfortable with the basics, here is a next tutorial:
|
If you feel comfortable with the basics, here is a next tutorial:
|
||||||
[building a simple blog](tutorial2.html).
|
[more about writing pages](tutorial2.html).
|
||||||
|
|
|
@ -1,162 +1,159 @@
|
||||||
---
|
---
|
||||||
title: Tutorial (Part II)
|
title: Tutorial (Part II)
|
||||||
what: creates a simple blog
|
what: elaborates a little on writing pages and templates
|
||||||
---
|
---
|
||||||
|
|
||||||
## Creating a simple blog with Hakyll
|
## The structure of a Page
|
||||||
|
|
||||||
After we created a simple brochure site, we're going to try something more
|
The most important thing to realize is that a `Page` is just a key-value
|
||||||
advanced: we are going to create a simple blog system.
|
mapping. Another example:
|
||||||
|
|
||||||
A [zip file containing the source](examples/simpleblog.zip) for this
|
---
|
||||||
tutorial is also available.
|
title: About
|
||||||
|
author: Mia Wallace
|
||||||
|
---
|
||||||
|
Hello there! This is
|
||||||
|
a simple about page.
|
||||||
|
|
||||||
Blogs, as you probably know, are composed of posts. In Hakyll, we're going
|
This will produce the following mapping:
|
||||||
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
|
- `title`: About
|
||||||
see why later. First, some trivial things like css.
|
- `author`: Mia Wallace
|
||||||
|
- `body`: Hello there! This is a simple about page.
|
||||||
|
|
||||||
|
`body` is the traditional name for the main body part of a page. If the page has
|
||||||
|
a `.markdown` extension for example, this would also be rendered by pandoc. But
|
||||||
|
pages are more flexible. The following is also a valid page:
|
||||||
|
|
||||||
|
Hello there! This is
|
||||||
|
a simple about page.
|
||||||
|
|
||||||
|
This will produce one key-value pair:
|
||||||
|
|
||||||
|
- `body`: Hello there! This is a simple about page.
|
||||||
|
|
||||||
|
But the `Page` parser can do more than this. You can add extra sections, apart
|
||||||
|
from the body, and even leave out the body.
|
||||||
|
|
||||||
|
---
|
||||||
|
author: Vincent Vega
|
||||||
|
|
||||||
|
--- prelude
|
||||||
|
A small introduction goes here. I can write *markdown* here, by the way. Well,
|
||||||
|
assuming this page has a `.markdown` extension.
|
||||||
|
|
||||||
|
--- main
|
||||||
|
I can write some more things here.
|
||||||
|
|
||||||
|
This will produce the following:
|
||||||
|
|
||||||
|
- `author`: Vincent Vega
|
||||||
|
- `prelude`: A small introduction goes here. I can write *markdown* here, by the
|
||||||
|
way. Well, assuming this page has a `.markdown` extension.
|
||||||
|
- `main`: I can write some more things here.
|
||||||
|
|
||||||
|
The example from this tutorial (we will see later) uses this to build a
|
||||||
|
three-column system for the website, separating content from layout.
|
||||||
|
|
||||||
|
## Combining pages
|
||||||
|
|
||||||
|
Now you know that `Page`s, and `Renderable`s in general, are basically nothing
|
||||||
|
more than key-values mappings, it is time to abuse this fact. There is another
|
||||||
|
`Renderable` type we haven't talked about before: a `CombinedRenderable`.
|
||||||
|
|
||||||
|
The type signature of the `combine` function does a pretty good job at
|
||||||
|
explaining it:
|
||||||
|
|
||||||
~~~~~{.haskell}
|
~~~~~{.haskell}
|
||||||
main = hakyll $ do
|
combine :: (Renderable a, Renderable b)
|
||||||
directory css "css"
|
=> a -> b -> CombinedRenderable a b
|
||||||
~~~~~
|
~~~~~
|
||||||
|
|
||||||
## Finding the posts, and a bit about renderables
|
This means we can take two `Renderable` values and combine them. This is
|
||||||
|
basically a `Map.union`: The result will contain all keys from `a`, and all
|
||||||
|
keys from `b`. If a key is present in both `Renderable`s, the value from `a`
|
||||||
|
will be chosen. This is, for example, always the case with an `url` (since
|
||||||
|
all `Renderable` types always have an url).
|
||||||
|
|
||||||
`Text.Hakyll.File` contains a handy function `getRecursiveContents`, which will
|
Combining two `Renderable`s, but setting a different `url` is quite common, so
|
||||||
provide us with all the blog posts. The blog posts have a
|
there is another function that helps us here:
|
||||||
`yyyy-mm-dd-title.extension` naming scheme. This is just a simple trick so we
|
|
||||||
can sort them easily, you could of course name them whatever you want. 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:
|
|
||||||
|
|
||||||
~~~~~{.haskell}
|
~~~~~{.haskell}
|
||||||
-- Find all post paths.
|
combineWithURL :: (Renderable a, Renderable b)
|
||||||
postPaths <- liftM (reverse . sort) $ getRecursiveContents "posts"
|
=> FilePath -> a -> b -> CombinedRenderable a b
|
||||||
~~~~~
|
~~~~~
|
||||||
|
|
||||||
Our `postPaths` value is now of the type `[FilePath]`. `FilePath` is no
|
## The example
|
||||||
instance of `Renderable`, but `PagePath` is:
|
|
||||||
|
|
||||||
~~~~~{.haskell}
|
Now that we have the tools, we'll get on to the example. This time, we'll
|
||||||
let renderablePosts = map createPagePath postPaths
|
be making a more advanced brochure site. Here
|
||||||
~~~~~
|
[is a zip file](examples/morepages.zip) containing the source code for the
|
||||||
|
tutorial.
|
||||||
|
|
||||||
We have two templates we want to render our posts with: first we would like to
|
Every page consists of three sections, originally named `section1`, `section2`
|
||||||
render them using `templates/post.html`, and we want to render the result
|
and `section3`. So our pages look more or less like this:
|
||||||
using `templates/default.html`. This can be done with the `renderChain`
|
|
||||||
function:
|
|
||||||
|
|
||||||
~~~~~{.haskell}
|
---
|
||||||
mapM_ (renderChain [ "templates/post.html"
|
title: About
|
||||||
, "templates/default.html"
|
|
||||||
]) renderablePosts
|
|
||||||
~~~~~
|
|
||||||
|
|
||||||
Remember that the `renderChain` works by rendering the datatype using the first
|
--- section1
|
||||||
template, creating a new page with the render result in the `body` field, and so
|
## Mattis
|
||||||
on until it has been rendered with all templates.
|
Nullam imperdiet sodales orci vitae molestie. Nunc...
|
||||||
|
|
||||||
Now, we have the posts rendered. What is left is to generate some kind of index
|
--- section2
|
||||||
page with links to those posts. We want one general list showing all posts, and
|
## Orci
|
||||||
we want to show a few recent posts on the index page.
|
Vivamus eget mauris sit amet nulla laoreet lobortis. Nulla in...
|
||||||
|
|
||||||
## Custom Pages
|
--- section3
|
||||||
|
## Augue
|
||||||
|
In urna ante, pulvinar et imperdiet nec, fermentum ac...
|
||||||
|
|
||||||
Currently, there are 3 renderable datatypes in Hakyll:
|
The cool thing is we do not have to specify how these will be layed out. In our
|
||||||
|
template, we decide to use a simple three column system:
|
||||||
- `Page`: The result of any rendering action. It is generally not recommended
|
|
||||||
to use pages a lot, because they cannot check dependencies (and therefore,
|
|
||||||
you would always regenerate your entire site if you use pages the wrong way).
|
|
||||||
- `PagePath`: Basically just a `FilePath` in a box. Internally, this will use
|
|
||||||
a `Page` for rendering, but `PagePath` provides better dependency checking
|
|
||||||
and works on a higher level.
|
|
||||||
- `CustomPage`: Basically the name says it - the preferred way of creating
|
|
||||||
custom pages in Hakyll.
|
|
||||||
|
|
||||||
We will use a `CustomPage` here. Basically, all `Renderable` datatypes are in
|
|
||||||
the end just `key: value` mappings. A CustomPage is created using the
|
|
||||||
`createCustomPage` function, which has the following type signature:
|
|
||||||
|
|
||||||
~~~~~{.haskell}
|
|
||||||
createCustomPage :: FilePath
|
|
||||||
-> [FilePath]
|
|
||||||
-> [(String, Either String (IO String)]
|
|
||||||
~~~~~
|
|
||||||
|
|
||||||
The first argument is the `url` of the page to generate. For our index page,
|
|
||||||
this will be, `index.html`. The second argument is _a list of dependencies_.
|
|
||||||
Basically, you should here give a list of files on which your custom page
|
|
||||||
depends.
|
|
||||||
|
|
||||||
The last 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`.
|
|
||||||
- `IO String`: Here, you can give an arbitrary `IO` action that will result
|
|
||||||
in a String. However - this action _will not be executed_ when the file
|
|
||||||
in `_site` is up-to-date.
|
|
||||||
|
|
||||||
First, let us define this `IO String` for our index page. We want to render
|
|
||||||
every post using a simple template:
|
|
||||||
|
|
||||||
~~~~~{.html}
|
~~~~~{.html}
|
||||||
<li>
|
<div class="column"> $section1 </div>
|
||||||
<a href="$$root/$url">$title</a>
|
<div class="column"> $section2 </div>
|
||||||
- <em>$date</em> - by <em>$author</em>
|
<div class="column"> $section3 </div>
|
||||||
</li>
|
|
||||||
~~~~~
|
~~~~~
|
||||||
|
|
||||||
When every post is rendered with this template, we then want to concatenate the
|
The columns are then floated using css. So far so good, but what if we wanted
|
||||||
result. Since rendering and concatenating is pretty common, Hakyll provides us
|
an additional text block on every page? An easy solution would be to add this
|
||||||
with a high-level function to do this.
|
to the template, but then our layout-content separation idea will be broken
|
||||||
|
again. So we simply add to the template:
|
||||||
|
|
||||||
|
~~~~~{.html}
|
||||||
|
<div class="footer"> $footer </div>
|
||||||
|
~~~~~
|
||||||
|
|
||||||
|
And now we will use `combine` to put the footer on every page. We write a small
|
||||||
|
auxiliary function that combines a given `Renderable` with the footer:
|
||||||
|
|
||||||
~~~~~{.haskell}
|
~~~~~{.haskell}
|
||||||
let recentPosts = renderAndConcat "templates/postitem.html"
|
withFooter a = a `combine` createPagePath "footer.markdown"
|
||||||
(take 3 renderablePosts)
|
|
||||||
~~~~~
|
~~~~~
|
||||||
|
|
||||||
Now, creating our custom page is fairly straight-forward:
|
Now, were we previously wrote:
|
||||||
|
|
||||||
~~~~~{.haskell}
|
~~~~~{.haskell}
|
||||||
createCustomPage "index.html"
|
render "about.markdown"
|
||||||
("templates/postitem.html" : take 3 postPaths)
|
where render = renderChain ["templates/default.html"]
|
||||||
[ ("title", Left "All posts")
|
. createPagePath
|
||||||
, ("posts", Right recentPosts)
|
|
||||||
]
|
|
||||||
~~~~~
|
~~~~~
|
||||||
|
|
||||||
You can see our three arguments here. We're rendering `index.html`, then we tell
|
We simply have to add our footer:
|
||||||
Hakyll on what files it depends - here the `templates/postitem.html` template
|
|
||||||
and the latest 3 posts. Finally, we give a `title` value to substitute in the
|
|
||||||
template, and the result of our concatenation. Of course, we also need to render
|
|
||||||
this custom page:
|
|
||||||
|
|
||||||
~~~~~{.haskell}
|
~~~~~{.haskell}
|
||||||
renderChain ["index.html", "templates/default.html"] $
|
render "about.markdown"
|
||||||
createCustomPage "index.html"
|
where render = renderChain ["templates/default.html"]
|
||||||
("templates/postitem.html" : take 3 postPaths)
|
. withFooter
|
||||||
[ ("title", Left "All posts")
|
. createPagePath
|
||||||
, ("posts", Right recentPosts)
|
|
||||||
]
|
|
||||||
~~~~~
|
~~~~~
|
||||||
|
|
||||||
Note that the `index.html` in the `renderChain` list is also a template.
|
And now every page will include the footer.
|
||||||
|
|
||||||
## That's that
|
## That's all folks
|
||||||
|
|
||||||
If you have any more questions, feel free to ask them on the
|
I hope this tutorial was clear enough to teach you the concepts of pages and
|
||||||
|
combining renderables. As always, questions and feedback are welcome at the
|
||||||
[google discussion group](http://groups.google.com/group/hakyll).
|
[google discussion group](http://groups.google.com/group/hakyll).
|
||||||
|
|
||||||
There is a [next tutorial](tutorial3.html), explaining how to add an RSS feed
|
|
||||||
to our sample blog.
|
|
||||||
|
|
|
@ -1,124 +1,162 @@
|
||||||
---
|
---
|
||||||
title: Tutorial (Part III)
|
title: Tutorial (Part III)
|
||||||
what: adds an RSS feed to the blog from the previous tutorial
|
what: creates a simple blog
|
||||||
---
|
---
|
||||||
|
|
||||||
## Adding RSS to our simple blog
|
## Creating a simple blog with Hakyll
|
||||||
|
|
||||||
In this tutorial, we're going to add an RSS feed to the blog we wrote in
|
After we created a simple brochure site, we're going to try something more
|
||||||
[the previous tutorial](tutorial2.html). Here is a
|
advanced: we are going to create a simple blog system.
|
||||||
[zip file containing the source](examples/rssblog.zip).
|
|
||||||
|
|
||||||
An RSS feed looks like this:
|
A [zip file containing the source](examples/simpleblog.zip) for this
|
||||||
|
tutorial is also available.
|
||||||
|
|
||||||
~~~~~{.xml}
|
Blogs, as you probably know, are composed of posts. In Hakyll, we're going
|
||||||
<?xml version="1.0" ?>
|
to use simple pages for posts. All posts are located in the `posts`
|
||||||
<rss version="2.0">
|
directory. But we're not going to use the `directory` command here - you will
|
||||||
<channel>
|
see why later. First, some trivial things like css.
|
||||||
<title>The SimpleBlog</title>
|
|
||||||
<link>http://example.com/</link>
|
|
||||||
<description>Simple blog in hakyll</description>
|
|
||||||
<item>
|
|
||||||
<title>Title goes here</title>
|
|
||||||
<link>http://example.com/post.html</link>
|
|
||||||
<description>
|
|
||||||
A description is optional.
|
|
||||||
</description>
|
|
||||||
</item>
|
|
||||||
</channel>
|
|
||||||
</rss>
|
|
||||||
~~~~~
|
|
||||||
|
|
||||||
Note that, obviously, there can be more than one item. We're going to use a
|
|
||||||
template to render this. This is where `templates/rss.xml` comes in:
|
|
||||||
|
|
||||||
~~~~~{.xml}
|
|
||||||
<?xml version="1.0" ?>
|
|
||||||
<rss version="2.0">
|
|
||||||
<channel>
|
|
||||||
<title>The SimpleBlog</title>
|
|
||||||
<link>http://jaspervdj.be/</link>
|
|
||||||
<description>Simple blog in hakyll</description>
|
|
||||||
$items
|
|
||||||
</channel>
|
|
||||||
</rss>
|
|
||||||
~~~~~
|
|
||||||
|
|
||||||
We thus render our feed with the following code (no, I didn't define `rssPage`
|
|
||||||
yet - I'm going to work from bottom to top here, it's easier to explain it
|
|
||||||
that way).
|
|
||||||
|
|
||||||
~~~~~{.haskell}
|
~~~~~{.haskell}
|
||||||
renderChain ["templates/rss.xml"] rssPage
|
main = hakyll $ do
|
||||||
|
directory css "css"
|
||||||
~~~~~
|
~~~~~
|
||||||
|
|
||||||
This, as you can see, is a regular render chain, once again. We need make a
|
## Finding the posts, and a bit about renderables
|
||||||
`Renderable` that "fills in" the `$items` identifier. We're going to do this
|
|
||||||
using a [custom page](tutorial2.html#custom-pages).
|
|
||||||
|
|
||||||
## Custom pages again
|
`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, you could of course name them whatever you want. They
|
||||||
|
contain some metadata, too:
|
||||||
|
|
||||||
Note that we do not have to include all posts in the rss feed - only a few
|
title: A first post
|
||||||
recent ones. We'll settle on the latest three here.
|
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...
|
||||||
|
|
||||||
We want to render every post using the following template,
|
Now, we find the posts and sort them reversed:
|
||||||
`templates/rssitem.xml`:
|
|
||||||
|
|
||||||
~~~~~{.haskell}
|
~~~~~{.haskell}
|
||||||
<item>
|
-- Find all post paths.
|
||||||
<title>$title</title>
|
postPaths <- liftM (reverse . sort) $ getRecursiveContents "posts"
|
||||||
<link>http://example.com/$url</link>
|
|
||||||
<description>$title by $author</description>
|
|
||||||
</item>
|
|
||||||
~~~~~
|
~~~~~
|
||||||
|
|
||||||
Since we build on the previous example, we still have our `renderablePosts`
|
Our `postPaths` value is now of the type `[FilePath]`. `FilePath` is no
|
||||||
list. We'll be using it again:
|
instance of `Renderable`, but `PagePath` is:
|
||||||
|
|
||||||
~~~~~{.haskell}
|
~~~~~{.haskell}
|
||||||
let recentRSSItems = renderAndConcat "templates/rssitem.xml"
|
let renderablePosts = map createPagePath postPaths
|
||||||
(take 3 renderablePosts)
|
|
||||||
~~~~~
|
~~~~~
|
||||||
|
|
||||||
We're using the `renderAndConcat` function again. Note that because of
|
We have two templates we want to render our posts with: first we would like to
|
||||||
hakyll/haskell laziness, this action isn't executed directly, and this helps
|
render them using `templates/post.html`, and we want to render the result
|
||||||
dependency handling.
|
using `templates/default.html`. This can be done with the `renderChain`
|
||||||
|
function:
|
||||||
Now, the `rssPage` page. As you might remember, we use the `createCustomPage`
|
|
||||||
function to create a custom page. We first give the destination url, then a
|
|
||||||
list of dependencies, and then a list of `(key, value)` pairs.
|
|
||||||
|
|
||||||
~~~~~{.haskell}
|
~~~~~{.haskell}
|
||||||
let rssPage = createCustomPage
|
mapM_ (renderChain [ "templates/post.html"
|
||||||
"rss.xml"
|
, "templates/default.html"
|
||||||
("templates/postitem.html" : take 3 postPaths)
|
]) renderablePosts
|
||||||
[("items", Right recentRSSItems)]
|
|
||||||
~~~~~
|
~~~~~
|
||||||
|
|
||||||
## Adding a link to the feed
|
Remember that the `renderChain` works by rendering the datatype 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.
|
||||||
|
|
||||||
According to the w3 organization,
|
Now, we have the posts rendered. What is left is to generate some kind of index
|
||||||
[you should add \<link\> tags](http://www.w3.org/QA/Tips/use-links) to your
|
page with links to those posts. We want one general list showing all posts, and
|
||||||
documents, so we'll do this. In `templates/default.html`:
|
we want to show a few recent posts on the index page.
|
||||||
|
|
||||||
~~~~~~{.html}
|
## Custom Pages
|
||||||
<head>
|
|
||||||
<title>SimpleBlog - $title</title>
|
|
||||||
<link rel="stylesheet" type="text/css" href="$$root/css/default.css" />
|
|
||||||
<link rel="alternate"
|
|
||||||
type="application/rss+xml"
|
|
||||||
title="SimpleBlog"
|
|
||||||
href="http://example.com/rss.xml" />
|
|
||||||
</head>
|
|
||||||
~~~~~~
|
|
||||||
|
|
||||||
This makes most browsers see the rss feed, and show it in the address bar.
|
Currently, there are 3 renderable datatypes in Hakyll:
|
||||||
If you want, you can also add a pretty button to your blog linking to
|
|
||||||
`rss.xml`.
|
|
||||||
|
|
||||||
## That's it!
|
- `Page`: The result of any rendering action. It is generally not recommended
|
||||||
|
to use pages a lot, because they cannot check dependencies (and therefore,
|
||||||
|
you would always regenerate your entire site if you use pages the wrong way).
|
||||||
|
- `PagePath`: Basically just a `FilePath` in a box. Internally, this will use
|
||||||
|
a `Page` for rendering, but `PagePath` provides better dependency checking
|
||||||
|
and works on a higher level.
|
||||||
|
- `CustomPage`: Basically the name says it - the preferred way of creating
|
||||||
|
custom pages in Hakyll.
|
||||||
|
|
||||||
Yep, that's it. Feel free to play with the source code in
|
We will use a `CustomPage` here. Basically, all `Renderable` datatypes are in
|
||||||
[the zip file](examples/rssblog.zip) and extend the blog further. As always,
|
the end just `key: value` mappings. A CustomPage is created using the
|
||||||
all questions are welcome on the
|
`createCustomPage` function, which has the following type signature:
|
||||||
|
|
||||||
|
~~~~~{.haskell}
|
||||||
|
createCustomPage :: FilePath
|
||||||
|
-> [FilePath]
|
||||||
|
-> [(String, Either String (IO String)]
|
||||||
|
~~~~~
|
||||||
|
|
||||||
|
The first argument is the `url` of the page to generate. For our index page,
|
||||||
|
this will be, `index.html`. The second argument is _a list of dependencies_.
|
||||||
|
Basically, you should here give a list of files on which your custom page
|
||||||
|
depends.
|
||||||
|
|
||||||
|
The last 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`.
|
||||||
|
- `IO String`: Here, you can give an arbitrary `IO` action that will result
|
||||||
|
in a String. However - this action _will not be executed_ when the file
|
||||||
|
in `_site` is up-to-date.
|
||||||
|
|
||||||
|
First, let us define this `IO String` for our index page. We want to render
|
||||||
|
every post using a simple template:
|
||||||
|
|
||||||
|
~~~~~{.html}
|
||||||
|
<li>
|
||||||
|
<a href="$$root/$url">$title</a>
|
||||||
|
- <em>$date</em> - by <em>$author</em>
|
||||||
|
</li>
|
||||||
|
~~~~~
|
||||||
|
|
||||||
|
When every post is rendered with this template, we then want to concatenate the
|
||||||
|
result. Since rendering and concatenating is pretty common, Hakyll provides us
|
||||||
|
with a high-level function to do this.
|
||||||
|
|
||||||
|
~~~~~{.haskell}
|
||||||
|
let recentPosts = renderAndConcat ["templates/postitem.html"]
|
||||||
|
(take 3 renderablePosts)
|
||||||
|
~~~~~
|
||||||
|
|
||||||
|
Now, creating our custom page is fairly straight-forward:
|
||||||
|
|
||||||
|
~~~~~{.haskell}
|
||||||
|
createCustomPage "index.html"
|
||||||
|
("templates/postitem.html" : take 3 postPaths)
|
||||||
|
[ ("title", Left "All posts")
|
||||||
|
, ("posts", Right recentPosts)
|
||||||
|
]
|
||||||
|
~~~~~
|
||||||
|
|
||||||
|
You can see our three arguments here. We're rendering `index.html`, then we tell
|
||||||
|
Hakyll on what files it depends - here the `templates/postitem.html` template
|
||||||
|
and the latest 3 posts. Finally, we give a `title` value to substitute in the
|
||||||
|
template, and the result of our concatenation. Of course, we also need to render
|
||||||
|
this custom page:
|
||||||
|
|
||||||
|
~~~~~{.haskell}
|
||||||
|
renderChain ["index.html", "templates/default.html"] $
|
||||||
|
createCustomPage "index.html"
|
||||||
|
("templates/postitem.html" : take 3 postPaths)
|
||||||
|
[ ("title", Left "All posts")
|
||||||
|
, ("posts", Right recentPosts)
|
||||||
|
]
|
||||||
|
~~~~~
|
||||||
|
|
||||||
|
Note that the `index.html` in the `renderChain` list is also a template.
|
||||||
|
|
||||||
|
## That's that
|
||||||
|
|
||||||
|
If you have any more questions, feel free to ask them on the
|
||||||
[google discussion group](http://groups.google.com/group/hakyll).
|
[google discussion group](http://groups.google.com/group/hakyll).
|
||||||
|
|
||||||
|
There is a [next tutorial](tutorial4.html), explaining how to add an RSS feed
|
||||||
|
to our sample blog.
|
||||||
|
|
|
@ -1,211 +1,124 @@
|
||||||
---
|
---
|
||||||
title: Tutorial (Part IV)
|
title: Tutorial (Part IV)
|
||||||
what: adds tags and context manipulations to our blog
|
what: adds an RSS feed to the blog from the previous tutorial
|
||||||
---
|
---
|
||||||
|
|
||||||
Here, have [a zip file](examples/tagblog.zip) for this tutorial.
|
## Adding RSS to our simple blog
|
||||||
|
|
||||||
## Context manipulations
|
In this tutorial, we're going to add an RSS feed to the blog we wrote in
|
||||||
|
[the previous tutorial](tutorial2.html). Here is a
|
||||||
|
[zip file containing the source](examples/rssblog.zip).
|
||||||
|
|
||||||
As you might remember, `Renderable` objects are usually just key-value mappings.
|
An RSS feed looks like this:
|
||||||
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_.
|
|
||||||
|
|
||||||
In `Text.Hakyll.Context`, we see the type of `ContextManipulation`:
|
~~~~~{.xml}
|
||||||
|
<?xml version="1.0" ?>
|
||||||
|
<rss version="2.0">
|
||||||
|
<channel>
|
||||||
|
<title>The SimpleBlog</title>
|
||||||
|
<link>http://example.com/</link>
|
||||||
|
<description>Simple blog in hakyll</description>
|
||||||
|
<item>
|
||||||
|
<title>Title goes here</title>
|
||||||
|
<link>http://example.com/post.html</link>
|
||||||
|
<description>
|
||||||
|
A description is optional.
|
||||||
|
</description>
|
||||||
|
</item>
|
||||||
|
</channel>
|
||||||
|
</rss>
|
||||||
|
~~~~~
|
||||||
|
|
||||||
|
Note that, obviously, there can be more than one item. We're going to use a
|
||||||
|
template to render this. This is where `templates/rss.xml` comes in:
|
||||||
|
|
||||||
|
~~~~~{.xml}
|
||||||
|
<?xml version="1.0" ?>
|
||||||
|
<rss version="2.0">
|
||||||
|
<channel>
|
||||||
|
<title>The SimpleBlog</title>
|
||||||
|
<link>http://jaspervdj.be/</link>
|
||||||
|
<description>Simple blog in hakyll</description>
|
||||||
|
$items
|
||||||
|
</channel>
|
||||||
|
</rss>
|
||||||
|
~~~~~
|
||||||
|
|
||||||
|
We thus render our feed with the following code (no, I didn't define `rssPage`
|
||||||
|
yet - I'm going to work from bottom to top here, it's easier to explain it
|
||||||
|
that way).
|
||||||
|
|
||||||
~~~~~{.haskell}
|
~~~~~{.haskell}
|
||||||
type ContextManipulation = Context -> Context
|
renderChain ["templates/rss.xml"] rssPage
|
||||||
~~~~~
|
~~~~~
|
||||||
|
|
||||||
Where `Context` is a key-value mapping. Usually, you don't create
|
This, as you can see, is a regular render chain, once again. We need make a
|
||||||
`ContextManipulation`'s by hand, but you use the `renderValue` function. Let's
|
`Renderable` that "fills in" the `$items` identifier. We're going to do this
|
||||||
have a look at it's type.
|
using a [custom page](tutorial2.html#custom-pages).
|
||||||
|
|
||||||
|
## Custom pages again
|
||||||
|
|
||||||
|
Note that we do not have to include all posts in the rss feed - only a few
|
||||||
|
recent ones. We'll settle on the latest three here.
|
||||||
|
|
||||||
|
We want to render every post using the following template,
|
||||||
|
`templates/rssitem.xml`:
|
||||||
|
|
||||||
~~~~~{.haskell}
|
~~~~~{.haskell}
|
||||||
renderValue :: String -> String
|
<item>
|
||||||
-> (String -> String)
|
<title>$title</title>
|
||||||
-> ContextManipulation
|
<link>http://example.com/$url</link>
|
||||||
|
<description>$title by $author</description>
|
||||||
|
</item>
|
||||||
~~~~~
|
~~~~~
|
||||||
|
|
||||||
This is the preferred way of creating context manipulations. The first argument
|
Since we build on the previous example, we still have our `renderablePosts`
|
||||||
is the `key` to manipulate. The second argument is the `key` where the new value
|
list. We'll be using it again:
|
||||||
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.
|
|
||||||
|
|
||||||
~~~~~{.haskell}
|
~~~~~{.haskell}
|
||||||
import Data.Char (toUpper)
|
let recentRSSItems = renderAndConcat ["templates/rssitem.xml"]
|
||||||
|
(take 3 renderablePosts)
|
||||||
titleUpper :: ContextManipulation
|
|
||||||
titleUpper = renderValue "title" "title" $ map toUpper
|
|
||||||
~~~~~
|
~~~~~
|
||||||
|
|
||||||
## Applying Context Manipulations
|
We're using the `renderAndConcat` function again. Note that because of
|
||||||
|
hakyll/haskell laziness, this action isn't executed directly, and this helps
|
||||||
|
dependency handling.
|
||||||
|
|
||||||
Now, the question is how to apply these `ContextManipulation`'s. The answer is
|
Now, the `rssPage` page. As you might remember, we use the `createCustomPage`
|
||||||
simple. For every important render function (`render`, `renderChain`,
|
function to create a custom page. We first give the destination url, then a
|
||||||
`renderAndConcat`), there is a variant that takes a `ContextManipulation` as a
|
list of dependencies, and then a list of `(key, value)` pairs.
|
||||||
first argument. These functions are thus: `renderWith`, `renderChainWith`,
|
|
||||||
`renderAndConcatWith`. In fact, the following holds true:
|
|
||||||
|
|
||||||
~~~~~{.haskell}
|
~~~~~{.haskell}
|
||||||
render == renderWith id
|
let rssPage = createCustomPage
|
||||||
renderChain == renderChainWith id
|
"rss.xml"
|
||||||
renderAndConcat == renderAndConcatWith id
|
("templates/postitem.html" : take 3 postPaths)
|
||||||
|
[("items", Right recentRSSItems)]
|
||||||
~~~~~
|
~~~~~
|
||||||
|
|
||||||
So we could use or title manipulation like this:
|
## Adding a link to the feed
|
||||||
|
|
||||||
~~~~~{.haskell}
|
According to the w3 organization,
|
||||||
renderChainWith titleUpper ["templates/default.html"]
|
[you should add \<link\> tags](http://www.w3.org/QA/Tips/use-links) to your
|
||||||
(createPagePath "index.markdown")
|
documents, so we'll do this. In `templates/default.html`:
|
||||||
~~~~~
|
|
||||||
|
|
||||||
## Rendering dates
|
~~~~~~{.html}
|
||||||
|
<head>
|
||||||
|
<title>SimpleBlog - $title</title>
|
||||||
|
<link rel="stylesheet" type="text/css" href="$$root/css/default.css" />
|
||||||
|
<link rel="alternate"
|
||||||
|
type="application/rss+xml"
|
||||||
|
title="SimpleBlog"
|
||||||
|
href="http://example.com/rss.xml" />
|
||||||
|
</head>
|
||||||
|
~~~~~~
|
||||||
|
|
||||||
As you remember, in our previous blog, all posts had a file name like
|
This makes most browsers see the rss feed, and show it in the address bar.
|
||||||
`posts/yyyy-mm-dd-title.extension`, as is the Hakyll convention. But they also
|
If you want, you can also add a pretty button to your blog linking to
|
||||||
had a metadata field `date`, containing a human-readable date. This is not very
|
`rss.xml`.
|
||||||
D.R.Y., of course! Hakyll has a specialized `renderValue` function to deal with
|
|
||||||
dates encoded in paths: `renderDate`.
|
|
||||||
|
|
||||||
~~~~~{.haskell}
|
## That's it!
|
||||||
postManipulation :: ContextManipulation
|
|
||||||
postManipulation = renderDate "date" "%B %e, %Y" "Unknown date"
|
|
||||||
~~~~~
|
|
||||||
|
|
||||||
That manipulation will:
|
Yep, that's it. Feel free to play with the source code in
|
||||||
- Read the date from the file name the post was loaded from.
|
[the zip file](examples/rssblog.zip) and extend the blog further. As always,
|
||||||
- Parse the date and render it in a `%B %e, %Y` format. This is a
|
all questions are welcome on the
|
||||||
`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 postItems = renderAndConcatWith
|
|
||||||
postManipulation
|
|
||||||
"templates/postitem.html"
|
|
||||||
(map createPagePath posts)
|
|
||||||
customPage = createCustomPage
|
|
||||||
url
|
|
||||||
("templates/postitem.html" : posts)
|
|
||||||
[ ("title", Left title)
|
|
||||||
, ("posts", Right postItems)
|
|
||||||
]
|
|
||||||
renderChain ["posts.html", "templates/default.html"]
|
|
||||||
customPage
|
|
||||||
~~~~~
|
|
||||||
|
|
||||||
Our "render all posts" action can now be written as:
|
|
||||||
|
|
||||||
~~~~~{.haskell}
|
|
||||||
renderPostList "posts.html" "All posts" postPaths
|
|
||||||
~~~~~
|
|
||||||
|
|
||||||
## 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 = "/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 :: ContextManipulation
|
|
||||||
postManipulation = renderDate "date" "%B %e, %Y" "Unknown date"
|
|
||||||
. renderTagLinks tagToURL
|
|
||||||
~~~~~
|
|
||||||
|
|
||||||
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}
|
|
||||||
readTagMap [FilePath] -> IO Map String [FilePath]
|
|
||||||
~~~~~
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
~~~~~{.haskell}
|
|
||||||
tagMap <- readTagMap postPaths
|
|
||||||
let renderListForTag (tag, posts) =
|
|
||||||
renderPostList (tagToURL tag)
|
|
||||||
("Posts tagged " ++ tag)
|
|
||||||
mapM_ renderListForTag (toList tagMap)
|
|
||||||
~~~~~
|
|
||||||
|
|
||||||
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}
|
|
||||||
TagCloud :: M.Map String [FilePath]
|
|
||||||
-> (String -> String)
|
|
||||||
-> Float
|
|
||||||
-> Float
|
|
||||||
-> String
|
|
||||||
~~~~~
|
|
||||||
|
|
||||||
The first argument is obviously the result of the `readTagMap` function. The
|
|
||||||
second 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 the
|
|
||||||
tag cloud back. We can add this to our index:
|
|
||||||
|
|
||||||
~~~~~{.haskell}
|
|
||||||
let tagCloud = renderTagCloud tagMap tagToURL 100 200
|
|
||||||
...
|
|
||||||
createCustomPage "index.html"
|
|
||||||
("templates/postitem.html" : take 3 postPaths)
|
|
||||||
[ ("title", Left "Home")
|
|
||||||
, ("posts", Right recentPosts)
|
|
||||||
, ("tagcloud", Left tagCloud)
|
|
||||||
]
|
|
||||||
~~~~~
|
|
||||||
|
|
||||||
## That's it
|
|
||||||
|
|
||||||
Feel free to hack around with the code from the zip file. As always, if you
|
|
||||||
still have questions, ask them at the
|
|
||||||
[google discussion group](http://groups.google.com/group/hakyll).
|
[google discussion group](http://groups.google.com/group/hakyll).
|
||||||
|
|
211
examples/hakyll/tutorial5.markdown
Normal file
211
examples/hakyll/tutorial5.markdown
Normal file
|
@ -0,0 +1,211 @@
|
||||||
|
---
|
||||||
|
title: Tutorial (Part V)
|
||||||
|
what: adds tags and context manipulations to our blog
|
||||||
|
---
|
||||||
|
|
||||||
|
Here, have [a zip file](examples/tagblog.zip) for this tutorial.
|
||||||
|
|
||||||
|
## Context manipulations
|
||||||
|
|
||||||
|
As you might remember, `Renderable` objects are usually 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_.
|
||||||
|
|
||||||
|
In `Text.Hakyll.Context`, we see the type of `ContextManipulation`:
|
||||||
|
|
||||||
|
~~~~~{.haskell}
|
||||||
|
type ContextManipulation = Context -> Context
|
||||||
|
~~~~~
|
||||||
|
|
||||||
|
Where `Context` is a key-value mapping. Usually, you don't create
|
||||||
|
`ContextManipulation`'s by hand, but you use the `renderValue` function. Let's
|
||||||
|
have a look at it's type.
|
||||||
|
|
||||||
|
~~~~~{.haskell}
|
||||||
|
renderValue :: String -> String
|
||||||
|
-> (String -> String)
|
||||||
|
-> ContextManipulation
|
||||||
|
~~~~~
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
~~~~~{.haskell}
|
||||||
|
import Data.Char (toUpper)
|
||||||
|
|
||||||
|
titleUpper :: ContextManipulation
|
||||||
|
titleUpper = renderValue "title" "title" $ map toUpper
|
||||||
|
~~~~~
|
||||||
|
|
||||||
|
## Applying Context Manipulations
|
||||||
|
|
||||||
|
Now, the question is how to apply these `ContextManipulation`'s. The answer is
|
||||||
|
simple. For every important render function (`render`, `renderChain`,
|
||||||
|
`renderAndConcat`), there is a variant that takes a `ContextManipulation` as a
|
||||||
|
first argument. These functions are thus: `renderWith`, `renderChainWith`,
|
||||||
|
`renderAndConcatWith`. In fact, the following holds true:
|
||||||
|
|
||||||
|
~~~~~{.haskell}
|
||||||
|
render == renderWith id
|
||||||
|
renderChain == renderChainWith id
|
||||||
|
renderAndConcat == renderAndConcatWith id
|
||||||
|
~~~~~
|
||||||
|
|
||||||
|
So we could use or title manipulation like this:
|
||||||
|
|
||||||
|
~~~~~{.haskell}
|
||||||
|
renderChainWith titleUpper ["templates/default.html"]
|
||||||
|
(createPagePath "index.markdown")
|
||||||
|
~~~~~
|
||||||
|
|
||||||
|
## 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 :: ContextManipulation
|
||||||
|
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`
|
||||||
|
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 postItems = renderAndConcatWith
|
||||||
|
postManipulation
|
||||||
|
"templates/postitem.html"
|
||||||
|
(map createPagePath posts)
|
||||||
|
customPage = createCustomPage
|
||||||
|
url
|
||||||
|
("templates/postitem.html" : posts)
|
||||||
|
[ ("title", Left title)
|
||||||
|
, ("posts", Right postItems)
|
||||||
|
]
|
||||||
|
renderChain ["posts.html", "templates/default.html"]
|
||||||
|
customPage
|
||||||
|
~~~~~
|
||||||
|
|
||||||
|
Our "render all posts" action can now be written as:
|
||||||
|
|
||||||
|
~~~~~{.haskell}
|
||||||
|
renderPostList "posts.html" "All posts" postPaths
|
||||||
|
~~~~~
|
||||||
|
|
||||||
|
## 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 = "/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 :: ContextManipulation
|
||||||
|
postManipulation = renderDate "date" "%B %e, %Y" "Unknown date"
|
||||||
|
. renderTagLinks tagToURL
|
||||||
|
~~~~~
|
||||||
|
|
||||||
|
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}
|
||||||
|
readTagMap [FilePath] -> IO Map String [FilePath]
|
||||||
|
~~~~~
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
~~~~~{.haskell}
|
||||||
|
tagMap <- readTagMap postPaths
|
||||||
|
let renderListForTag (tag, posts) =
|
||||||
|
renderPostList (tagToURL tag)
|
||||||
|
("Posts tagged " ++ tag)
|
||||||
|
mapM_ renderListForTag (toList tagMap)
|
||||||
|
~~~~~
|
||||||
|
|
||||||
|
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}
|
||||||
|
TagCloud :: M.Map String [FilePath]
|
||||||
|
-> (String -> String)
|
||||||
|
-> Float
|
||||||
|
-> Float
|
||||||
|
-> String
|
||||||
|
~~~~~
|
||||||
|
|
||||||
|
The first argument is obviously the result of the `readTagMap` function. The
|
||||||
|
second 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 the
|
||||||
|
tag cloud back. We can add this to our index:
|
||||||
|
|
||||||
|
~~~~~{.haskell}
|
||||||
|
let tagCloud = renderTagCloud tagMap tagToURL 100 200
|
||||||
|
...
|
||||||
|
createCustomPage "index.html"
|
||||||
|
("templates/postitem.html" : take 3 postPaths)
|
||||||
|
[ ("title", Left "Home")
|
||||||
|
, ("posts", Right recentPosts)
|
||||||
|
, ("tagcloud", Left tagCloud)
|
||||||
|
]
|
||||||
|
~~~~~
|
||||||
|
|
||||||
|
## That's it
|
||||||
|
|
||||||
|
Feel free to hack around with the code from the zip file. As always, if you
|
||||||
|
still have questions, ask them at the
|
||||||
|
[google discussion group](http://groups.google.com/group/hakyll).
|
27
examples/morepages/about.markdown
Normal file
27
examples/morepages/about.markdown
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
---
|
||||||
|
title: About
|
||||||
|
|
||||||
|
--- section1
|
||||||
|
## Mattis
|
||||||
|
Nullam imperdiet sodales orci vitae molestie. Nunc quam orci, pharetra a
|
||||||
|
rhoncus vitae, eleifend id felis. Suspendisse potenti. Etiam vitae urna orci.
|
||||||
|
Quisque pellentesque dignissim felis, egestas tempus urna luctus vitae. In hac
|
||||||
|
habitasse platea dictumst. Morbi fringilla mattis odio, et mattis tellus
|
||||||
|
accumsan vitae.
|
||||||
|
|
||||||
|
--- section2
|
||||||
|
## Orci
|
||||||
|
Vivamus eget mauris sit amet nulla laoreet lobortis. Nulla in diam elementum
|
||||||
|
risus convallis commodo. Cras vehicula varius dui vitae facilisis. Proin
|
||||||
|
elementum libero eget leo aliquet quis euismod orci vestibulum. Duis rhoncus
|
||||||
|
lorem consequat tellus vestibulum aliquam. Quisque orci orci, malesuada porta
|
||||||
|
blandit et, interdum nec magna.
|
||||||
|
|
||||||
|
--- section3
|
||||||
|
## Augue
|
||||||
|
In urna ante, pulvinar et imperdiet nec, fermentum ac tortor. Cras tristique
|
||||||
|
pellentesque euismod. Pellentesque est ante, sagittis vitae vehicula vitae,
|
||||||
|
ullamcorper eget lectus. Curabitur egestas accumsan leo, ac ullamcorper nibh
|
||||||
|
tincidunt id. Curabitur lorem libero, fermentum non tincidunt ac, pretium in
|
||||||
|
libero. Donec vel mi eu tortor accumsan dictum ut in augue. Vestibulum est
|
||||||
|
lorem, bibendum eu vehicula eu, convallis eget mauris.
|
27
examples/morepages/css/default.css
Normal file
27
examples/morepages/css/default.css
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
body {
|
||||||
|
width: 600px;
|
||||||
|
margin: 0px auto 0px auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
div#navigation {
|
||||||
|
text-align: center;
|
||||||
|
border-bottom: 4px solid black;
|
||||||
|
}
|
||||||
|
|
||||||
|
div#navigation a {
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
background-color: black;
|
||||||
|
padding: 3px 10px 3px 10px;
|
||||||
|
margin: 0px 10px 0px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.column {
|
||||||
|
width: 30%;
|
||||||
|
float: left;
|
||||||
|
margin: 0px 10px 0px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div#footer {
|
||||||
|
clear: both;
|
||||||
|
}
|
16
examples/morepages/footer.markdown
Normal file
16
examples/morepages/footer.markdown
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
--- footer
|
||||||
|
## Sapien
|
||||||
|
|
||||||
|
In hac habitasse platea dictumst. Cras placerat felis nec risus varius in
|
||||||
|
accumsan sem fermentum. Vestibulum elementum aliquam tortor semper vulputate.
|
||||||
|
Vivamus tincidunt tellus sed purus tempor fringilla. Morbi dui nisl, eleifend
|
||||||
|
non dictum vitae, luctus eu lacus. Duis vitae lacus sem, ut porta mauris.
|
||||||
|
Aenean sed ultricies dui. Vivamus ullamcorper metus lorem, at ornare nibh.
|
||||||
|
Mauris mi metus, convallis id lobortis vitae, interdum quis felis. Cras elit
|
||||||
|
massa, pellentesque sit amet pharetra ut, volutpat in arcu. Vivamus blandit,
|
||||||
|
ligula et ultricies consequat, metus sem congue quam, ac pretium enim velit at
|
||||||
|
tortor. Cras in tellus eu sapien pulvinar sollicitudin eu id ipsum. Mauris nec
|
||||||
|
urna tellus, et scelerisque tellus. Nunc imperdiet felis nec libero consectetur
|
||||||
|
tristique tristique ipsum sodales. Cras tortor nisl, condimentum in
|
||||||
|
pellentesque id, interdum vel mi. Suspendisse auctor vehicula orci at
|
||||||
|
scelerisque. Vivamus quis sagittis felis.
|
13
examples/morepages/hakyll.hs
Normal file
13
examples/morepages/hakyll.hs
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import Text.Hakyll (hakyll, defaultHakyllConfiguration)
|
||||||
|
import Text.Hakyll.File (directory)
|
||||||
|
import Text.Hakyll.Render (css, static, renderChain)
|
||||||
|
import Text.Hakyll.Renderables (createPagePath, combine)
|
||||||
|
|
||||||
|
main = hakyll defaultHakyllConfiguration $ do
|
||||||
|
directory css "css"
|
||||||
|
render "about.markdown"
|
||||||
|
render "index.markdown"
|
||||||
|
render "products.markdown"
|
||||||
|
where
|
||||||
|
render = renderChain ["templates/default.html"] . withFooter . createPagePath
|
||||||
|
withFooter a = a `combine` createPagePath "footer.markdown"
|
24
examples/morepages/index.markdown
Normal file
24
examples/morepages/index.markdown
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
---
|
||||||
|
title: Home
|
||||||
|
|
||||||
|
--- section1
|
||||||
|
## Purus
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce tempor, urna et
|
||||||
|
auctor tincidunt, eros mauris facilisis purus, eget sollicitudin leo massa sit
|
||||||
|
amet ipsum. Vivamus eu massa in urna vehicula rutrum eget sit amet purus.
|
||||||
|
|
||||||
|
--- section2
|
||||||
|
## Ligula
|
||||||
|
Mauris sit amet justo mi. Curabitur vel quam felis. In hac habitasse platea
|
||||||
|
dictumst. Etiam nec consequat risus. Donec consequat est vitae neque fermentum
|
||||||
|
feugiat nec ac nibh. Nulla rhoncus, odio quis scelerisque rutrum, metus sem
|
||||||
|
tempor ante, a ornare ipsum felis sed ligula. Morbi urna lectus, scelerisque
|
||||||
|
non pharetra in, rutrum quis ligula. Phasellus semper ullamcorper arcu eu
|
||||||
|
auctor.
|
||||||
|
|
||||||
|
--- section3
|
||||||
|
## Justo
|
||||||
|
Aliquam sagittis tincidunt libero ut elementum. Ut sit amet vestibulum metus.
|
||||||
|
Ut aliquet congue neque eu tincidunt. Integer eu elit sed massa sollicitudin
|
||||||
|
vehicula nec ut sem. Cras euismod enim eget purus lacinia non feugiat urna
|
||||||
|
imperdiet. Aliquam justo sem, viverra eu vehicula vitae, imperdiet vel magna.
|
23
examples/morepages/products.markdown
Normal file
23
examples/morepages/products.markdown
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
---
|
||||||
|
title: Products
|
||||||
|
|
||||||
|
--- section1
|
||||||
|
## Lacus
|
||||||
|
Etiam condimentum auctor semper. Donec lobortis, magna id sodales sollicitudin,
|
||||||
|
lectus mi egestas nulla, pulvinar lobortis nunc eros id nisl. Curabitur
|
||||||
|
imperdiet, erat at accumsan vulputate, purus nunc blandit nulla, dictum
|
||||||
|
vestibulum sem lorem eget ipsum.
|
||||||
|
|
||||||
|
--- section2
|
||||||
|
## Vitae
|
||||||
|
Integer ut dui eu felis mollis vestibulum. Etiam at nibh id diam aliquet
|
||||||
|
vestibulum sit amet a nibh. Aliquam erat volutpat. Etiam vitae nulla at dolor
|
||||||
|
fringilla tempor ut a nunc. Pellentesque elementum elit lorem. Quisque nec
|
||||||
|
ligula ipsum. Nunc augue lacus, ullamcorper vel dapibus in, mattis eget elit.
|
||||||
|
|
||||||
|
--- section3
|
||||||
|
## Feugiat
|
||||||
|
Pellentesque enim dui, interdum elementum vehicula luctus, feugiat vitae arcu.
|
||||||
|
Vestibulum ut felis justo. Quisque vestibulum mauris eget ipsum luctus
|
||||||
|
consequat. Nunc tincidunt, turpis ut fermentum dapibus,
|
||||||
|
justo tortor bibendum sem, at facilisis justo odio luctus lectus.
|
22
examples/morepages/templates/default.html
Normal file
22
examples/morepages/templates/default.html
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
|
||||||
|
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
||||||
|
<head>
|
||||||
|
<title>MyAweSomeCompany - $title</title>
|
||||||
|
<link rel="stylesheet" type="text/css" href="$root/css/default.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>MyAweSomeCompany - $title</h1>
|
||||||
|
<div id="navigation">
|
||||||
|
<a href="$root/index.html">Home</a>
|
||||||
|
<a href="$root/about.html">About</a>
|
||||||
|
<a href="$root/products.html">Products</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="column"> $section1 </div>
|
||||||
|
<div class="column"> $section2 </div>
|
||||||
|
<div class="column"> $section3 </div>
|
||||||
|
<div id="footer"> $footer </div>
|
||||||
|
</body>
|
||||||
|
</html>
|
Loading…
Reference in a new issue