From 0ba0eac8d96c11d13163c7ec184392513de7ca77 Mon Sep 17 00:00:00 2001 From: Jasper Van der Jeugt Date: Tue, 19 Jan 2010 17:17:48 +0100 Subject: Wrote another tutorial, and updated some tutorials. --- examples/hakyll/tutorial1.markdown | 14 +- examples/hakyll/tutorial2.markdown | 221 ++++++++++++------------- examples/hakyll/tutorial3.markdown | 222 +++++++++++++++---------- examples/hakyll/tutorial4.markdown | 267 ++++++++++-------------------- examples/hakyll/tutorial5.markdown | 211 +++++++++++++++++++++++ examples/morepages/about.markdown | 27 +++ examples/morepages/css/default.css | 27 +++ examples/morepages/footer.markdown | 16 ++ examples/morepages/hakyll.hs | 13 ++ examples/morepages/index.markdown | 24 +++ examples/morepages/products.markdown | 23 +++ examples/morepages/templates/default.html | 22 +++ 12 files changed, 699 insertions(+), 388 deletions(-) create mode 100644 examples/hakyll/tutorial5.markdown create mode 100644 examples/morepages/about.markdown create mode 100644 examples/morepages/css/default.css create mode 100644 examples/morepages/footer.markdown create mode 100644 examples/morepages/hakyll.hs create mode 100644 examples/morepages/index.markdown create mode 100644 examples/morepages/products.markdown create mode 100644 examples/morepages/templates/default.html diff --git a/examples/hakyll/tutorial1.markdown b/examples/hakyll/tutorial1.markdown index fafaea7..f3c99aa 100644 --- a/examples/hakyll/tutorial1.markdown +++ b/examples/hakyll/tutorial1.markdown @@ -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 `about.markdown` page. -> --- -> title: About -> --- -> Nullam imperdiet sodales orci vitae molestie. -> Nunc quam orci, pharetra a rhoncus vitae, -> eleifend id felis. Suspendisse potenti... + --- + title: About + --- + Nullam imperdiet sodales orci vitae molestie. + Nunc quam orci, pharetra a rhoncus vitae, + eleifend id felis. Suspendisse potenti... 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 @@ -209,4 +209,4 @@ questions, feel free to ask them on the [google discussion group](http://groups.google.com/group/hakyll). If you feel comfortable with the basics, here is a next tutorial: -[building a simple blog](tutorial2.html). +[more about writing pages](tutorial2.html). diff --git a/examples/hakyll/tutorial2.markdown b/examples/hakyll/tutorial2.markdown index 0fa4002..d4c3d2f 100644 --- a/examples/hakyll/tutorial2.markdown +++ b/examples/hakyll/tutorial2.markdown @@ -1,162 +1,159 @@ --- 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 -advanced: we are going to create a simple blog system. +The most important thing to realize is that a `Page` is just a key-value +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 -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 will produce the following mapping: -~~~~~{.haskell} -main = hakyll $ do - directory css "css" -~~~~~ +- `title`: About +- `author`: Mia Wallace +- `body`: Hello there! This is a simple about page. -## Finding the posts, and a bit about renderables +`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: -`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: + Hello there! This is + a simple about page. -> 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... +This will produce one key-value pair: -Now, we find the posts and sort them reversed: +- `body`: Hello there! This is a simple about page. -~~~~~{.haskell} --- Find all post paths. -postPaths <- liftM (reverse . sort) $ getRecursiveContents "posts" -~~~~~ +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 -Our `postPaths` value is now of the type `[FilePath]`. `FilePath` is no -instance of `Renderable`, but `PagePath` is: +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} -let renderablePosts = map createPagePath postPaths +combine :: (Renderable a, Renderable b) + => a -> b -> CombinedRenderable a b ~~~~~ -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: +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). + +Combining two `Renderable`s, but setting a different `url` is quite common, so +there is another function that helps us here: ~~~~~{.haskell} -mapM_ (renderChain [ "templates/post.html" - , "templates/default.html" - ]) renderablePosts +combineWithURL :: (Renderable a, Renderable b) + => FilePath -> a -> b -> CombinedRenderable a b ~~~~~ -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. +## The example -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. +Now that we have the tools, we'll get on to the example. This time, we'll +be making a more advanced brochure site. Here +[is a zip file](examples/morepages.zip) containing the source code for the +tutorial. -## Custom Pages +Every page consists of three sections, originally named `section1`, `section2` +and `section3`. So our pages look more or less like this: -Currently, there are 3 renderable datatypes in Hakyll: + --- + title: About -- `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. + --- section1 + ## Mattis + Nullam imperdiet sodales orci vitae molestie. Nunc... -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)] -~~~~~ + --- section2 + ## Orci + Vivamus eget mauris sit amet nulla laoreet lobortis. Nulla in... -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. + --- section3 + ## Augue + In urna ante, pulvinar et imperdiet nec, fermentum ac... -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: +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: -- `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. +~~~~~{.html} +
$section1
+
$section2
+
$section3
+~~~~~ -First, let us define this `IO String` for our index page. We want to render -every post using a simple template: +The columns are then floated using css. So far so good, but what if we wanted +an additional text block on every page? An easy solution would be to add this +to the template, but then our layout-content separation idea will be broken +again. So we simply add to the template: ~~~~~{.html} -
  • - $title - - $date - by $author -
  • + ~~~~~ -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. +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} -let recentPosts = renderAndConcat "templates/postitem.html" - (take 3 renderablePosts) +withFooter a = a `combine` createPagePath "footer.markdown" ~~~~~ -Now, creating our custom page is fairly straight-forward: +Now, were we previously wrote: ~~~~~{.haskell} -createCustomPage "index.html" - ("templates/postitem.html" : take 3 postPaths) - [ ("title", Left "All posts") - , ("posts", Right recentPosts) - ] +render "about.markdown" +where render = renderChain ["templates/default.html"] + . createPagePath ~~~~~ -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: +We simply have to add our footer: ~~~~~{.haskell} -renderChain ["index.html", "templates/default.html"] $ - createCustomPage "index.html" - ("templates/postitem.html" : take 3 postPaths) - [ ("title", Left "All posts") - , ("posts", Right recentPosts) - ] +render "about.markdown" +where render = renderChain ["templates/default.html"] + . withFooter + . createPagePath ~~~~~ -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). - -There is a [next tutorial](tutorial3.html), explaining how to add an RSS feed -to our sample blog. diff --git a/examples/hakyll/tutorial3.markdown b/examples/hakyll/tutorial3.markdown index e241d63..80f90e8 100644 --- a/examples/hakyll/tutorial3.markdown +++ b/examples/hakyll/tutorial3.markdown @@ -1,124 +1,162 @@ --- 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 - -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). - -An RSS feed looks like this: - -~~~~~{.xml} - - - - The SimpleBlog - http://example.com/ - Simple blog in hakyll - - Title goes here - http://example.com/post.html - - A description is optional. - - - - -~~~~~ +## Creating a simple blog with Hakyll -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} - - - - The SimpleBlog - http://jaspervdj.be/ - Simple blog in hakyll - $items - - -~~~~~ +After we created a simple brochure site, we're going to try something more +advanced: we are going to create a simple blog system. -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). +A [zip file containing the source](examples/simpleblog.zip) for this +tutorial is also available. + +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} -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 -`Renderable` that "fills in" the `$items` identifier. We're going to do this -using a [custom page](tutorial2.html#custom-pages). +## Finding the posts, and a bit about renderables + +`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: -## Custom pages again + 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... -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. +Now, we find the posts and sort them reversed: + +~~~~~{.haskell} +-- Find all post paths. +postPaths <- liftM (reverse . sort) $ getRecursiveContents "posts" +~~~~~ -We want to render every post using the following template, -`templates/rssitem.xml`: +Our `postPaths` value is now of the type `[FilePath]`. `FilePath` is no +instance of `Renderable`, but `PagePath` is: ~~~~~{.haskell} - - $title - http://example.com/$url - $title by $author - +let renderablePosts = map createPagePath postPaths ~~~~~ -Since we build on the previous example, we still have our `renderablePosts` -list. We'll be using it again: +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} -let recentRSSItems = renderAndConcat "templates/rssitem.xml" - (take 3 renderablePosts) +mapM_ (renderChain [ "templates/post.html" + , "templates/default.html" + ]) renderablePosts ~~~~~ -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. +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. -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. +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. + +## Custom Pages + +Currently, there are 3 renderable datatypes in Hakyll: + +- `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} -let rssPage = createCustomPage - "rss.xml" - ("templates/postitem.html" : take 3 postPaths) - [("items", Right recentRSSItems)] +createCustomPage :: FilePath + -> [FilePath] + -> [(String, Either String (IO String)] ~~~~~ -## Adding a link to the feed +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. -According to the w3 organization, -[you should add \ tags](http://www.w3.org/QA/Tips/use-links) to your -documents, so we'll do this. In `templates/default.html`: +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: -~~~~~~{.html} - - SimpleBlog - $title - - - -~~~~~~ +- `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. -This makes most browsers see the rss feed, and show it in the address bar. -If you want, you can also add a pretty button to your blog linking to -`rss.xml`. +First, let us define this `IO String` for our index page. We want to render +every post using a simple template: -## That's it! +~~~~~{.html} +
  • + $title + - $date - by $author +
  • +~~~~~ + +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. -Yep, that's it. Feel free to play with the source code in -[the zip file](examples/rssblog.zip) and extend the blog further. As always, -all questions are welcome on the +~~~~~{.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). + +There is a [next tutorial](tutorial4.html), explaining how to add an RSS feed +to our sample blog. diff --git a/examples/hakyll/tutorial4.markdown b/examples/hakyll/tutorial4.markdown index d1d6366..a53e795 100644 --- a/examples/hakyll/tutorial4.markdown +++ b/examples/hakyll/tutorial4.markdown @@ -1,211 +1,124 @@ --- 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. - -## 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 +## Adding RSS to our simple blog + +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). + +An RSS feed looks like this: + +~~~~~{.xml} + + + + The SimpleBlog + http://example.com/ + Simple blog in hakyll + + Title goes here + http://example.com/post.html + + A description is optional. + + + + ~~~~~ -So we could use or title manipulation like this: - -~~~~~{.haskell} -renderChainWith titleUpper ["templates/default.html"] - (createPagePath "index.markdown") +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} + + + + The SimpleBlog + http://jaspervdj.be/ + Simple blog in hakyll + $items + + ~~~~~ -## 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`. +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} -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 +renderChain ["templates/rss.xml"] rssPage ~~~~~ -Our "render all posts" action can now be written as: +This, as you can see, is a regular render chain, once again. We need make a +`Renderable` that "fills in" the `$items` identifier. We're going to do this +using a [custom page](tutorial2.html#custom-pages). -~~~~~{.haskell} -renderPostList "posts.html" "All posts" postPaths -~~~~~ +## Custom pages again -## Tag links +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 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: +We want to render every post using the following template, +`templates/rssitem.xml`: ~~~~~{.haskell} -tagToURL tag = "/tags/" ++ removeSpaces tag ++ ".html" + + $title + http://example.com/$url + $title by $author + ~~~~~ -`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`. +Since we build on the previous example, we still have our `renderablePosts` +list. We'll be using it again: ~~~~~{.haskell} -postManipulation :: ContextManipulation -postManipulation = renderDate "date" "%B %e, %Y" "Unknown date" - . renderTagLinks tagToURL +let recentRSSItems = renderAndConcat ["templates/rssitem.xml"] + (take 3 renderablePosts) ~~~~~ -If we click a tag, we get a `404`. That's because we haven't generated the -post lists for every tag. +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. -## The Tag Map - -Hakyll provides a function called `readTagMap`. Let's inspect it's type. +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} -readTagMap [FilePath] -> IO Map String [FilePath] +let rssPage = createCustomPage + "rss.xml" + ("templates/postitem.html" : take 3 postPaths) + [("items", Right recentRSSItems)] ~~~~~ -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. +## Adding a link to the feed -~~~~~{.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 +According to the w3 organization, +[you should add \ tags](http://www.w3.org/QA/Tips/use-links) to your +documents, so we'll do this. In `templates/default.html`: -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. +~~~~~~{.html} + + SimpleBlog - $title + + + +~~~~~~ -~~~~~{.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) - ] -~~~~~ +This makes most browsers see the rss feed, and show it in the address bar. +If you want, you can also add a pretty button to your blog linking to +`rss.xml`. -## That's it +## 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 +Yep, that's it. Feel free to play with the source code in +[the zip file](examples/rssblog.zip) and extend the blog further. As always, +all questions are welcome on the [google discussion group](http://groups.google.com/group/hakyll). diff --git a/examples/hakyll/tutorial5.markdown b/examples/hakyll/tutorial5.markdown new file mode 100644 index 0000000..84f0dee --- /dev/null +++ b/examples/hakyll/tutorial5.markdown @@ -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). diff --git a/examples/morepages/about.markdown b/examples/morepages/about.markdown new file mode 100644 index 0000000..59d1aca --- /dev/null +++ b/examples/morepages/about.markdown @@ -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. diff --git a/examples/morepages/css/default.css b/examples/morepages/css/default.css new file mode 100644 index 0000000..0376cae --- /dev/null +++ b/examples/morepages/css/default.css @@ -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; +} diff --git a/examples/morepages/footer.markdown b/examples/morepages/footer.markdown new file mode 100644 index 0000000..408541c --- /dev/null +++ b/examples/morepages/footer.markdown @@ -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. diff --git a/examples/morepages/hakyll.hs b/examples/morepages/hakyll.hs new file mode 100644 index 0000000..a806042 --- /dev/null +++ b/examples/morepages/hakyll.hs @@ -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" diff --git a/examples/morepages/index.markdown b/examples/morepages/index.markdown new file mode 100644 index 0000000..4a2dc80 --- /dev/null +++ b/examples/morepages/index.markdown @@ -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. diff --git a/examples/morepages/products.markdown b/examples/morepages/products.markdown new file mode 100644 index 0000000..7032479 --- /dev/null +++ b/examples/morepages/products.markdown @@ -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. diff --git a/examples/morepages/templates/default.html b/examples/morepages/templates/default.html new file mode 100644 index 0000000..ef3e03c --- /dev/null +++ b/examples/morepages/templates/default.html @@ -0,0 +1,22 @@ + + + + + MyAweSomeCompany - $title + + + +

    MyAweSomeCompany - $title

    + + +
    $section1
    +
    $section2
    +
    $section3
    + + + -- cgit v1.2.3