on master help sign in
add tutorial on conditional page generation using arrows
b41482fFlorian Hars
web/tutorials/03-arrows.markdownnew file+223
@@ -0,0 +1,223 @@
1+---
2+title: Arrow Magic: Metadata Dependent Page Generation
3+author: Florian Hars
4+---
5+
6+## Supporting a "published: false" attribute on pages
7+
8+Many content management systems or blog platforms support
9+some kind of workflow that display articles differently or
10+not at all depending on which state the article is in, for
11+example whether it has a "published" attribute or not.
12+Hakyll has no built-in support for anything like this, but since
13+its compilers are just arrows, it is easy to implement arbitrary
14+metadata dependent behaviour for rendering pages.
15+
16+Let's start by adding support for a "published" attributes to the
17+`simpleblog` example. We want to consider a blog post published if it
18+has a `published` metadata element that does not have the value
19+`false`. A function to test for this is simple:
20+
21+~~~~~{.haskell}
22+isPublished :: Page a -> Bool
23+isPublished p =
24+ let published = getField "published" p in
25+ published /= "" && published /= "false"
26+~~~~~
27+
28+The next step is to write a function that tags a page with its
29+published status, which can be either unpublished or published, using
30+the standard `Either` datatype and then transform this function
31+into a `Compiler`. The latter can be done with the standard `arr`
32+function from `Control.Arrow`, which lifts a function into an arrow:
33+
34+~~~~~{.haskell}
35+isPagePublished :: Compiler (Page a) (Either (Page a) (Page a))
36+isPagePublished = arr (\p -> if isPublished p then Right p else Left p)
37+~~~~~
38+
39+For the next processing steps we now need a compiler that takes an
40+`Either (Page a) (Page a)` instead of the usual `Page a` as an
41+input. But the former can be built up from the latter using some
42+standard combinators from the `Control.Arrow` library. The simplest
43+one is `|||`, which takes two comilers (arrows) with the same output
44+type and returns a new compiler that takes an `Either` of the input
45+types of the Compilers as an input. Maybe we just want to render our
46+unpublished posts with a big warning that they are provisional, so we
47+just want to render the unpublished `Left` pages with another template
48+than the published `Right` pages:
49+
50+~~~~~{.haskell}
51+ match "posts/*" $ do
52+ route $ setExtension ".html"
53+ compile $ pageCompiler
54+ >>> isPagePublished
55+ >>> (applyTemplateCompiler "templates/embargo-post.html"
56+ ||| applyTemplateCompiler "templates/post.html")
57+ >>> applyTemplateCompiler "templates/default.html"
58+ >>> relativizeUrlsCompiler
59+~~~~~
60+
61+With the conditional rendering in place, the next step is to hide
62+the unpublished posts from the homepage and the list of posts.
63+Both lists are generated from the results of a requireAllA call.
64+The last argument of requireAllA is a Compiler, and requireAllA
65+passes a pair consiting of the currently rendered page and a list
66+of all the required pages. All we have to do to suppress the pages
67+is to write a Compiler that takes such a pair as input, leaves the
68+first element of the pair unchanged and filters out all the unpublished
69+pages from list in the second element of the pair and then pass the
70+output from this compiler to the existing compilers handling the
71+list generation for the `index` and `posts` pages.
72+
73+Again, we can use a function from `Control.Arrow` to build this
74+compiler from simpler ones, in this case it is `***`, which combines
75+two arrows to one arrow from pairs to pairs. For our purposes, we
76+combine the identity arrow, which leaves its input unchanged, and an
77+ordinary `filter` on a list lifted into the compiler arrow:
78+
79+~~~~~{.haskell}
80+filterPublished :: Compiler (Page a, [Page b]) (Page a, [Page b])
81+filterPublished = id *** arr (filter isPublished)
82+~~~~~
83+
84+All that remains to do is to chain this compiler in front of the existing
85+compilers passed to requireAllA in the code for `posts.html`
86+
87+~~~~~{.haskell}
88+ >>> requireAllA "posts/*" (filterPublished >>> addPostList)
89+~~~~~
90+
91+and for `index.html`:
92+
93+~~~~~{.haskell}
94+ >>> requireAllA "posts/*"
95+ (filterPublished
96+ >>> (id *** arr (take 3 . reverse . sortByBaseName))
97+ >>> addPostList)
98+~~~~~
99+
100+You may have noticed that the code for the index page uses the same
101+`id *** something` construct to extract some elements from the list
102+of all posts.
103+
104+## Don't generate unpublished pages at all
105+
106+The above code will treat unpublished posts differently and hide them
107+from all lists of posts, but they will still be generated, and someone
108+who knows their URLs will still be able to access them. That may be
109+what you need, but sometimes you might want to suppress them
110+completely. The simplest way to do so is to leave the rendering
111+pipeline for `"posts/*"` unchanged and just add the `isPagePublished`
112+compiler at the end. This will not compile, since hakyll knows how to
113+write a `Page String`, but not how to write an `Either (Page String)
114+(Page String)`. But that can be amended by a simple type class
115+declaration:
116+
117+~~~~~{.haskell}
118+instance Writable b => Writable (Either a b) where
119+ write p (Right b) = write p b
120+ write _ _ = return ()
121+~~~~~
122+
123+Now hakyll will happily generate published pages and ignore
124+unpublished ones. This solution is of course slightly wasteful, as at
125+will apply all the templates to an unpublished page before finally
126+discarding it. You can avoid this by using the `+++` function, which
127+does for the sum datatype `Either` what `***` does for the product
128+type pair:
129+
130+~~~~~{.haskell}
131+ match "posts/*" $ do
132+ route $ setExtension ".html"
133+ compile $ pageCompiler
134+ >>> isPublishedPage
135+ >>> (id +++ (applyTemplateCompiler "templates/post.html"
136+ >>> applyTemplateCompiler "templates/default.html"
137+ >>> relativizeUrlsCompiler))
138+~~~~~
139+
140+The other problem with this solution is more severe: hakyll will no
141+longer generate the index and posts pages due to a rare problem in
142+haskell land: a runtime type error. Hakyll tries to be smart and reuse
143+the parsed pages from the `match "posts/*"` when processing the
144+`requireAllA "posts/*"` calls by caching them. But the compilers there
145+still expect a list of pages instead of a list of eithers, so we have
146+to replace `filterPublised` with something that works on the
147+latter. Luckily (or, probably, by design), `Data.Either` provides just
148+the function we need, so the new filtering compiler is actually
149+shorter that the original, even though it has a more intimidating
150+type:
151+
152+~~~~~{.haskell}
153+filterPublishedE :: Compiler (Page a, [Either (Page b) (Page b)]) (Page a, [Page b])
154+filterPublishedE = id *** arr rights
155+~~~~~
156+
157+## Timed releases
158+
159+Exploiting the fact that compilers are arrows, we can do more mixing
160+and matching of compilers to further refine how hakyll deals with page
161+attributes like `published`. Maybe you want `cron` to update your blog
162+while you are on vacation, so you want posts to be considered
163+published if the `published` field is either `true` or a time in the
164+past.
165+
166+If you happen to live in the UK in winter or enjoy to do time zone
167+calculation in your head, your new function to test if a page is
168+published and the compiler derived from it might then look like this
169+
170+~~~~~{.haskell}
171+isPublishedYet :: Page a -> UTCTime -> Bool
172+isPublishedYet page time =
173+ let published = getField "published" page in
174+ published == "true" || after published
175+ where
176+ after published =
177+ let publishAt = parseTime defaultTimeLocale "%Y-%m-%d %H:%M" published in
178+ fromMaybe False (fmap (\embargo -> embargo < time) publishAt)
179+
180+isPagePublishedYet :: Compiler (Page a, UTCTime) (Either (Page a) (Page a))
181+isPagePublishedYet = arr (\(p,t) -> if isPublishedYet p t then Right p else Left p)
182+~~~~~
183+
184+This compiler has a pair of a page and a time as its input, and we can
185+use yet another function from `Control.Arrow` to construct a compiler
186+that generates the input for it, the function `&&&`. It takes two
187+compilers (arrows) with the same input type and constructs a compiler
188+from that type to a pair of the output types of the two compilers.
189+For the first argument we take the `pageCompiler` which we already
190+call at the beginning of the page compilation. The second argument
191+should be a compiler with the same input type as `pageCompiler` that
192+returns the current time. But the current time lives in the IO monad
193+and does not at all depend on the resource the current page is
194+generated from, so we have to cheat a little bit by calling
195+`unsafeCompiler` with a function that ignores its argument and returns
196+an `IO UTCTime`, which `unsafeCompiler` will unwrap for us:
197+
198+~~~~~{.haskell}
199+ match "posts/*" $ do
200+ route $ setExtension ".html"
201+ compile $ (pageCompiler &&& (unsafeCompiler (\_ -> getCurrentTime)))
202+ >>> isPagePublishedYet
203+ >>> (id +++ ( ... as above ...))
204+~~~~~
205+
206+This is all we have to change if we don't generate unpublished pages
207+at all. If we just hide them from the lists, the call to `|||`
208+discards the information that a page is not (yet) published that was
209+encoded in the `Either`. In that case we could use the `setField`
210+function from `Hakyll.Web.Page.Metadata` to rewrite the `published`
211+field of the left and right pages in `isPagePublished(Yet)` to
212+canonical values that the original `isPublished` function called from
213+`filterPublished` understands:
214+
215+~~~~~{.haskell}
216+isPagePublishedYet = arr (\(p,t) -> if isPublishedYet p t then pub p else unpub p)
217+ where
218+ pub p = Right $ setField "published" "true" p
219+ unpub p = Left $ setField "published" "false" p
220+~~~~~
221+
222+The final version of this code can be found in the timedblog example,
223+together with the required `import` statements.