From d3b87ba2590480a05d8b13770889ac1f14a2b573 Mon Sep 17 00:00:00 2001 From: Jasper Van der Jeugt Date: Fri, 6 Aug 2010 13:28:23 +0200 Subject: Add tutorial about templates --- examples/hakyll/tutorials/part03.markdown | 2 +- examples/hakyll/tutorials/part04.markdown | 183 ++++++---------------- examples/hakyll/tutorials/part05.markdown | 177 ++++++++++++++------- examples/hakyll/tutorials/part06.markdown | 239 ++++++----------------------- examples/hakyll/tutorials/part07.markdown | 247 ++++++++++++++++++++++-------- examples/hakyll/tutorials/part08.markdown | 135 +++++++--------- examples/hakyll/tutorials/part09.markdown | 104 +++++++++++++ 7 files changed, 565 insertions(+), 522 deletions(-) create mode 100644 examples/hakyll/tutorials/part09.markdown diff --git a/examples/hakyll/tutorials/part03.markdown b/examples/hakyll/tutorials/part03.markdown index 3b04f9a..0ba633e 100644 --- a/examples/hakyll/tutorials/part03.markdown +++ b/examples/hakyll/tutorials/part03.markdown @@ -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 diff --git a/examples/hakyll/tutorials/part04.markdown b/examples/hakyll/tutorials/part04.markdown index f21819f..22fc846 100644 --- a/examples/hakyll/tutorials/part04.markdown +++ b/examples/hakyll/tutorials/part04.markdown @@ -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. +Simple templates are simply HTML files, with `$identifiers`. An example: -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 +~~~~~{.html} + + + $title + + + $body + + ~~~~~ -[^1]: Since Hakyll-1.3 onwards. +## Markup in templates -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: +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: -~~~~~{.html} -
  • - $title - - $date - by $author -
  • -~~~~~ + # $title -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: + _On $date_ -~~~~~{.haskell} -let index = createListing "index.html" - ["templates/postitem.html"] - (take 3 postPages) - [("title", Left "Home")] -~~~~~ + $body -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. +__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. -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. +## Hamlet templates -~~~~~{.haskell} -renderChain ["index.html", "templates/default.html"] index -~~~~~ +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: -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. + !!! + %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$ -## The gist of it +Hakyll will recognise hamlet templates automatically by the `.hamlet` extension. -- 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. +[hamlet]: http://docs.yesodweb.com/hamlet/ diff --git a/examples/hakyll/tutorials/part05.markdown b/examples/hakyll/tutorials/part05.markdown index d64dc55..f21819f 100644 --- a/examples/hakyll/tutorials/part05.markdown +++ b/examples/hakyll/tutorials/part05.markdown @@ -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 +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. -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. +~~~~~{.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: -## Creating a configuration +~~~~~{.haskell} +postPaths <- liftM (reverse . sort) $ getRecursiveContents "posts" +~~~~~ -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. +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} -myFeedConfiguration = FeedConfiguration - { feedUrl = "rss.xml" - , feedTitle = "SimpleBlog RSS feed." - , feedDescription = "Simple demo of a feed created with Hakyll." - , feedAuthorName = "Jasper Van der Jeugt" - } +let postPages = map createPage postPaths ~~~~~ -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. +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} -renderRss myFeedConfiguration (take 3 postPages) +mapM_ (renderChain [ "templates/post.html" + , "templates/default.html" + ]) postPages ~~~~~ -## But it's not that easy +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. -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? +## Creating listings. -To render feeds, Hakyll expects a number of fields in the renderables you put -in the feed. They are: +`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`. -- `$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. +~~~~~{.haskell} +createCustomPage :: FilePath + -> [(String, Either String (HakyllAction () String)] + -> HakyllAction () Context +~~~~~ -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. +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: -## Where arrows come in +- `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. -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. +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} -renderRss myFeedConfiguration $ - map (>>> copyValue "body" "description") (take 3 postPages) +createListing :: FilePath + -> [FilePath] + -> [HakyllAction () Context] + -> [(String, Either String (HakyllAction () String)] + -> HakyllAction () Context ~~~~~ -And that's that, now our feed gets rendered properly. Exercise for the reader -is to add a Atom feed[^2]. +[^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} +
  • + $title + - $date - by $author +
  • +~~~~~ + +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 +~~~~~ -[^2]: Hint: look around in the [reference]($root/reference.html). +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. diff --git a/examples/hakyll/tutorials/part06.markdown b/examples/hakyll/tutorials/part06.markdown index d017a1e..d64dc55 100644 --- a/examples/hakyll/tutorials/part06.markdown +++ b/examples/hakyll/tutorials/part06.markdown @@ -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 -~~~~~{.haskell} -renderValue :: String - -> String - -> (String -> String) - -> HakyllAction Context Context -~~~~~ +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. -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. +## Creating a configuration -As a simple example, let's write a function that puts the `$title` in uppercase. +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} -import Data.Char (toUpper) - -titleUpper :: HakyllAction Context Context -titleUpper = renderValue "title" "title" $ map toUpper +myFeedConfiguration = FeedConfiguration + { feedUrl = "rss.xml" + , feedTitle = "SimpleBlog RSS feed." + , feedDescription = "Simple demo of a feed created with Hakyll." + , feedAuthorName = "Jasper Van der Jeugt" + } ~~~~~ -Because the destination `key` is the same as the source `key`, we can also use -the `changeValue` function here. +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} -titleUpper = changeValue "title" $ map toUpper +renderRss myFeedConfiguration (take 3 postPages) ~~~~~ -For further reading, refer to the `Text.Hakyll.ContextManipulations` -documentation. - -## Applying Context Manipulations +## But it's not that easy -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) -~~~~~ +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? -## Rendering dates +To render feeds, Hakyll expects a number of fields in the renderables you put +in the feed. They are: -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. - -## 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 `epic fail, ...`. -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. +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. -~~~~~{.haskell} -let tagMap = readTagMap "postTags" postPaths -~~~~~ +## Where arrows come in -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? +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} -let renderListForTag tag posts = - renderPostList (tagToUrl tag) - ("Posts tagged " ++ tag) - (map (>>> postManipulation) posts) -withTagMap tagMap renderPostList +renderRss myFeedConfiguration $ + map (>>> copyValue "body" "description") (take 3 postPages) ~~~~~ -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. +And that's that, now our feed gets rendered properly. Exercise for the reader +is to add a Atom feed[^2]. -~~~~~{.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`. diff --git a/examples/hakyll/tutorials/part07.markdown b/examples/hakyll/tutorials/part07.markdown index 87c999a..d017a1e 100644 --- a/examples/hakyll/tutorials/part07.markdown +++ b/examples/hakyll/tutorials/part07.markdown @@ -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. +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_. + +Like rendering actions, _context manipulations_ are also simply +`HakyllAction Context Context` arrows. The `Text.Hakyll.ContextManipulations` +contains some functions to easily construct easy variants. + +One of the most general functions is the `renderValue` function. Let's have a +look at it's type. + +~~~~~{.haskell} +renderValue :: String + -> String + -> (String -> String) + -> HakyllAction Context Context +~~~~~ + +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 :: HakyllAction Context Context +titleUpper = renderValue "title" "title" $ map toUpper ~~~~~ -[jasper@alice ~]$ cabal install --reinstall -fhighlighting pandoc + +Because the destination `key` is the same as the source `key`, we can also use +the `changeValue` function here. + +~~~~~{.haskell} +titleUpper = changeValue "title" $ map toUpper ~~~~~ -## Auto-compilation +For further reading, refer to the `Text.Hakyll.ContextManipulations` +documentation. -Hakyll features a simple _auto-compilation_ mode. This is invoked by running +## 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) ~~~~~ -[jasper@alice ~]$ ./hakyll preview -Starting hakyll server on port 8000... + +## 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" ~~~~~ -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. +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 +~~~~~ -## When to rebuild +Our "render all posts" action can now be written as: -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. +~~~~~{.haskell} +renderPostList "posts.html" "All posts" renderablePosts +~~~~~ -- 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. +## Tag links -## Pretty URL's +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: -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. +~~~~~{.haskell} +tagToUrl tag = "$root/tags/" ++ removeSpaces tag ++ ".html" +~~~~~ -It can be enabled this way: +`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} -import Text.Hakyll -import Text.Hakyll.Hakyll +postManipulation :: HakyllAction Context Context +postManipulation = renderDate "date" "%B %e, %Y" "Unknown date" + >>> renderTagLinks tagToUrl +~~~~~ -myConfig :: HakyllConfiguration -myConfig = (defaultHakyllConfiguration "http://jaspervdj.be") - { enableIndexUrl = True - } +We apply this manipulation when we load the tags. -main = hakyllWithConfiguration myConfig $ do - -- Further code here +~~~~~{.haskell} +let renderablePosts = + map ((>>> postManipulation) . createPage) postPaths ~~~~~ -The effect will be that the internal `toUrl` function will behave differently. -A few examples: +So, the `renderTagLinks` function replaces the `$tags` value from +`epic fail, random` to `epic fail, ...`. +If we click a tag, we get a `404`. That's because we haven't generated the +post lists for every tag. -- `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`. +## The Tag Map -The benefit of this is simply prettier URL's. That is, if you consider -`example.com/about` prettier than `example.com/about.html`. +Hakyll provides a function called `readTagMap`. Let's inspect it's type. -## Default values +~~~~~{.haskell} +type TagMap = Map String [HakyllAction () Context] +readTagMap String [FilePath] -> HakyllAction () TagMap +~~~~~ -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. +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. -- 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} +let tagMap = readTagMap "postTags" postPaths +~~~~~ -- 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]. +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]: Original idea by zenzike. +[^1]: Exercise for the reader: why do we use `>>> postManipulation` again here? -## Markup in templates +~~~~~{.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. -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: +## A Tag Cloud - # $title +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. - _On $date_ +~~~~~{.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 +~~~~~ - $body +## The gist of it -__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. +- 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. diff --git a/examples/hakyll/tutorials/part08.markdown b/examples/hakyll/tutorials/part08.markdown index 4cc1d43..bfb986b 100644 --- a/examples/hakyll/tutorials/part08.markdown +++ b/examples/hakyll/tutorials/part08.markdown @@ -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. +~~~~~ +[jasper@alice ~]$ cabal install --reinstall -fhighlighting pandoc +~~~~~ -![Tags illustration]($root/images/tutorial8-categories.png) +## Auto-compilation -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. +Hakyll features a simple _auto-compilation_ mode. This is invoked by running -## About category support +~~~~~ +[jasper@alice ~]$ ./hakyll preview +Starting hakyll server on port 8000... +~~~~~ -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. +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. -## Reading Categories +## When to rebuild -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: +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. - 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 +- 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. -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: +## Pretty URL's -~~~~~{.haskell} -postPaths <- liftM (reverse . sortByBaseName) - (getRecursiveContents "posts") -~~~~~ +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. -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. +It can be enabled this way: ~~~~~{.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. +import Text.Hakyll +import Text.Hakyll.Hakyll -## Rendering a category list +myConfig :: HakyllConfiguration +myConfig = (defaultHakyllConfiguration "http://jaspervdj.be") + { enableIndexUrl = True + } -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 = - "
  • " ++ link category (categoryToUrl category) - ++ " - " ++ show (length posts) ++ " items.
  • " +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`. + +The benefit of this is simply prettier URL's. That is, if you consider +`example.com/about` prettier than `example.com/about.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. +## Default values -[^1]: This is a feature, not a bug. It helps dependency handling. +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. -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). +- 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. -## The gist of it +- 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]. -- 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. +[^1]: Original idea by zenzike. diff --git a/examples/hakyll/tutorials/part09.markdown b/examples/hakyll/tutorials/part09.markdown new file mode 100644 index 0000000..4cc1d43 --- /dev/null +++ b/examples/hakyll/tutorials/part09.markdown @@ -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 = + "
  • " ++ link category (categoryToUrl category) + ++ " - " ++ show (length posts) ++ " items.
  • " +~~~~~ + +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. -- cgit v1.2.3