Wagtailでページネーションを実装する
Wagtailで複数のPageを異なるページに分割して表示する方法を解説します。
ECサイトのように商品がたくさんあるページでは、コンテンツを複数のページに渡って表示していることが多いと思います。これはページネーションと呼ばれ、コンテンツが多い場合に複数のページに分けて表示することで、ユーザーの読みやすさの向上や読み込み速度の低下を防ぐ目的があります。
この記事ではWagtailのPageモデルを継承したページの一覧を想定し、そのページをページネーションで適切に分割する方法を解説します。
Djangoでのページネーションの基本
WagtailはDjangoをベースにしたCMSなので、ページネーションの実装もDjangoの実装方法に従う形になります。Djangoには簡単にページネーションを実現するためのPaginator
と呼ばれるクラスが実装されており、これを利用して大量にあるコンテンツを任意の数で分割していきます。
DjangoのPaginator
の基本は以下のとおりです。
# Paginatorのサンプル
from django.core.paginator import Paginator
# 簡易的に5つのコンテンツのリストを作成
contents = ["content_1", "content_2", "content_3", "content_4", "content_5"]
# contentsを1ページ2コンテンツに分割
paginator = Paginator(contents, 2)
# 含まれるページ数
print(paginator. num_pages) # 3
# 1ページ目を取得、0スタートではなく、1スタートなので注意
page1 = paginator.get_page(1) # <Page 1 of 3> DjangoのPageオブジェクト
Paginator
クラスには、分割したいリストと1ページあたりのコンテンツ数を指定します。コンテンツのリストがあったときに、それらを任意の数ごとにページに分割してくれます。get_page
メソッドは数字以外の引数があった場合には1ページ目を返し、マイナスや含まれるページ数より大きい値を入力された場合には最後のページを返すように設計されているので安全です。
注意点としては、Paginatorのリストのインデックスは1から始まるという点です。通常のリストは0からインデックスが始まるので間違えやすいですが、ページ数を指定する際にはこちらの方が直感的でわかりやすいかと思います。
WagtailのPageモデル内でコンテンツを分割する
続いて、WagtailのPage
モデルを継承したページの中で利用されるコンテンツをPaginator
を使って分割していきましょう。今回は、ブログの記事一覧ページを想定し、それらを1ページあたり20件の記事のリストを表示するように実装します。
WagtailのPage
モデルでは、そのページのテンプレートがレンダリングされるたびにget_context
というメソッドが呼び出されており、その中でテンプレート内で利用される変数のdictionary(辞書)が作成されています。たとえば、Pageモデルを継承したページのテンプレート内で{{ page.title }}
という変数を利用することができているのはget_context
のもともとの実装でpage
という変数にそのページのインスタンスを割り当てているからです。このget_context
メソッドをオーバーライドすることで、テンプレート内で使いたい変数を自由に設定することができます。
今回の例では、テンプレート内で表示するコンテンツのリストを、20件ずつ分割して取得し、contents
という変数でテンプレートに渡しましょう。
# blog/models.py
from django.core.paginator import Paginator
from wagtail.core.models import Page
class BlogListingPage(Page):
"""ブログ記事一覧ページ"""
template = "blog/blog_listing_page.html"
max_count = 1
subpage_types = ['blog.BlogDetailPage']
# テンプレートに渡す変数を設定する
def get_context(self, request, *args, **kwargs):
# デフォルトのcontextを取得
context = super().get_context(request, *args, **kwargs)
# 全ての公開済みのブログ記事を日付の降順で取得
all_contents = BlogDetailPage.objects.live().public().order_by('-date')
# Paginatorを作成、1ページ20件の記事
paginator = Paginator(all_contents, 20)
# URLからpageパラメータを取得、何番目のページかを判別
page_number = request.GET.get('page')
# 表示に必要なコンテンツを取得
contents = paginator.get_page(page_number)
# contextのdictionaryにcontentsを追加
# テンプレート内でcontentsという変数が使えるようになる
context["contents"] = contents
return context
ページネーションを実装する際に、何番目のページにいるのかを判別するためにURLにパラメータを付与します。たとえばexample.com/blogs/?page=3
のようなURLにアクセスすることで、ブログ記事一覧ページの3ページ目にアクセスしていることがわかります。このURLのpage
パラメータをrequest.GET.get('page')
で取得することで、そのページ番号に対応するコンテンツを取得できるようになります。
テンプレートの表示
Paginator
クラスのPage
オブジェクトの形でコンテンツのリストをテンプレートに渡すことができたので、これを適切にページネーションの形で表示していきます。対応するテンプレートに対して以下のような実装をしてください。機能の説明にフォーカスするため、スタイルや表示内容などは簡易的にしています。
<!-- blog/templates/blog/blog_listing_page.html -->
<!-- contentを20件表示 -->
{% for content in contents %}
{{ content.title }}
{{ content.specific.overview }} <!-- Pageモデルのサブクラスの属性にアクセスするにはspecificが必要 -->
{% endfor %}
<div class="pagination">
<!-- 前のページがあればリンクを表示 -->
{% if contents.has_previous %}
<a href="?page={{ contents.previous_page_number }}">«</a>
{% endif %}
<!-- 現在のページ番号 / 全体のページ数 -->
<span>{{ contents.number }} / {{ contents.paginator.num_pages }}</span>
<!-- 次のページがあればリンクを表示 -->
{% if contents.has_next %}
<a href="?page={{ contents.next_page_number }}">»</a>
{% endif %}
</div>
models.py
ファイルからcontents
という変数を渡しました。この変数には大きく二つ役割があります。
一つ目は、その変数自体を通常のモデルオブジェクトとして利用することです。取得したコンテンツの中身はそのままPageモデルとして利用できるため、コンテンツをリストとして表示するのであれば、for
文などでリストから個別の記事データを取得してtitle
などの属性を表示に利用することができます。
ここでの注意点は、Paginator
によって取得されるのはPage
オブジェクトであって、そのサブクラス(今回の場合だとBlogDetailPage
)ではないということです。そのため、サブクラスで実装した属性(上の例ではoverview)には直接アクセスすることができません。Pageオブジェクトからそのサブクラスにアクセスするためには、.specific
をつけるようにしましょう。
Paginator
を利用したcontents
変数の役割の二つ目は、ページネーションで必要になる情報へのアクセスです。ページネーションを実装する際には、現在のページは全体の何ページ目か、前のページや次のページは存在するのか、といった情報があると便利です。Paginator
によって作成されたオブジェクト(この例ではcontents
)には、これらの情報を簡単に取得できる属性がついています。
- has_previous: 前のページがあるかないかのBoolean
- has_next: 次のページがあるかないかのBoolean
- previous_page_number: 前のページ番号
- next_page_number: 次のページ番号
- number: 現在のページ番号
- paginator.num_pages: 全体のページ番号
この情報はテンプレート内で利用することができるので、条件に合わせて表示の実装をしていきます。前後のページにアクセスするためのURLに、ページ番号の情報であるpage
パラメータをつけることを忘れないでください。