Расстраничивание в Hakyll
Пара слов о том, как сделать разделение набора элементов (например постов) на страницы. Примером может являться, скажем, главная страничка этого блога.
Можно пойти путем джедая и пытаться писать что-то на коленке. Это сработает, но не факт, что стоит того. А можно воспользоваться модулем Hakyll.Web.Paginate
. Мы пойдем по второму пути.
Итак, что же нам нужно для того, чтобы сделать расстраничивание? В целом, совсем немного:
Во-первых,
buildPaginateWith ::
MonadMetadata m =>
Identifier] -> m [[Identifier]]) ->
([Pattern ->
PageNumber -> Identifier) ->
(Paginate m
Эта функция, собственно, строит расстраничивание. Первый аргумент – это функция, принимающая список идентификаторов и возвращающая список списков идентификаторов. Каждый список в возвращаемом списке – это одна страница. Второй аргумент – паттерн для выборки элементов, которые надо расстраничивать. Третий – функция-генератор идентификатора от номера страницы. Возвращает тип Paginate
. Все вычисления в монаде Rules
, Compiler
, etc.
В простых случаях, например, если нужно разобрать посты на странички по N
постов, есть функция
paginateEvery :: Int -> [a] -> [[a]]
Конечно, она очень простая, и не сортирует идентификаторы. Для сортировки есть другие функции, например
sortRecentFirst :: MonadMetadata m => [Identifier] -> m [Identifier]
определенная в Hakyll.Web.Template.List
. Здесь можно заметить некоторое несоответствие: paginateEvery
работает со списками, buildPaginateWith требует преобразование из списка в MonadMetadata
, а sortRecentFirst
возвращает monadMetadata
. В принципе do
-блок решает эту проблему, но я предпочитаю более идиоматический liftM
, определенный в Control.Monad
. Комбинируя эти функции, первый аргумент для buildPaginateWith
может выглядеть, например, так:
. sortRecentFirst liftM (paginateEvery postsPerPage)
где postsPerPage
имеет тип Int
и означает, собственно, сколько постов должно быть на странице. .
это композиция функций, она означает “применить функцию слева к результату функции справа”.
Кроме sortRecentFirst
есть sortChronological
, сортирующая записи в хронологическом порядке. И естественно, можно задать свою функцию сортировки.
Со вторым аргументом все должно быть в целом ясно: это может быть строка, например "posts/*"
, или более сложный Pattern
.
Третий аргумент должен генерировать идентификаторы для номеров страниц. Я, скажем, использую такой вариант:
let pagePath page | page==1 = fromFilePath "index.html"
| otherwise = fromFilePath $ "archive/page/"++
show (page::PageNumber)++".html"
fromFilePath
просто делает из строки Identifier
.
Собирая все вместе,
let pagePath page | page==1 = fromFilePath "index.html"
| otherwise = fromFilePath $ "archive/page/"++
show (page::PageNumber)++".html"
<- buildPaginateWith
archivePaginate .sortRecentFirst)
(liftM (paginateEvery postsPerPage)"posts/*" pagePath
Итак, у нас есть данные о расстраничивании в переменной archivePaginate
. Теперь нам нужно построить правила для генерации страниц, и получить доступ к контексту страницы. Для этого есть функции paginateRules
и paginateContext
соответственно.
paginateRules :: Paginate -> (PageNumber -> Pattern -> Rules ()) -> Rules ()
paginateContext :: Paginate -> PageNumber -> Context a
paginateRules
принимает результат buildPaginateWith
(без монады) и функцию двух аргументов: номера страницы и паттерна, содержащего идентификаторы, которые должны быть на этой странице. Возвращать эта функция должна Rules ()
, как и в случае с простым match
.
paginateContext
принимает результат buildPaginateWith
(без монады) и номер страницы.
Посмотрим, как это можно использовать:
$ \pageNum pattern -> do
paginateRules archivePaginate -- относительный url совпадает с идентификатором
route idRoute $ do
compile <- recentFirst =<< loadAll pattern -- загружаем посты для данной страницы
posts let
| pageNum==1 = "Главная"
title | otherwise = "Архив"
= -- конеткст
archiveCtx "posts" postCtx (return posts) `mappend` -- посты
listField "title" title `mappend` -- заголовок
constField `mappend` -- контекст страницы
paginateContext archivePaginate pageNum
defaultContext
""
makeItem >>= loadAndApplyTemplate "templates/default.html" archiveCtx
>>= relativizeUrls
$
изменяет порядок ассоциативности аргументов. В рамках обсуждения можно считать, что все, что идет после $
трактуется как один аргумент. По сути, это способ избежать скобочек. \... ->
объявляет анонимную функцию, в данном случае двух аргументов. Контекст составляется из постов, определяемых паттерном pattern
, заголовка, зависящего от номера страницы, и paginateContext
.
Что же содержит в себе paginateContext
?
firstPageNum
– номер первой страницы, если текущая страница – первая, то не задано.firstPageUrl
– url первой страницы, если текущая страница – первая, то не задано.previousPageNum
– номер предыдущей страницы, если текущая страница – первая, то не задано.previousPageUrl
– url предыдущей страницы, если текущая страница – первая, то не задано.nextPageNum
– номер следующей страницы, если текущая страница – последняя, то не задано.nextPageUrl
– url следующей страницы, если текущая страница – последняя, то не задано.lastPageNum
– номер последней страницы, если текущая страница – последняя, то не задано.lastPageUrl
– url последней страницы, если текущая страница – последняя, то не задано.currentPageNum
– номер текущей страницыcurrentPageUrl
– url текущей страницыnumPages
– общее количество страниц
Составить навигацию по страницам, используя эти переменные шаблона, достаточно просто:
<div>
$if(nextPageUrl)$<a href="$nextPageUrl$">
Раньше</a>
$endif$
$if(firstPageNum)$
Страница $currentPageNum$ из $numPages$
$endif$
$if(previousPageUrl)$<a href="$previousPageUrl$">
Позже</a>
$endif$</div>
Источники: