Next.js でのページネーション実装

このポートフォリオサイト(shu-web-portfolio)の制作実績一覧(works)で実装した方法をベースにまとめる。全220件のブログ記事一覧にも同じ考え方を使う予定。

前提:静的サイト(output: export)ではページネーションの作り方が変わる

一般的なWebアプリ(Vercelの動的SSRやAPIサーバーがある構成)なら、クエリパラメータ(例:/works?page=2)を受け取ってサーバー側でその都度データを絞り込めばよい。

ところが今回のプロジェクトはNext.jsの output: export(完全な静的書き出し)を使っている。この方式ではビルド時にすべてのHTMLファイルを事前生成してしまうため、「アクセスされた時にクエリパラメータを見てサーバー側で処理する」という仕組みが存在しない。クエリパラメータ(?page=2)は静的ファイルのURLとしては区別できないため、ページ番号ごとに別々のURL(別々の静的ファイル)として最初から全部書き出しておく必要がある。

具体的には、1ページ目は works の一覧ページそのもの、2ページ目以降は works の下に page というディレクトリを作り、その下に2, 3, 4…という連番のディレクトリを用意して、それぞれに独立したページを生成する、という設計にした。

ディレクトリ構成

1ページ目を works ディレクトリ直下の page.tsx が担当し、2ページ目以降は works の下の page というディレクトリの中に、角括弧で囲んだ動的セグメント用のディレクトリを作ってそこに page.tsx を置く。この角括弧のディレクトリ名がそのままURLのページ番号(動的パラメータ)に対応する。

これにより、1ページ目のURLはそのまま works の一覧ページとなり、2ページ目以降は works の下の page の下に番号が続くURL構造になる。

generateStaticParams でビルド時に全ページ分を列挙する

動的セグメントを持つページでは、ビルド時に「どの値のページを何個生成するか」をNext.jsに教えてやる必要がある。これを担うのが generateStaticParams という特別な関数で、動的セグメントを持つ page.tsx の中で async 関数としてエクスポートする。

この関数の中でmicroCMSから全件数を取得し、1ページあたりの件数で割ってページ数を計算し、2ページ目から最後のページまでの番号を配列にして返す。返す配列の各要素は、動的セグメントの名前をキーにしたオブジェクトになる。例えば全体で2ページある場合、2ページ目だけが対象になるので、要素は1つだけの配列を返すことになる。

Next.jsはビルド時にこの関数を呼び出し、返ってきた値の数だけ実際にページコンポーネントを実行してHTMLファイルを生成する。1ページ目については、この仕組みとは別に、動的セグメントを持たない普通の page.tsx として用意しているので、generateStaticParams の対象には含めていない。

データ取得と切り出しのロジックは共通化しておく

1ページ目用のファイルと2ページ目以降用のファイルの両方で「全件取得して、指定したページ番号に該当する範囲だけ切り出す」という処理が必要になる。これを両方に別々に書くと保守しにくいので、works ディレクトリの直下に共通ロジック専用のファイルを1つ用意し、そこに1ページあたりの件数の定数と、指定したページのデータと総ページ数を返す関数をまとめておいた。

この関数の中身は単純で、まず全件を取得し、全体の件数を1ページあたりの件数で割って切り上げることで総ページ数を出す。そのうえで、配列の slice を使って、要求されたページ番号に対応する範囲だけを取り出して返す。今回は全体の件数がそれほど多くないため、一度全件取得してからメモリ上で配列を切り出す方式にしている。件数が数百件を超えるブログ記事一覧では、全件取得ではなくmicroCMSのクエリのoffsetとlimitを使って、必要な範囲だけを直接取得する方式に変える予定。

ページ番号のリンクUIも共通コンポーネント化する

「前へ」「1 2 3…」「次へ」を表示するナビゲーション部分は、現在のページ番号と総ページ数、そしてURLのベースとなるパス(例えば works)の3つを受け取れば、works以外の一覧ページ(今後作るブログ一覧など)でもそのまま使い回せる。

このコンポーネントの中身は、総ページ数が1以下ならそもそも何も表示しない、というガード節から始まる。そのうえで、リンク先のURLを組み立てる小さな関数を用意し、ページ番号が1のときは一覧ページそのもののURLを、2以降のときは前述の page ディレクトリ配下のURLを返すようにする。総ページ数分の配列を作ってそれぞれの番号にリンクを描画し、現在のページ番号と一致するものだけ見た目を変える。あわせて、現在のページが最初でなければ「前へ」、最後でなければ「次へ」のリンクも出す。

ここで使うリンクは、Reactのリンク用コンポーネントではなく、ごく普通のアンカータグで実装している。静的書き出しされたサイトなので、素直に別ページへの遷移として扱えばよく、特別な仕組みは不要だった。