Wagtailでページネーションを実装する

Wagtailで複数のPageを異なるページに分割して表示する方法を解説します。

Wagtailでページネーションを実装する

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 }}">&laquo;</a>
    {% endif %}

    <!-- 現在のページ番号 / 全体のページ数 -->
    <span>{{ contents.number }} / {{ contents.paginator.num_pages }}</span>

    <!-- 次のページがあればリンクを表示 -->
    {% if contents.has_next %}
        <a href="?page={{ contents.next_page_number }}">&raquo;</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パラメータをつけることを忘れないでください。