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はそのままだと型エラーがでるので数値型にする