841-biborokuWebフロントエンドの備忘録

Astroのpaginate()を使ってページネーションコンポーネントをつくる

この記事は、microCMSのデータとAstroのpaginate関数を使用して、ChatGPTでページネーションコンポーネントを作成した備忘録です。

※microCMSの詳しい記事取得方法は含みません。

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

やりたいこと

  • 最初・最後のページを表示するオプション
  • ページ数が多い場合に「...」を表示する機能
  • 前後のページへ移動するボタン
  • 現在のページを示すインジケーター
  • ページが1つしかない場合にページネーションを非表示にするオプション
  • 全ページ表示ができる
  • 1つのコンポーネントで管理できる
  • aria-current/aria-labelの設定

ページの作成

getStaticPaths()でページを作成します。

/pages/blog/[...page].astro

---
import type { InferGetStaticPropsType, GetStaticPaths } from "astro";
import Pagination from "@components/Pagination.astro";
import { getAllBlogList } from "@libs/microcms";
type Props = InferGetStaticPropsType<typeof getStaticPaths>;

export const getStaticPaths = (async ({ paginate }) => {
  const allPosts = await getAllBlogList();  // 全記事取得
  return paginate(allPosts, { pageSize: 1 });  // pageSize=1ページあたりの件数・わかりやすくするため1件にしてます
}) satisfies GetStaticPaths;

const { page } = Astro.props;
---
~略:記事一覧
<Pagination page={page} baseUrl="/blog" />

ページネーションの表示形式を独自オプションで柔軟にカスタマイズ

オプションは主に表示に関するもののみを管理し、それ以外の部分はHTMLソースで柔軟にカスタマイズできるようにしています。

コンポーネントは、アイテムごとに分けることもできますが、他の部分で再利用されることが少ないため、1つのコンポーネントで管理する形にしました☺️

Pagination.astro

---
const { page, baseUrl } = Astro.props;
const { lastPage, currentPage, url } = page;
const paginationOptions = {
  alwaysShowPagination: true, // ページネーションを常に表示するか
  showFirst: true,  // 「最初のページ」ボタンを表示するか
  showLast: true,  // 「最後のページ」ボタンを表示するか
  showItems: 5, // 表示するページ番号の最大数(0で全ページ表示、偶数指定時は自動で-1して奇数に調整)
  showIndicator: true,  // 現在のページを示すインジケーターを表示するか
  showDots: true, // ページ数が多い場合に「...」を表示するか
  dot: "...", // ドットの表記文字列
};
// 略
---

alwaysShowPagination: true

ページネーションは、記事の総数が pageSize よりも少ない場合でも、常に表示するかどうかを設定するオプションです。もし記事の総数がページサイズより少ない場合でも、1ページのみが生成されます。それでもページネーションを常に表示するかどうか、このオプションで制御できます。

showFirst: true

最初のページへのリンクを表示するかどうかを設定するオプションです。true に設定すると、最初のページへのリンクが表示されます。

showLast: true

最後のページへのリンクを表示するかどうかを設定するオプションです。true に設定すると、最後のページへのリンクが表示されます。

showItems: 5

ページネーションで表示するページ番号の個数を設定するオプションです。

  • 0 に設定すると、ページ番号を省略せず、全てのページ番号を表示します。
  • 偶数の値を設定した場合、自動的に1引かれて奇数の値に調整されます。これにより、左右のページリンクが均等に表示されるようになります。

showIndicator: true

現在のページ番号と総ページ数を表示するかどうかを設定するオプションです。true に設定すると、現在のページ番号と総ページ数(例: "3 / 10")が表示されます。

showDots: true

ページ番号の間にドット(...)を表示するオプションです。ページ数が多くなると、全てのページ番号を表示するのは煩雑になるため、ドットで省略する部分を表示します。

例えば、ページ数が10ページの場合、1ページ目、3ページ目、最終ページなどを表示し、それ以外はドットで省略されます。

dot: "..."

ドットを表示する文字列を設定するオプションです。デフォルトでは "..." が使用されますが、カスタマイズが可能です。

dot機能の適用に関する処理

  • ページ数が showItems より多く、showItems が3ページ以上の場合、ドット(...)を表示します。
  • startPage が1より大きい場合、最初のページ番号の後にドットを表示します。
  • endPage が総ページ数の1つ前より小さい場合、最後のページ番号の前にドットを表示します。
  • 最初のページは 1 に設定され、最後のページは totalPages に設定されます。

例えば、ページ数が3ページの場合は、ドットが表示されることなく、すべてのページ番号が表示されます。

Pagination.astro(全コード)

---
const { page, baseUrl } = Astro.props;
const { lastPage, currentPage, url } = page;
const paginationOptions = {
  alwaysShowPagination: true,
  showFirst: true,
  showLast: true,
  showItems: 5,
  showIndicator: true,
  showDots: true,
  dot: "...",
};
let {
  alwaysShowPagination,
  showFirst,
  showLast,
  showDots,
  showItems,
  showIndicator,
  dot,
} = paginationOptions;

// showItemsが偶数なら-1して奇数にする
if (showItems % 2 === 0 && showItems !== 0) {
  showItems -= 1;
}

const totalPages = lastPage;

// 表示するページ番号を計算
let startPage, endPage;
if (showItems === 0 || totalPages <= showItems) {
  startPage = 1;
  endPage = totalPages;
} else if (currentPage <= Math.floor(showItems / 2) + 1) {
  startPage = 1;
  endPage = showItems;
} else if (currentPage > totalPages - Math.floor(showItems / 2)) {
  startPage = totalPages - showItems + 1;
  endPage = totalPages;
} else {
  startPage = currentPage - Math.floor(showItems / 2);
  endPage = currentPage + Math.floor(showItems / 2);
}

let pageNumbers = [...Array(endPage - startPage + 1)].map(
  (_, i) => startPage + i
);

// dot機能の適用
if (showDots && totalPages >= showItems + 1 && showItems > 3) {
  if (startPage > 1) pageNumbers[1] = 0;
  if (endPage <= totalPages - 1) pageNumbers[pageNumbers.length - 2] = 0;

  // 1番目を1、items番目を最後のページにする
  pageNumbers[0] = 1;
  pageNumbers[pageNumbers.length - 1] = totalPages;
}

const getAriaLabel = (pageNum: number) =>
  pageNum !== currentPage
    ? `ページ ${pageNum}`
    : `現在のページ(ページ ${pageNum}`;
---

{
  alwaysShowPagination || totalPages > 1 ? (
    <nav aria-label="ページネーション" class="pagination">
      {showIndicator && (
        <p class="indicator" aria-live="polite">
          {currentPage} / {totalPages}
        </p>
      )}
      <ul class="list">
        {showFirst && url.first && (
          <li class="item first">
            <a
              href={url.first}
              class="link"
              aria-label={`最初のページ(ページ 1)`}
            >
              &#171;
            </a>
          </li>
        )}
        {url.prev && (
          <li class="item prev">
            <a
              href={url.prev}
              class="link"
              aria-label={`前のページ(ページ ${currentPage - 1})`}
            >
              &#139;
            </a>
          </li>
        )}
        {pageNumbers.map((pageNum) => (
          <li class="item">
            {pageNum === 0 ? (
              <span class="dot" aria-hidden="true">
                {dot}
              </span>
            ) : (
              <a
                href={`${baseUrl}${pageNum > 1 ? `/${pageNum}` : ""}`}
                class:list={["link", pageNum === currentPage ? "current" : ""]}
                aria-current={pageNum === currentPage ? "page" : undefined}
                aria-label={getAriaLabel(pageNum)}
              >
                {pageNum}
              </a>
            )}
          </li>
        ))}
        {url.next && (
          <li class="item next">
            <a
              href={url.next}
              class="link"
              aria-label={`次のページ(ページ ${currentPage + 1})`}
            >
              &#155;
            </a>
          </li>
        )}
        {showLast && url.last && (
          <li class="item last">
            <a
              href={url.last}
              class="link"
              aria-label={`最後のページ(ページ ${totalPages}`}
            >
              &#187;
            </a>
          </li>
        )}
      </ul>
    </nav>
  ) : null
}

まとめ

最初と最後の「前へ」「次へ」ボタンはpaginate関数を使用して実装しました。

間のページリストやドット表示は、ChatGPTを活用して基本的な構成を作り、その後、細かな条件調整を自分で行いました。ChatGPTにまとめて条件を提示すると希望通りにならないことがあるため、各オプションについて具体的な条件を提示し、段階を経て最終的な仕様に仕上げました。

📝参考サイト

シェア