普通のブログには、各記事の最初か最後に、前後の記事へのリンクが設置されていると思います。
しかし、(Haskellerの)ブログに使われることが多いと思われる静的サイトジェネレーターHakyllのデフォルトでは、前後の記事へのリンクが設置されません。Hakyll自身のチュートリアルにも、前後の記事へのリンクはありません。
個々の記事の内容が独立しているような静的サイトなら前後に辿れなくてもいいかもしれませんが、内容が連続している場合(チュートリアルなど)はそういうリンクが欲しいところです。
Hakyllには前後の繋がりを持ったページの列を作るために Hakyll.Web.Paginate というモジュールが用意されていますが、これは個々の記事に対してというよりも、「記事 k 個ごとの一覧(アーカイブ)」を作成することを想定したもののように見えます。
まあでも頑張ったらできました。
site.hs: ビフォー
hakyll-init で吐かせたデフォルトの site.hs では次のようになっているところを、
main = hakyll $ do
...略...
match "posts/*" $ do
route $ setExtension "html"
compile $ pandocCompiler
>>= loadAndApplyTemplate "templates/post.html" postCtx
>>= loadAndApplyTemplate "templates/default.html" postCtx
>>= relativizeUrls
site.hs: アフター
次のように書き換えます:
main = hakyll $ do
...略...
-- 記事一覧を取得
postIDs <- sortChronological =<< getMatches "posts/*"
let makeId pageNum = postIDs !! (pageNum - 1)
grouper items = return (map (:[]) items) -- one item per group
pageTitle :: Int -> Compiler String
pageTitle n | 1 <= n && n <= length postIDs = do
mtitle <- getMetadataField (makeId n) "title"
case mtitle of
Just title -> return title
Nothing -> fail "no 'title' field"
| otherwise = fail "unavailable"
pag <- buildPaginateWith grouper "posts/*" makeId
paginateRules pag $ \pageNum pattern -> do
let ctx = mconcat [ field "previousPageTitle" (\_ -> pageTitle (pageNum - 1))
, field "nextPageTitle" (\_ -> pageTitle (pageNum + 1))
, paginateContext pag pageNum
, postCtx
]
-- あとはいつも通り(ただしテンプレートに与える Context を変える)
route $ setExtension "html"
compile $ pandocCompiler
>>= loadAndApplyTemplate "templates/post.html" ctx
>>= loadAndApplyTemplate "templates/default.html" ctx
>>= relativizeUrls
前後の記事のタイトルに相当するフィールドは用意されていないので、自分で実装してやります。
「記事 k 個ごとのアーカイブ」を作る場合は pageNum
引数を元に、アーカイブのURL /archives/NN/
を組み立てるのかもしれませんが、今回の使い方ではページ番号を数値として使っていません。
site.hs: アフター(別解)
Hakyll.Web.Paginate を使わない例は以下のようになります:
main = hakyll $ do
...略...
-- 記事一覧を取得
postIDs <- sortChronological =<< getMatches "posts/*"
let nextPosts = tail $ map Just postIDs ++ [Nothing]
prevPosts = Nothing : map Just postIDs
pageTitle, pageUrl :: Identifier -> Compiler String
-- 記事IDに対する 'title' 属性を取得する
pageTitle i = do
mtitle <- getMetadataField i "title"
case mtitle of
Just title -> return title
Nothing -> fail "no 'title' field"
-- 記事IDに対するURLを取得する
pageUrl i = do
mfilePath <- getRoute i
case mfilePath of
Just filePath -> return (toUrl filePath)
Nothing -> fail "no route"
-- (記事のID,次の記事のID,前の記事のID) の3つ組について処理を行う
forM_ (zip3 postIDs nextPosts prevPosts) $
\(postID,mnextPost,mprevPost) -> create [postID] $ do
-- 以下、テンプレートの中で変数 previousPageUrl, previousPageTitle, nextPageUrl, nextPageTitle を使えるようにするための定義
let prevPageCtx = case mprevPost of
Just i -> field "previousPageUrl" (\_ -> pageUrl i) `mappend`
field "previousPageTitle" (\_ -> pageTitle i)
_ -> mempty
nextPageCtx = case mnextPost of
Just i -> field "nextPageUrl" (\_ -> pageUrl i) `mappend`
field "nextPageTitle" (\_ -> pageTitle i)
_ -> mempty
ctx = prevPageCtx `mappend` nextPageCtx `mappend` postCtx
-- 以下同じなので省略
Paginate を使った実装で "posts/*"
を2回書いていたり、リストにインデックスでアクセスしているのがイケてない、と思う方はこっちを使うといいと思いますが、正直どうでもいいです。
関数化
main 関数の中にこれらの処理を書いてしまうと見通しが悪いので、関数に分けてみました。
makeSeries :: [Identifier] -> (Context String -> Rules ()) -> Rules ()
makeSeries postIDs rule = do
let nextPosts = tail $ map Just postIDs ++ [Nothing] :: [Maybe Identifier]
prevPosts = Nothing : map Just postIDs :: [Maybe Identifier]
forM_ (zip3 postIDs nextPosts prevPosts) $ \(postID,mnextPost,mprevPost) -> create [postID] $ do
let siblingCtx = mconcat $ catMaybes [(field "previousPageUrl" . pageUrlOf) <$> mprevPost
,(field "previousPageTitle" . pageTitleOf) <$> mprevPost
,(field "nextPageUrl" . pageUrlOf) <$> mnextPost
,(field "nextPageTitle" . pageTitleOf) <$> mnextPost
]
rule siblingCtx
where
pageTitleOf, pageUrlOf :: Identifier -> Item a -> Compiler String
pageTitleOf i _item = do
mtitle <- getMetadataField i "title"
case mtitle of
Just title -> return title
Nothing -> fail "no 'title' field"
pageUrlOf i _item = do
mfilePath <- getRoute i
case mfilePath of
Just filePath -> return (toUrl filePath)
Nothing -> fail "no route"
使用例は
main = hakyll $ do
postIDs <- sortChronological =<< getMatches "posts/*"
makeSeries postIDs $ \siblingCtx -> do
route $ setExtension "html"
let ctx = siblingCtx `mappend` postCtx
compile ...
です。
テンプレートでの使用例
使い方としては、投稿用のテンプレートに
$if(previousPageUrl)$
<a href="$previousPageUrl$">前の記事</a>
$endif$
$if(nextPageUrl)$
<a href="$nextPageUrl$">次の記事</a>
$endif$
または
$if(previousPageUrl)$
<a href="$previousPageUrl$">前:$previousPageTitle$</a>
$endif$
$if(nextPageUrl)$
<a href="$nextPageUrl$">次:$nextPageTitle$</a>
$endif$
と書き込んでやれば、それが前後の記事へのリンクになります。
普通のブログだと「前」「次」ではなく「古い」「新しい」の方がいいかもしれませんが、そこは適当にしてください。
おまけ
site.hs を何回もいじって編集する場合は、 stack build --fast
で最適化を無効にする、.cabalの ghc-options
に -dynamic
を指定して動的リンクさせるなどの措置をとると、 stack build
の時間を削減できて良いと思います。