Next.js + contentlayerで作ったブログにページネーションを実装する
2023/08/252023/08/30

Next.js + contentlayerで作ったブログにページネーションを実装する

8 mins to read

今回はlodashのchunk関数を使ってページネーションを実装しました。いろんな場面で使いまわせると思うので、作り方を記事にまとめておきます。

前提

以下を使う前提です。

  • Next.js (App Router使用)
  • contentlayer
  • TailwindCSS
  • lodash

呼び出し元ページ(page.tsx)

URLに与えられたパラメータから表示したいページ数を取得して、それに応じて表示する記事が切り替わるようになっています。

app/posts/page.tsx
import { Post, allPosts } from "contentlayer/generated";
import _ from "lodash";
import Link from "next/link";
import { Pager } from "@/components/pager";

const getCorrectPage = ({
  page,
  max
}: {
  page: string | number | undefined;
  max: number;
}) => {
  let _page = Number(page);
  if (page === undefined || !_.isInteger(_page) || _page > max || _page <= 0) {
    return 1;
  } else {
    return _page;
  }
};

const PER_PAGE = 10;

interface PageProps {
  searchParams: { page: number };
}

export default function Home({ searchParams }: PageProps) {
  const sorted = _.orderBy(allPosts, "date", "desc");
  const chunked = _.chunk(sorted, PER_PAGE);

  let { page } = searchParams;
  page = getCorrectPage(page);
  const posts = chunked[page - 1];

  return (
    <div>
      <div>
        {posts.map((post: Post) => {
          <Link key={post.slug} href={post.slug}>{post.title}</Link>;
        })}
      </div>
      {chunked.length > 1 ? (
        <Pager path="/posts" max={chunked.length} page={page} />
      ) : (
        ""
      )}
    </div>
  );
}

解説

lodashのchunk関数は一定の数ごとに配列を分ける関数です。

const array = [1, 2, 3, 4, 5, 6, 7, 8];
console.log(_.chunk(array, 3));

例えばこの場合、コンソールには[[1, 2, 3], [4, 5, 6], [7, 8]]と3つずつに区切られた配列が出力されます。

上記のページコンポーネントでは記事が全件入った配列を日付(date)が新しい順にソートした後、この要領で10個(PER_PAGE)ずつの配列に分けて、pageから1引いた数をインデックスに渡す事で、渡されたパラメータによって表示する記事が切り替わるようになっています。

getCorrectPage関数は、pageパラメータがなかった時や数字以外の文字列が渡された時に、初期値の1を返す関数です。

そして<Pager />コンポーネントには、maxpagepathというpropsを渡しています。

  • max ... 最大ページ数(chunked.length)
  • page ... パラメータに渡されたページ数
  • path ... 呼び出し元のパス

では、次にページャーコンポーネントを作ります。

ページャーコンポーネント

components/pager.tsx
import { cn } from "@/lib/utils";
import { ChevronRight, ChevronLeft } from "lucide-react";
import Link from "next/link";

export const Pager = ({
  href,
  page,
  max,
}: {
  href: string;
  page: number;
  max: number;
}) => {
  const arr: number[] = [...Array(max)].map((_, i) => i + 1);
  return (
    <div className="flex w-[100%] justify-center  justify-between pt-8 sm:justify-center">
      {page > 1 ? (
        <button>
          <Link
            href={`${href}${
              page > 2
                ? `${href.includes("?") ? "&" : "?"}page=${page - 1}`
                : ""
            }`}
            className={cn(
              "hover flex h-[35px] w-[35px] select-none items-center justify-center rounded-full bg-card text-sm font-black shadow-sm",
            )}
          >
            <ChevronLeft size={17} strokeWidth={1.5} />
          </Link>
        </button>
      ) : (
        <button
          className={cn(
            "flex h-[35px] w-[35px] cursor-default select-none items-center justify-center rounded-full bg-[rgba(var(--muted-foreground),0.05)] text-sm",
          )}
        >
          <ChevronLeft
            size={17}
            strokeWidth={1.5}
            className="text-[rgba(var(--muted-foreground),0.6)]"
          />
        </button>
      )}
      <ul className="mx-3 flex items-center gap-2">
        {arr.map((e: any) => {
          return (
            <li key={e}>
              <Link
                href={`${href}${
                  e > 1 ? `${href.includes("?") ? "&" : "?"}page=${e}` : ""
                }`}
                className={cn(
                  "hover flex h-[35px] w-[35px] select-none items-center justify-center rounded-full text-sm shadow-sm",
                  e === page
                    ? "bg-gradient font-bold text-card"
                    : "bg-card text-muted-foreground",
                )}
              >
                {e}
              </Link>
            </li>
          );
        })}
      </ul>
      {page < max ? (
        <button>
          <Link
            href={`${href}${href.includes("?") ? "&" : "?"}page=${page + 1}`}
            className={cn(
              "hover flex h-[35px] w-[35px] select-none items-center justify-center rounded-full bg-card text-sm font-black shadow-sm",
            )}
          >
            <ChevronRight size={17} strokeWidth={1.5} />
          </Link>
        </button>
      ) : (
        <button
          className={cn(
            "flex h-[35px] w-[35px] cursor-default select-none items-center justify-center rounded-full bg-[rgba(var(--muted-foreground),0.05)] text-sm",
          )}
        >
          <ChevronRight
            size={17}
            strokeWidth={1.5}
            className="text-[rgba(var(--muted-foreground),0.6)]"
          />
        </button>
      )}
    </div>
  );
};

解説

配列arrの中には1からmaxまでの数字が入っています。(例えばmaxが3の場合は、[1,2,3]という感じ)
これをループさせて、${path}?page=${n}へのリンクを生成しています。

また、${path}?page=1になる場合は、初期ページである${path}へのリンクになるような分岐も入れています。

cn関数は、TailwindCSSで当てるスタイルを動的に切り替える為の関数で、現在表示されているページのリンクをハイライトする為に使用しています。詳しくはこちらの記事を参考にしてください。

終わりに

このブログの場合、ページネーションを設置しているページは以下です。

  • 記事一覧ページ (/posts)
  • タグごとの記事一覧ページ (/tags/タグ名)
  • 検索結果ページ (/s?keyword=検索ワード)

pathに渡す値をそれぞれカッコ内の値に変えるだけで、全てのページで同じ要領でページネーションを設置する事ができます。ページネーションは複数のページで必要になると思うので、こういう風に使い回せる形にしておくと便利だと思います。