@@ -1,8 +1,8 @@
3Hakyll is a simple static site generator library in Haskell. It is mostly
3Hakyll is a simple static site generator library in Haskell. It is mostly
4−inspired by [Jekyll](http://github.com/mojombo/jekyll), but I like to believe
4+inspired by [Jekyll](http://github.com/mojombo/jekyll), but I like to
5−it is simpler. An example site where it is used is
5+believe it is simpler. An example site where it is used is
6[my personal homepage](http://jaspervdj.be) of which
6[my personal homepage](http://jaspervdj.be) of which
7[the source code](http://jaspervdj.be/snapshot.tar.gz) is available as a
7[the source code](http://jaspervdj.be/snapshot.tar.gz) is available as a
@@ -19,6 +19,7 @@ be generated. In the rest of this document, we will examine a small example.
20This is our directory layout:
20This is our directory layout:
23 |-- favicon.ico
24 |-- favicon.ico
24 |-- hakyll.hs
25 |-- hakyll.hs
@@ -29,12 +30,16 @@ This is our directory layout:
29 | `-- sample.html
30 | `-- sample.html
30 `-- text.markdown
31 `-- text.markdown
33+The `_cache` and `_site` directories will be automatically created by hakyll.
32## Static files
35## Static files
34−Static files can be rendered using the `static` command. This command ensures
37+Static files can be rendered using the `static` function. This function
35−the files will copied when you compile the site.
38+ensures the files will copied when you compile the site. Hakyll is smart enough
39+to know when files have changed, and will check the modification time of a file
37−For convenience reasons, there is also a `staticDirectory` command, which works
42+For convenience reasons, there is also a `staticDirectory` function, which works
38recursively.
43recursively.
@@ -44,9 +49,8 @@ recursively.
47−Pages can be written in html, markdown, LaTeX, and basically anything
52+Pages can be written in html, markdown, LaTeX, and basically anything pandoc
48−pandoc supports. They can also contain metadata, which are always key-value
53+supports. They can also contain metadata, which are always key-value mappings.
52 author: Jasper Van der Jeugt
56 author: Jasper Van der Jeugt
@@ -61,11 +65,11 @@ mappings.
61 main = putStrLn "Hello World!"
65 main = putStrLn "Hello World!"
64−Metadata is always placed in the beginning of a file, and is delimited by a
68+Metadata is always placed in the header of a file, and is delimited by a `---`
65−`---` string. The metadata can only contain simple key-value pairs. We can
69+string. The metadata can only contain simple key-value pairs. We can now read
66−now read in this page using the `Text.Hakyll.Page.readPage` function. This
70+in this page using the `Text.Hakyll.Page.readPage` function. This will return a
67−will return a `Page`, which is actually just a `Map String ByteString`. In
71+`Page`, which is actually just a `Map String ByteString`. In this example, the
68−this example, the map would consist of the following key-value pairs:
72+map would consist of the following key-value pairs:
70- `author`: `Jasper Van der Jeugt`
74- `author`: `Jasper Van der Jeugt`
71- `title`: `A sample markdown post`
75- `title`: `A sample markdown post`
@@ -79,7 +83,7 @@ In hakyll, there is a strict separation between pages and templates. Templates,
79for example, cannot contain metadata.
83for example, cannot contain metadata.
81 <h2> $title </h2>
85 <h2> $title </h2>
82− by <strong> $author </title>
86+ by <strong> $author </strong>
@@ -87,16 +91,18 @@ Templates are rendered using the Haskell `Text.Template` library. This means
87that in your template, you can use `$identifier`, and it will be replaced by
91that in your template, you can use `$identifier`, and it will be replaced by
88the value of `identifier`.
92the value of `identifier`.
90−With this template we could, for example, render the file we saw in the previous
94+With this template we could, for example, render the file we saw in the
91−section. It would go like this:
95+previous section. It would go like this:
93 page <- readPage "text.markdown"
97 page <- readPage "text.markdown"
94− render <- renderPage "templates/sample.html" page
98+ rendered <- render "templates/sample.html" page
95−
99+ writePage rendered
96−Now, `render` will be a `Page` containing all metadata from `page`, but the
100+
97−`body` key would be replaced by the substitution. This means we can combine
101+This reads in `text.markdown`, renders it and writes it to the site destination
102+(`_site/text.html`). The result of a `render` action is an `IO Page`, the
103+metadata will be copied from the original page, and the body will be replaced by
104+the rendering result. This means we can combine rendering actions. Given another
98−rendering actions. Given another template `templates/default.html`:
105+template `templates/default.html`:
102 <title> $title </title>
108 <title> $title </title>
@@ -106,29 +112,39 @@ rendering actions. Given another template `templates/default.html`:
109−We can now combine the rendering actions:
115+We can now combine the rendering actions (I use `>>=` notation here):
116+ readPage "text.markdown" >>=
117+ render "templates/sample.html" >>=
118+ render "templates/default.html" >>=
121+Jolly good fun and all that, but you can imagine that when we render over nine
122+thousand posts, our generator will be busy for a while. That's why we have the
123+`depends` function. It takes a url and a list of dependencies as arguments, and
124+an IO action. The trick is that this IO action will only be executed if any of
125+the dependencies is newer than the url given. In our example, we would write:
127+ depends "text.html" ["text.markdown", "templates/sample.html", "templates/default.html"]
111− page <- readPage "text.markdown"
128+ (readPage "text.markdown" >>= render "templates/sample.html" >>=
112− render <- (renderPage "templates/sample.html" page >>=
113− renderPage "templates/default.html")
129+ render "templates/default.html" >>= writePage)
115−Of course, you can't really do anything with the render if you don't write it
131+Not exactly the prettiest code I've ever seen. Because rendering a page with a
116−to a file somewhere. That's why the function `renderAndWrite` exists:
132+number of templates is very common, there's a `renderChain` function to do this
133+for us. The above can be replaced by
135+ renderChain ["templates/sample.html", "templates/default.html"] $
118− readPage "text.markdown" >>=
136+ createPagePath "text.markdown"
119− renderPage "templates/sample.html" page >>=
120− renderAndWrite "templates/default.html"
122−Now, where will this file be written? In `_site/text.html`, of course! That's
138+The `renderChain` function will automatically check dependencies and write the
123−because the page still contains a key called `url`, which the renderAndWrite
139+page. In fact, it is recommended that you __always__ use this, even when there's
124−function uses to determine the file destination.
140+only one template in the chain.
126## More advanced things
142## More advanced things
128Sometimes, you want to create a `Page` from scratch, without reading from a
144Sometimes, you want to create a `Page` from scratch, without reading from a
129file. There are functions to do that for you, and I suggest you read the
145file. There are functions to do that for you, and I suggest you read the
130−documentation of `Text.Hakyll.Page`. As a more advanced example, I will
146+documentation of `Text.Hakyll.Page`. As a more advanced example, I will explain
131−explain the RSS system I wrote for my website.
147+the RSS system I wrote for my website.
133 |-- generate.hs
149 |-- generate.hs
134 |-- posts
150 |-- posts
@@ -137,29 +153,30 @@ explain the RSS system I wrote for my website.
137 |-- rss.xml
153 |-- rss.xml
138 `-- rssitem.xml
154 `-- rssitem.xml
140−Our post contains some metadata:
156+Our post contains some metadata:</p
143 title: A first post
159 title: A first post
144 date: December 2, 2009
160 date: December 2, 2009
161+ about: A post describing the why and the how of the technical setup of this blog.
147 # A first post
164 # A first post
149 A first post describing the technical setup of this blog, for that is
166 A first post describing the technical setup of this blog, for that is
151−The `templates/rssitem.xml` file is a template for rendering one post to an
168+The `templates/rssitem.xml` file is a template for rendering one post to an rss
155− <title>$title</title>
172+ <title> $title </title>
156 <link>http://jaspervdj.be/$url</link>
173 <link>http://jaspervdj.be/$url</link>
157− <description>New blogpost: $title</description>
174+ <description> $about </description>
160Now a template for rendering the whole rss feed, `templates/rss.xml`:
177Now a template for rendering the whole rss feed, `templates/rss.xml`:
162− <?xml version="1.0" ?>
179+ <?xml version="1.0"?>
163 <rss version="2.0">
180 <rss version="2.0">
164 <channel>
181 <channel>
165 <title>jaspervdj - a personal blog</title>
182 <title>jaspervdj - a personal blog</title>
@@ -169,16 +186,39 @@ Now a template for rendering the whole rss feed, `templates/rss.xml`:
169 </channel>
186 </channel>
172−Alright, let's get coding.
189+Alright, let's get coding. We first want a list of all posts, sorted so the most
190+recent entries are first in the list.
174− -- Find all posts paths.
175 postPaths <- liftM (L.reverse . L.sort) $ getRecursiveContents "posts"
192 postPaths <- liftM (L.reverse . L.sort) $ getRecursiveContents "posts"
176− -- Read and render all posts with the rssitem.xml template
193+
177− -- Also, only render 5 posts.
194+Note that this sorting works because the posts have a
178− pages <- mapM readPage (take 5 postPaths)
195+`yyyy-mm-dd-title.extension` naming scheme. We want the paths as `PagePath` and
179− items <- mapM (renderPage "templates/rssitem.xml") pages
196+not as `FilePath` or `String`, because `PagePath` is an instance of
180− -- Render the result
197+`Renderable`. Also, we only want the 5 most recent posts.
181− renderAndWrite "templates/rss.xml" $ pageFromList [ ("items", concatPages items),
198+
182− ("url", "rss.xml") ]
199+ let renderablePosts = map createPagePath postPaths
183−
200+ let recentItems = renderAndConcat "templates/postitem.html" $ take 5 renderablePosts
202+The `renderAndConcat` function takes a template and a list of `Renderable`
203+items. It renders all renderables with the given template, and concatenates the
204+result. Note that the concating and reading of pages is not executed yet,
205+because of laziness. This helps us, since we want to use the modification
206+timestamps of files, and not render everything every time.
208+let rssPage = createCustomPage "rss.xml"
209+ ("templates/rssitem.xml" : postPaths) [("items", Right recentItems)]
211+We now created the custom rss page. The `createCustomPage` is a function that
212+produces a `CustomPage`, which is an instance of `Renderable`. `"rss.xml"` is
213+our destination url. We then give a list of extra dependencies that were used
214+to generate the custom page, so Hakyll can check modification stamps. The last
215+argument is the key-value mapping of our `CustomPage`. Note that the type for
216+values is `Either String (IO ByteString)`. So, we can either give a simple
217+string, or an IO action that results in a `ByteString`. The benefit of this is
218+that the IO action will not be executed if the page in `_site/` is already
219+up-to-date. Now, we only need to render it using our rss template.
221+ renderChain ["templates/rss.xml"] rssPage
184−That's that. Now we have a nice rss feed.
223+That's it. Now we have a rss feed that is generated only when it is not already