このポートフォリオサイト(shu-web-portfolio)の制作実績一覧(works)で実装した方法をベースにまとめる。全220件のブログ記事一覧にも同じ考え方を使う予定。
一般的な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構造になる。
動的セグメントを持つページでは、ビルド時に「どの値のページを何個生成するか」を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を使って、必要な範囲だけを直接取得する方式に変える予定。
「前へ」「1 2 3…」「次へ」を表示するナビゲーション部分は、現在のページ番号と総ページ数、そしてURLのベースとなるパス(例えば works)の3つを受け取れば、works以外の一覧ページ(今後作るブログ一覧など)でもそのまま使い回せる。
このコンポーネントの中身は、総ページ数が1以下ならそもそも何も表示しない、というガード節から始まる。そのうえで、リンク先のURLを組み立てる小さな関数を用意し、ページ番号が1のときは一覧ページそのもののURLを、2以降のときは前述の page ディレクトリ配下のURLを返すようにする。総ページ数分の配列を作ってそれぞれの番号にリンクを描画し、現在のページ番号と一致するものだけ見た目を変える。あわせて、現在のページが最初でなければ「前へ」、最後でなければ「次へ」のリンクも出す。
ここで使うリンクは、Reactのリンク用コンポーネントではなく、ごく普通のアンカータグで実装している。静的書き出しされたサイトなので、素直に別ページへの遷移として扱えばよく、特別な仕組みは不要だった。