Astro Content Collectionsの一覧ページでページネーションを実装する

はじめに

Astro Content Collections を使ってページネーションを実装したい!!と思ったので、実装してみました。 ページネーションがあれば、一ページに表示するコンテンツの数を制限できるので、コンテンツが増えてきたときに便利ですし、ユーザーの利便性も向上しますよね。

Content Collections

Astro Content Collectionsは、Astro V 2.0.0 からある Astro の古参機能で、Markdown 系のファイルをコンテンツとして扱うことができる機能です。

以下のようなファイルをsrc/pages以下に置けば、src/content/blog/*にある Markdown ファイルを一覧として取得する事ができます。

---
import { getCollection } from "astro:content";

const posts = (await getCollection("blog"));
---
{
    posts.map((post) => (
        <div>
        <h2>{post.data.title}</h2>
        <p>{post.data.description}</p>
        </div>
    ));
}

しかし、このままだと、全てのコンテンツが一度に表示されてしまいます。 数件しかないコンテンツなら問題ありませんが、数十件、数百件と増えてくると、ページの表示速度が遅くなったり、ユーザーの利便性が低下してしまいます。 そこで、ページネーションを実装して、コンテンツを分割して表示するようにします。

ページネーションの実装

Astro でページネーションを実装するには、動的ルーティングgetStaticPathsを使用します。具体的には、[page].astroのような動的ルートファイルを作成し、その中でgetStaticPaths関数を実装します。

1. 動的ルートファイルの作成

まず、src/pages/blog/[page].astroというファイルを作成します。このファイル名の[page]部分が実際のページ番号(1, 2, 3…)に置き換えられます。

2. getStaticPaths の実装

getStaticPaths関数内でpaginateヘルパー関数を使用して、ページネーション用のパスとデータを生成します。

---
interface Paginate {
    paginate: (
        items: any[],
        options: {
            pageSize: number;
        }
    ) => any;
}

export async function getStaticPaths({ paginate }: Paginate) {
    const posts = (await getCollection("blog")).sort(
        (a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf()
    );
    return paginate(posts, { pageSize: 10 });
}
---

この例では、すべてのblog記事を取得し、公開日の降順で並べ替えて、1 ページあたり 10 件の記事を表示するよう設定しています。 インターフェースはanyでもいいと思います。私の環境では、型の指定がないと getStaticPath で Warn が出てしまったので、指定しています。

3. ページデータの取得と表示

getStaticPathsで生成されたデータは、Astro.props.pageとして利用できます。

---
interface Props {
    page: {
        data: any[]; // 現在のページのデータ(ブログ記事など)
        url: {
            prev: string | undefined; // 前のページへのURL(ない場合はundefined)
            next: string | undefined; // 次のページへのURL(ない場合はundefined)
        };
        currentPage: number; // 現在のページ番号
    };
}

const { page } = Astro.props as Props;
---

4. ページネーション UI の実装

コンテンツの下部には、前後のページへのリンクを含むページネーション UI を配置します。

一般的なナビゲーションならこんな感じでしょうか。

  • 現在のページのハイライト表示
  • 前後のページへのリンク
  • 最初と最後のページへのショートカットリンク(「≪」と「≫」記号)
  • 長いページリストの省略表示(…)

があります。

コード
---
// src/pages/blog/[page].astro
const posts = (await getCollection("blog"))
const { page } = Astro.props as PageProps;

const lastPage = Math.ceil(posts.length / 10);
---
{
    page.url.prev && page.currentPage > 2 && (
        <a
            href={`/blog/1/`}
            class="px-3 py-2 rounded-lg text-sm text-blue-600 hover:bg-blue-50 dark:hover:bg-blue-900/30"
        >
            «
        </a>
    )
}
{
    page.url.prev && page.currentPage - 1 > 2 && (
        <span class="px-3 py-2 text-sm text-gray-600">...</span>
    )
}
{
    page.url.prev && (
        <a
            href={page.url.prev}
            class="px-3 py-2 rounded-lg text-sm text-blue-600 hover:bg-blue-50 dark:hover:bg-blue-900/30"
        >
            {page.currentPage - 1}
        </a>
    )
}
<span class="px-3 py-2 rounded-lg text-sm bg-blue-600 text-white">
    {page.currentPage}
</span>
{
    page.url.next && (
        <a
            href={page.url.next}
            class="px-3 py-2 rounded-lg text-sm text-blue-600 hover:bg-blue-50 dark:hover:bg-blue-900/30"
        >
            {page.currentPage + 1}
        </a>
    )
}
{
    page.url.next && page.currentPage + 1 < lastPage && (
        <span class="px-3 py-2 text-sm text-gray-600">...</span>
    )
}
{
    page.url.next && (
        <a
            href={`/blog/${lastPage}/`}
            class="px-3 py-2 rounded-lg text-sm text-blue-600 hover:bg-blue-50 dark:hover:bg-blue-900/30"
        >
            »
        </a>
    )
}

5. 完成したページネーション

この実装により、以下の URL パターンでブログ記事一覧にアクセスできるようになります:

  • /blog/1/ - 最初のページ
  • /blog/2/ - 2 ページ目
  • /blog/3/ - 3 ページ目 …

各ページには指定した件数(例:10 件)の記事が表示され、記事が増えても快適に閲覧できるようになります。