From e7918c39f8378cc5d5df390a0f6a9f60dd3ba1a3 Mon Sep 17 00:00:00 2001 From: Jasper Van der Jeugt Date: Fri, 12 Mar 2010 16:46:08 +0100 Subject: Wrote fourth part of the tutorials. --- examples/hakyll/tutorials/part04.markdown | 157 ++++++++++++++++++++++++++++++ examples/simpleblog/hakyll.hs | 12 +-- 2 files changed, 163 insertions(+), 6 deletions(-) create mode 100644 examples/hakyll/tutorials/part04.markdown diff --git a/examples/hakyll/tutorials/part04.markdown b/examples/hakyll/tutorials/part04.markdown new file mode 100644 index 0000000..5e69cd8 --- /dev/null +++ b/examples/hakyll/tutorials/part04.markdown @@ -0,0 +1,157 @@ +--- +title: Creating a Blog +what: creates a simple blog +--- + +## Creating a simple blog with Hakyll + +After we created a simple brochure site, we're going to try something more +advanced: we are going to create a simple blog system. + +A [zip file] containing the source for this tutorial is also available. + +[zip file]: examples/simpleblog.zip + +Blogs, as you probably know, are composed of posts. In Hakyll, we're going +to use simple pages for posts. All posts are located in the `posts` +directory. But we're not going to use the `directory` command here - you will +see why later. First, some trivial things like css. + +~~~~~{.haskell} +main = hakyll "http://example.com" $ do + directory css "css" +~~~~~ + +## Finding the posts + +`Text.Hakyll.File` contains a handy function `getRecursiveContents`, which will +provide us with all the blog posts. The blog posts have a +`yyyy-mm-dd-title.extension` naming scheme. This is just a simple trick so we +can sort them easily (sorting on filename implies sorting on date). You could of +course name them whatever you want, but it's always a good idea to stick to the +conventions. They contain some metadata, too: + + title: A first post + author: Julius Caesar + date: November 5, 2009 + --- + Lorem ipsum dolor sit amet, consectetur adipiscing elit. + Vivamus pretium leo adipiscing lectus iaculis lobortis. + Vivamus scelerisque velit dignissim metus... + +Now, we find the posts and sort them reversed, so the most recent post will +become the first item in the list: + +~~~~~{.haskell} +postPaths <- liftM (reverse . sort) $ getRecursiveContents "posts" +~~~~~ + +Our `postPaths` value is now of the type `[FilePath]`. We want to be able to +render all posts, so we pass them to the `createPage` function. + +~~~~~{.haskell} +let postPages = map createPage postPaths +~~~~~ + +We have two templates we want to render our posts with: first we would like to +render them using `templates/post.html`, and we want to render the result +using `templates/default.html`. This can be done with the `renderChain` +function: + +~~~~~{.haskell} +mapM_ (renderChain [ "templates/post.html" + , "templates/default.html" + ]) postPages +~~~~~ + +Remember that the `renderChain` works by rendering the item using the first +template, creating a new page with the render result in the `$body` field, and +so on until it has been rendered with all templates. + +Now, we have the posts rendered. What is left is to generate some kind of index +page with links to those posts. We want one general list showing all posts, and +we want to show a few recent posts on the index page. + +## Creating listings. + +`createPage` is the easiest way of reading a `Context`. But in this case, we +want something more custom, so we'll use the `createCustomPage` function. This +allows us to create a more specific `Context`. + +~~~~~{.haskell} +createCustomPage :: FilePath + -> [(String, Either String (HakyllAction () String)] + -> HakyllAction () Context +~~~~~ + +The first argument is the `url` of the page to generate. For our index page, +this will be, `index.html`. The second argument is obviously our `key: value` +mapping. But why the `Either`? This, once again, is about dependency handling. +The idea is that you can choose which type to use for the value: + +- `String`: Simply a `String`. +- `HakyllAction () String`: Here, you can give an `HakyllAction` Arrow action + that can produce a String. However - this action _will not be executed_ when + the file in `_site` is up-to-date. + +However, in this specific case - a list of posts - there is an easier, and more +high-level approach than `createCustomPage`[^1]. Let's look at the type +signature of `createListing`: + +~~~~~{.haskell} +createListing :: FilePath + -> [FilePath] + -> [HakyllAction () Context] + -> [(String, Either String (HakyllAction () String)] + -> HakyllAction () Context +~~~~~ + +[^1]: Since Hakyll-1.3 onwards. + +The first argument is the destination url. For our blog, this is of course +`index.html`. The second argument is a list templates to render _each_ `Context` +with. We use only `templates/postitem.html` here. This is, as you can see, a +simple template: + +~~~~~{.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 +~~~~~ + +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 + +- 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/simpleblog/hakyll.hs b/examples/simpleblog/hakyll.hs index 638861a..75173f8 100644 --- a/examples/simpleblog/hakyll.hs +++ b/examples/simpleblog/hakyll.hs @@ -4,29 +4,29 @@ import Text.Hakyll (hakyll) import Text.Hakyll.Render import Text.Hakyll.Context import Text.Hakyll.File (getRecursiveContents, directory) -import Text.Hakyll.Renderables (createPagePath, createCustomPage, createListing) +import Text.Hakyll.CreateContext (createPage, createCustomPage, createListing) import Data.List (sort) import Control.Monad (mapM_, liftM) import Control.Monad.Reader (liftIO) import Data.Either (Either(..)) -main = hakyll $ do +main = hakyll "http://example.com" $ do -- Static directory. directory css "css" -- Find all post paths. postPaths <- liftM (reverse . sort) $ getRecursiveContents "posts" - let renderablePosts = map createPagePath postPaths + let postPages = map createPage postPaths -- Render index, including recent posts. - let index = createListing "index.html" "templates/postitem.html" (take 3 renderablePosts) [("title", "Home")] + let index = createListing "index.html" ["templates/postitem.html"] (take 3 postPages) [("title", Left "Home")] renderChain ["index.html", "templates/default.html"] index -- Render all posts list. - let posts = createListing "posts.html" "templates/postitem.html" renderablePosts [("title", "All posts")] + let posts = createListing "posts.html" ["templates/postitem.html"] postPages [("title", Left "All posts")] renderChain ["posts.html", "templates/default.html"] posts -- Render all posts. liftIO $ putStrLn "Generating posts..." - mapM_ (renderChain ["templates/post.html", "templates/default.html"]) renderablePosts + mapM_ (renderChain ["templates/post.html", "templates/default.html"]) postPages -- cgit v1.2.3