ページネーション機能の解説 (Svelte)

このコードはSvelteを使用して実装されたページネーションコンポーネントです。このコンポーネントは記事一覧などで複数ページに分割された内容をナビゲートするために使用されます。ここでは、コードの各部分を詳細に解説し、その動作原理と実装の工夫について説明します。

プロパティとステート管理

interface Props {
  current: number;
  slug: string;
  total: number;
  limit: number;
  max: number;
}
let {
  current = $bindable(),
  slug,
  total,
  limit,
  max = $bindable(),
}: Props = $props();

このコンポーネントは次のプロパティを受け取ります:

  • current: 現在表示しているページ番号
  • slug: 記事のスラッグ(URL識別子)
  • total: 全アイテム数
  • limit: 1ページあたりのアイテム数
  • max: 最大ページ数

特筆すべき点は$bindable()$props()の使用です。これはSvelte 5で導入された新しい「runes」構文の一部で、よりシンプルなリアクティビティを実現します。$bindable()を使うことで、親コンポーネントからバインド可能なプロパティを定義しています。

リアクティブな効果

$effect(() => {
  current = Number(page.params.page);
});
$effect(() => {
  max = Math.ceil(total / limit);
});

$effectはSvelte 5のリアクティブステートメントで、依存するデータが変更されるたびに再実行されます:

  1. 最初の$effectpage.params.pageが変更されるたびにcurrentを更新します。これにより、URLのページパラメータが変わると現在のページが自動的に更新されます。
  2. 2つ目の$effecttotalまたはlimitが変更されるたびに最大ページ数(max)を計算します。

派生値の計算

let start = $derived((current - 1) * limit);
let end = $derived(current === max ? total - 1 : start + limit - 1);

$derivedを使用して、既存の状態から新しい値を計算しています:

  • startは現在のページの最初のアイテムのインデックスです(0ベース)
  • endは現在のページの最後のアイテムのインデックスです

通常のページではstart + limit - 1が使用されますが、最後のページでは残りのアイテム数に合わせてtotal - 1が使用されます。これにより、最後のページで余分なアイテムが表示されないようになっています。

ナビゲーション関数

const goBack = (e: Event) => {
  e.preventDefault();
  goto(`/articles/${slug}/${current - 1}`);
};
const goNext = (e: Event) => {
  e.preventDefault();
  goto(`/articles/${slug}/${current + 1}`);
};

これらの関数はページナビゲーションを処理します:

  • goBackは前のページに移動します
  • goNextは次のページに移動します

どちらの関数も、まずe.preventDefault()を呼び出してリンクのデフォルトの動作を防止し、次にSvelteKitのgoto関数を使用してクライアントサイドナビゲーションを実行します。これにより、ページ全体をリロードせずにURLと表示コンテンツを更新できます。

テンプレートマークアップ

{#if total && total > limit}
  <div class="pagination">
    {#if current > 1}
      <a
        href={`/articles/${slug}/${current - 1}`}
        class="button"
        onclick={goBack}
        aria-label="left arrow icon"
        aria-describedby="prev">prev</a
      >
    {/if}
    <p>{start + 1} - {end + 1} of {total}</p>
    {#if current < max}
      <a
        href={`/articles/${slug}/${current + 1}`}
        class="button"
        onclick={goNext}
        aria-label="right arrow icon"
        aria-describedby="next">next</a
      >
    {/if}
  </div>
{/if}

テンプレート部分は条件付きレンダリングを多用して、適切なUIを表示します:

  1. total && total > limitの条件は、ページネーションUI自体が表示されるべきかどうかを判断します。アイテム数が1ページに収まる場合、ページネーションは不要です。
  2. current > 1の条件は「prev」ボタンを表示するかどうかを判断します。最初のページでは「prev」ボタンは不要です。
  3. 中央のテキストは「X - Y of Z」形式でページ情報を表示します。表示はユーザーにわかりやすい1ベースのインデックスを使用しています。
  4. current < maxの条件は「next」ボタンを表示するかどうかを判断します。最後のページでは「next」ボタンは不要です。

アクセシビリティの観点から、aria-labelaria-describedby属性を使用してスクリーンリーダーユーザーに情報を提供しています。

SEOとアクセシビリティのための隠しリンク

<div class="dummy">
  {#each Array(max) as _, i}
    <a href="/articles/{slug}/{i + 1}">{i + 1}</a>
  {/each}
</div>

このセクションは視覚的には非表示ですが、すべてのページへの直接リンクを生成しています。これには主に2つの目的があります:

  1. SEO向上:検索エンジンのクローラーが全ページを発見できるようにする
  2. アクセシビリティ:JavaScriptが無効な環境でもすべてのページにアクセスできるようにする

display: nonevisibility: hiddenを両方使用することで、視覚的にもスクリーンリーダーにも非表示にしつつ、検索エンジンがリンクをたどれるようにしています。

スタイリング

.dummy {
  display: none;
  visibility: hidden;
}
.pagination {
  display: flex;
  align-items: center;
  justify-content: center;
  pointer-events: all;
}
.pagination p {
  margin: 1rem;
}

スタイリングはシンプルですが効果的です:

  • ダミーリンクコンテナは完全に非表示
  • ページネーションコンテナはフレックスボックスを使用して中央揃え
  • pointer-events: allはこの要素がポインタイベントを確実に受け取れるようにします(親要素がpointer-events: noneを持つ可能性がある場合に重要)
  • テキスト部分には適切なマージンが設定され、ボタンと適切な間隔を確保しています

実装の工夫と特筆点

  1. SvelteKitとの統合: $app/navigationからのgoto$app/stateからのpageの使用により、SvelteKitアプリケーションとシームレスに統合されています。

  2. Svelte 5のrunes: 新しい$bindable$props$effect$derivedなどの構文を使用して、よりクリーンで直感的なリアクティビティを実現しています。

  3. SEOとアクセシビリティへの配慮: 検索エンジンとJavaScriptが無効な環境の両方をサポートする隠しリンクを含めています。

  4. 柔軟性: このコンポーネントは異なる記事コレクションでのページネーションに再利用できるよう、slugをパラメータとして受け取ります。

  5. ユーザービリティ: 現在のページに不要なナビゲーションボタン(最初のページの「prev」や最後のページの「next」)は表示されません。

  6. 1ベースと0ベースのインデックスの使い分け: 内部計算では0ベースのインデックスを使用しつつ、ユーザーに表示する際には人間にとって自然な1ベースのインデックスを使用しています。

まとめ

このSvelteページネーションコンポーネントは、最新のSvelte 5の機能を活用しつつ、SEO、アクセシビリティ、ユーザビリティに配慮した設計になっています。シンプルながらも柔軟性が高く、さまざまなコンテンツリストで再利用できるコンポーネントです。また、クライアントサイドナビゲーションを活用することで、スムーズなユーザーエクスペリエンスを提供しています。