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

Next.jsの動的ルーティングとページネーションコンポーネント

microCMS公式ブログで紹介されている「Next.js(SSG)でページネーションを実装してみよう」を参考にページネーションを作ってみる。


やりたいこと

  • 最初、最後、前へ、次へのリンク表示
    • カレントが1ページ目の場合、最初と前へは非表示
    • カレントが最後のページの場合、最後と次へは非表示
  • 表示するページネーション数を指定できる
    • カレントを中央値として前後表示(表示数が奇数・偶数どちらでもOK)
    • カレントから前後〇ページ分表示
    • 表示するページネーション数が全体のページネーション数を超えた場合、全体のページネーション数を上限にする



設定ファイル

const/setting.ts

表示用テキストは空欄にすると非表示になる

FIRST_TXT : "",

type PageNaviProps = {
  FIRST_TXT: string;
  LAST_TXT: string;
  PREV_TXT: string;
  NEXT_TXT: string;
  ITEMS: number;
  POSTS_PER_PAGE: number;
}

export const PAGE_NAVI:PageNaviProps = { 
  FIRST_TXT : "≪",
  LAST_TXT : "≫",
  PREV_TXT : "<",
  NEXT_TXT : ">",
  ITEMS: 4,
  POSTS_PER_PAGE: 10
}

ページネーションコンポーネント

components/Pagination.tsx

import Link from "next/link";
import { PAGE_NAVI } from "@/const/setting";

type PaginationProps = {
  currentPage: number;
  totalCount: number;
  postPerPage?: number;
  parentSlug?: string;
  rewriteSlug?: string;
};

export const Pagination = ({
  currentPage,
  totalCount,
  postPerPage = PAGE_NAVI.POSTS_PER_PAGE,
  parentSlug = "",
  rewriteSlug,
}: PaginationProps) => {
  const dir = rewriteSlug
    ? `/${rewriteSlug}`
    : parentSlug
    ? `/${parentSlug}`
    : "";
  const range = (start: number, end: number) =>
    [...Array(end - start + 1)].map((_, i) => start + i);
  const lastPageNum = Math.ceil(totalCount / postPerPage);

  const firstPage = currentPage > 1 ? 1 : 0;
  const prevPage = currentPage > 1 ? currentPage - 1 : 0;
  const nextPage = currentPage < lastPageNum ? currentPage + 1 : 0;
  const lastPage = currentPage < lastPageNum ? lastPageNum : 0;

  const median = Math.ceil(PAGE_NAVI.ITEMS / 2) - 1;

  const left =
    currentPage - median > lastPageNum - PAGE_NAVI.ITEMS
      ? lastPageNum - PAGE_NAVI.ITEMS
      : Math.max(0, currentPage - median - 1);

  const right = left + PAGE_NAVI.ITEMS;

  const pageItems = range(1, Math.ceil(totalCount / postPerPage)).map(
    (number, index) => ({
      url: number === 1 ? "/" : `/page/${number}`,
      txt: number,
      current: currentPage === number ? true : false,
    })
  );
  const rangePageItems =
    PAGE_NAVI.ITEMS > lastPageNum ? pageItems : pageItems.slice(left, right);

  const fistPageItem = {
    url: firstPage > 0 ? "/" : "",
    txt: PAGE_NAVI.FIRST_TXT,
  };
  const prevPageItem = {
    url: prevPage > 0 ? (prevPage === 1 ? "/" : `/page/${prevPage}`) : "",
    txt: PAGE_NAVI.PREV_TXT,
  };
  const nextPageItem = {
    url: nextPage > 0 ? `/page/${nextPage}` : "",
    txt: PAGE_NAVI.NEXT_TXT,
  };
  const lastPageItem = {
    url: lastPage > 0 ? `/page/${lastPage}` : "",
    txt: PAGE_NAVI.LAST_TXT,
  };
  const allPageItems = [
    fistPageItem,
    prevPageItem,
    ...rangePageItems,
    nextPageItem,
    lastPageItem,
  ];

  return (
    <ul className="flex">
      {allPageItems.map((page, index) =>
         page.url ? (
        <li key={index}>
            <Link
              href={`${dir}${page.url}`}
              className={`p-4${
                "current" in page && page.current ? " font-bold" : ""
              }`}
            >
              {page.txt}
            </Link>
        </li>
          ) : (
            ""
          )
      )}
    </ul>
  );
};

一覧用

page/[id].tsx


/blog/page/2 -> parentSlug="blog"

/hoge/blog/page/2 -> parentSlug="blog", rewriteSlug="hoge/blog"

import { PAGE_NAVI } from "@/const/setting";


export const getStaticPaths: GetStaticPaths = async () => {
  const data = await client.blog.$get();
  const range = (start: number, end: number) =>
    [...Array(end - start + 1)].map((_, i) => start + i);
  const paths = range(1, Math.ceil(data.totalCount / PAGE_NAVI.POSTS_PER_PAGE)).map(
    (repo) => `/page/${repo}`
  );
  return {
    paths,
    fallback: false,
  };
};
export const getStaticProps: GetStaticProps = async (context) => {
  const id = context.params?.id;
  const data = await client.blog.$get({
    query: {
      offset: (Number.parseInt(String(id)) - 1) * PAGE_NAVI.POSTS_PER_PAGE,
      limit: PAGE_NAVI.POSTS_PER_PAGE,
    },
  });
  return {
    props: { postsData: data, totalCount: data.totalCount },
  };
};
type Props = InferGetStaticPropsType<typeof getStaticProps>;

const Archive: NextPage<Props> = ({ postsData, totalCount }) => {
  const currentPage = Math.ceil(postsData.offset / PAGE_NAVI.POSTS_PER_PAGE) + 1;
  return (
    <>
      ~略
      <Pagination
        currentPage={currentPage}
        postPerPage={PAGE_NAVI.POSTS_PER_PAGE}
        totalCount={totalCount}
      />
    </>
  );
};
export default Archive;

offset: (Number.parseInt(String(id)) - 1)

idはそのままだと型エラーがでるので数値型にする

シェア