TL;DR
- Hakyll の
Rules
でroute
は省略できる- 省略すると compile されるが出力されない
- この挙動は何度も呼ばれる処理のプリコンパイルに応用できそう
- blog.myon.info のフッタ生成処理にこれを採用してビルドを高速化できた
- 全ページのフッタに最新の記事へのリンクなどを入れているため時間がかかっていた
route
なしの Rules
Hakyll の Rules
には、「どこに配置するのか」を指定する route
と「どう加工するか」を指定する compile
を記述します。このうち route
は、ドキュメントにもあるように省略することができ、省略した場合はファイルが出力されなくなります。
Finally, some special cases:
- If there is no route for an item, this item will not be routed, so it will not appear in your site directory.
- If an item matches multiple routes, the first rule will be chosen.
実際に次のコードで確認してみましょう。
#!/usr/bin/env stack
-- stack --resolver lts-12.6 script --package hakyll
{-# LANGUAGE OverloadedStrings #-}
import Hakyll
main :: IO ()
main = hakyll $ do
create ["hoge.txt"] $
compile $
makeItem ("にゃーん" :: String)
create ["fuga.txt"] $ do
route idRoute
compile $ do
hoge <- loadBody "hoge.txt"
makeItem $ ("Λ__Λ < " <> hoge :: String)
コードを実行したあと出力ディレクトリを確認すると、route
を指定した fuga.txt
のみが出力されているのがわかります。また出力された fuga.txt
の内容を確認してみると hoge.txt
の Rules
で指定した結果が表れており、hoge.txt
の compile
の処理はちゃんと実行されているのがわかります。
$ chmod +x site.hs
$ ./site.hs -v build
Initialising...
Creating store...
Creating provider...
Running rules...
Checking for out-of-date items
[DEBUG] fuga.txt is out-of-date because it is new
[DEBUG] hoge.txt is out-of-date because it is new
Compiling
[DEBUG] Processing fuga.txt
[DEBUG] Hakyll.Core.Compiler.Internal: Adding dependency: IdentifierDependency hoge.txt
[DEBUG] Require hoge.txt (snapshot _final): chasing
[DEBUG] Processing hoge.txt
updated hoge.txt
[DEBUG] Processing fuga.txt
[DEBUG] Require hoge.txt (snapshot _final): OK
[DEBUG] Processing fuga.txt
updated fuga.txt
[DEBUG] Routed to _site/fuga.txt
Success
[DEBUG] Removing tmp directory...
$ ls _site/
fuga.txt
$ cat _site/fuga.txt
Λ__Λ < にゃーん
この挙動はうまく利用するといろいろなことができそうです。今回は何度も呼ばれる処理のプリコンパイルに使ってみた例を紹介したいと思います。
全ページに最新記事へのリンクを貼りたい
Hakyll で生成する全てのページに最新記事数件へのリンクを入れたいとします。ちょうどこのブログのフッタのような感じですね。
例として Hakyll のサンプルプロジェクトでこれをやってみます。stack でプロジェクトを作成し、
$ stack --resolver lts-12.6 new site hakyll-template
デフォルトテンプレートをこんな感じに変更、
diff --git a/templates/default.html b/templates/default.html
index cd20808..980bb9b 100644
--- a/templates/default.html
+++ b/templates/default.html
@@ -25,6 +25,18 @@
$body$
</div>
+
+ <div id="recent-posts">
+ <h2>Recent posts</h2>
+ <ul>
+ $for(recent-posts)$
+ <li>
+ <a href="$url$">$title$</a> - $date$
+ </li>
+ $endfor$
+ </ul>
+ </div>
+
<div id="footer">
Site proudly generated by
<a href="http://jaspervdj.be/hakyll">Hakyll</a>
最後に site.hs
で最新記事の情報を Context
に入れてやります。loadAll
を使うと posts/*
などで依存関係のエラーが出てしまう1ので、snapshot を作成してそれを利用するようにします。
diff --git a/site.hs b/site.hs
index 1214769..53650a8 100644
--- a/site.hs
+++ b/site.hs
@@ -17,16 +17,28 @@ main = hakyll $ do
match (fromList ["about.rst", "contact.markdown"]) $ do
route $ setExtension "html"
- compile $ pandocCompiler
- >>= loadAndApplyTemplate "templates/default.html" defaultContext
- >>= relativizeUrls
+ compile $ do
+ recent <- fmap (take 5) . recentFirst =<< loadAllSnapshots "posts/*" "content"
+ let ctx = listField "recent-posts" postCtx (return recent) `mappend`
+ defaultContext
+
+ pandocCompiler
+ >>= loadAndApplyTemplate "templates/default.html" ctx
+ >>= relativizeUrls
match "posts/*" $ do
route $ setExtension "html"
- compile $ pandocCompiler
- >>= loadAndApplyTemplate "templates/post.html" postCtx
- >>= loadAndApplyTemplate "templates/default.html" postCtx
- >>= relativizeUrls
+ compile $ do
+ content <- pandocCompiler
+ >>= saveSnapshot "content"
+
+ recent <- fmap (take 5) . recentFirst =<< loadAllSnapshots "posts/*" "content"
+ let ctx = listField "recent-posts" postCtx (return recent) `mappend`
+ postCtx
+
+ loadAndApplyTemplate "templates/post.html" postCtx content
+ >>= loadAndApplyTemplate "templates/default.html" ctx
+ >>= relativizeUrls
create ["archive.html"] $ do
route idRoute
@@ -37,9 +49,13 @@ main = hakyll $ do
constField "title" "Archives" `mappend`
defaultContext
+ recent <- fmap (take 5) . recentFirst =<< loadAllSnapshots "posts/*" "content"
+ let ctx = listField "recent-posts" postCtx (return recent) `mappend`
+ archiveCtx
+
makeItem ""
>>= loadAndApplyTemplate "templates/archive.html" archiveCtx
- >>= loadAndApplyTemplate "templates/default.html" archiveCtx
+ >>= loadAndApplyTemplate "templates/default.html" ctx
>>= relativizeUrls
@@ -52,9 +68,13 @@ main = hakyll $ do
constField "title" "Home" `mappend`
defaultContext
+ recent <- fmap (take 5) . recentFirst =<< loadAllSnapshots "posts/*" "content"
+ let ctx = listField "recent-posts" postCtx (return recent) `mappend`
+ indexCtx
+
getResourceBody
>>= applyAsTemplate indexCtx
- >>= loadAndApplyTemplate "templates/default.html" indexCtx
+ >>= loadAndApplyTemplate "templates/default.html" ctx
>>= relativizeUrls
match "templates/*" $ compile templateCompiler
これでとりあえずの目的は達成できました。(この図は雑に記事を増やしたあとにキャプチャしたものです)
遅い!!!
先程のコードのまま記事数を増やしてみます。すると・・・
$ for y in {2016..2050}; do
for m in {01..12}; do
cp posts/{2015-08,$y-$m}-23-example.markdown
done
done
$ time stack exec site rebuild
Removing _site...
Removing _cache...
Removing _cache/tmp...
Initialising...
Creating store...
Creating provider...
Running rules...
Checking for out-of-date items
Compiling
updated templates/default.html
updated about.rst
updated templates/post.html
updated posts/2015-08-23-example.markdown
updated posts/2016-01-23-example.markdown
...
updated templates/post-list.html
updated archive.html
updated contact.markdown
updated css/default.css
updated index.html
Success
stack exec site rebuild 89.15s user 5.75s system 103% cpu 1:31.86 total
ビルドにめちゃくちゃ時間がかかるようになってしまいました。もちろん生成されるページ数が増えたというのもありますが、変更前はログがバッと流れていたものが、1行毎に一瞬止まるようになってしまいました。ページ生成毎に全ての記事情報を読み込んで並べ替えて…なんてやっているので仕方ないですが、やっぱりなんとかしたいところですね。
route
なし Rules
を使ったプリコンパイル
最新記事を列挙する処理は各ページ固有の情報に依存しないので、全てのページで同じ結果になるはずです。ということは、あらかじめ最新記事リストを生成しておいて、各ページ生成時にその結果を呼び出すようにすれば高速化できそうです。これを route
なし Rules
を使って実装してみます。
まず、最新記事リストのテンプレート templates/recent-posts.html
を作成します。
<div id="recent-posts">
<h2>Recent posts</h2>
<ul>
$for(recent-posts)$
<li>
<a href="$url$">$title$</a> - $date$
</li>
$endfor$
</ul>
</div>
このテンプレートを使って、recent-posts.html
に最新記事リストを生成するようにします。このファイルはサイトを公開する際には必要ないので、route
は記述しません。
diff --git a/site.hs b/site.hs
index 53650a8..2d738ab 100644
--- a/site.hs
+++ b/site.hs
@@ -77,6 +77,14 @@ main = hakyll $ do
>>= loadAndApplyTemplate "templates/default.html" ctx
>>= relativizeUrls
+ create ["recent-posts.html"] $
+ compile $ do
+ recent <- fmap (take 5) . recentFirst =<< loadAllSnapshots "posts/*" "content"
+ let ctx = listField "recent-posts" postCtx (return recent) `mappend`
+ defaultContext
+ makeItem ""
+ >>= loadAndApplyTemplate "templates/recent-posts.html" ctx
+
match "templates/*" $ compile templateCompiler
そして、最新記事を毎回列挙するかわりに recent-posts.html
を loadBody
するようにします。今回は読み込んだ最新記事リストを recent-list
という Context
でテンプレート側に渡すことにしました。
diff --git a/site.hs b/site.hs
index 2d738ab..7a9d26a 100644
--- a/site.hs
+++ b/site.hs
@@ -18,8 +18,8 @@ main = hakyll $ do
match (fromList ["about.rst", "contact.markdown"]) $ do
route $ setExtension "html"
compile $ do
- recent <- fmap (take 5) . recentFirst =<< loadAllSnapshots "posts/*" "content"
- let ctx = listField "recent-posts" postCtx (return recent) `mappend`
+ recent <- loadBody "recent-posts.html"
+ let ctx = constField "recent-list" recent `mappend`
defaultContext
pandocCompiler
@@ -32,8 +32,8 @@ main = hakyll $ do
content <- pandocCompiler
>>= saveSnapshot "content"
- recent <- fmap (take 5) . recentFirst =<< loadAllSnapshots "posts/*" "content"
- let ctx = listField "recent-posts" postCtx (return recent) `mappend`
+ recent <- loadBody "recent-posts.html"
+ let ctx = constField "recent-list" recent `mappend`
postCtx
loadAndApplyTemplate "templates/post.html" postCtx content
@@ -49,8 +49,8 @@ main = hakyll $ do
constField "title" "Archives" `mappend`
defaultContext
- recent <- fmap (take 5) . recentFirst =<< loadAllSnapshots "posts/*" "content"
- let ctx = listField "recent-posts" postCtx (return recent) `mappend`
+ recent <- loadBody "recent-posts.html"
+ let ctx = constField "recent-list" recent `mappend`
archiveCtx
makeItem ""
@@ -68,8 +68,8 @@ main = hakyll $ do
constField "title" "Home" `mappend`
defaultContext
- recent <- fmap (take 5) . recentFirst =<< loadAllSnapshots "posts/*" "content"
- let ctx = listField "recent-posts" postCtx (return recent) `mappend`
+ recent <- loadBody "recent-posts.html"
+ let ctx = constField "recent-list" recent `mappend`
indexCtx
getResourceBody
最後に、templates/default.html
に追加した最新記事リスト生成部分を $recent-list$
に変更します。
diff --git a/templates/default.html b/templates/default.html
index 980bb9b..0963027 100644
--- a/templates/default.html
+++ b/templates/default.html
@@ -26,16 +26,9 @@
$body$
</div>
- <div id="recent-posts">
- <h2>Recent posts</h2>
- <ul>
- $for(recent-posts)$
- <li>
- <a href="$url$">$title$</a> - $date$
- </li>
- $endfor$
- </ul>
- </div>
+ $if(recent-list)$
+ $recent-list$
+ $endif$
<div id="footer">
Site proudly generated by
これで再度ビルドしてみます。すると・・・
$ time stack exec site rebuild
Removing _site...
Removing _cache...
Removing _cache/tmp...
Initialising...
Creating store...
Creating provider...
Running rules...
Checking for out-of-date items
Compiling
updated templates/recent-posts.html
updated recent-posts.html
updated templates/default.html
updated about.rst
updated templates/post.html
updated posts/2015-08-23-example.markdown
updated posts/2016-01-23-example.markdown
...
updated templates/post-list.html
updated archive.html
updated contact.markdown
updated css/default.css
updated index.html
Success
stack exec site rebuild 3.93s user 0.31s system 103% cpu 4.098 total
めちゃくちゃ速くなりました ∩(>◡<*)∩
まとめ
route
なしの Rules
の挙動を使ってプリコンパイルのようなことを行い、ビルドを高速化する方法を紹介しました。blog.myon.info では、この方法でフッタのプリコンパイルをするようにしたことで、1分半程度掛かっていたビルドが40秒ほどで済むようになりました。
route
なしの Rules
の活用法はまだたくさんありそうです。Extra Dependencies in Hakyll - Blaenk Denum では、scss のような複数のファイルから1つのファイルを生成したいというケースを Extra Dependencies を使って実現する例を紹介していますが、ここでも活躍しています。
ということで、Hakyll 便利なのでみんな使いましょう!
Footnotes
-
自身が自身に依存してしまうので ↩