ビルド時のページネーション:静的サイトのページ送りにクエリ文字列を使わない理由
一覧はビルド時に静的なページへ分割され、N ページ目は実在のルートになります。クライアント側のページング処理もクエリ文字列も使わず、ただのリンクです。
このサイトのブログ一覧は 1 ページ 6 件です。2 ページ目は /zh/blog/page/2/、ビルド時に生成された実在の HTML で、/zh/blog/?page=2 ではありません。
クエリ文字列を使わない理由
静的ホスティングでは、?page=2 はサーバーにとってもクローラーにとっても同じ HTML です。ページ分けは、ブラウザ側で切るか(初回描画に全記事を渡すことになり、性能上の利点を返上します)、サーバーがクエリを読むか(もはや静的ではありません)の二択になります。ビルド時にページを作れば、そのどちらも避けられます。
ページ番号の収束
切り出しと番号の収束は lib/paginate.ts にある import ゼロの純粋ロジックです。
export function paginate(items, page, size) {
const total = totalPages(items.length, size);
const current = clampPage(page, total);
const start = (current - 1) * step;
return {
items: items.slice(start, start + step),
page: current,
totalPages: total,
hasPrev: current > 1,
hasNext: current < total,
};
}
範囲外のページ番号は空ページではなく最終ページへ収束し、不正な size は 1 ページとして扱われゼロ除算を避けます。どちらの分岐もアサーションで覆っています。
ルートは記事件数から導出
server/paths.ts の blogPagedPaths が何ページ生成するかを件数から計算します。1 ページ目は一覧そのものなので重複生成しません。記事を一本足せば新しいページが自動で現れ、ルートを手書きする必要はありません。
複数ページのときだけ描画される
1 ページだけなら pagination は null で、コンポーネントは描画されません。フッターに「1」がぽつんと並ぶこともありません。
代償
1 ページにつき 1 つの HTML なので、記事が増えればページ数は線形に増えます。またページ送りは全画面遷移で、絞り込み状態は保持しません。このサイトに絞り込みは無いので、後者は損失になりません。

コメント
…