<- Back to list

NextJS + Markdown ブログでページネーションを作る

HelloWorldJavaScriptNext.jsReact

このブログは Next.js で、マークダウンファイルを SSG して Netlify で配信しています。 マークダウンブログにページネーションをつける方法を記します。自分で作ったやつなので、もっとスッキリした書き方があるとは思いますが‥。

0. 前提となる構成

ディレクトリ構成はこのようになっています。

├── components │ ├── CodeBlock.tsx │ ├── Header.tsx │ ├── Layout.tsx │ ├── Pagination.tsx // 1.ページネーションコンポーネントを作成 │ └── PostList.tsx ├── pages │ ├── _app.tsx │ ├── index.tsx // 3.投稿リストページにページネーションを設置 │ ├── page │ │ └── [pagenumber].tsx // 2.ページネーション用の動的ページを作成 │ └── post │ └── [postname].tsx

この記事で書くページネーションに必要なもののみにコメントを付けました。

1. ページネーションコンポーネントを作成

/components/Pagination.tsx はこの様になっています。
※スタイリングは emotion の styled-component でしていますが今回はスタイルの話はしないので省いています。

import Link from "next/link"; export default function Pagination({ totalCount, pageLimit, currentPage, }: { totalCount: number; pageLimit: number; currentPage: number; }) { if (totalCount > pageLimit) { const range = (start: number, end: number) => [...Array(end - start + 1)].map((_, i) => start + i); return ( <ol> {currentPage >= 2 && ( <Link href={`/page/${currentPage - 1}`}> <a>Prev</a> </Link> )} {range(1, Math.ceil(totalCount / pageLimit)).map((number, index) => ( <li key={index}> {currentPage === number ? ( <span>{number}</span> ) : ( <Link href={number === 1 ? "/" : `/page/${number}`}> <a>{number}</a> </Link> )} </li> ))} {currentPage < Math.ceil(totalCount / pageLimit) && ( <Link href={`/page/${currentPage + 1}`}> <a>Next</a> </Link> )} </ol> ); } else { return null; } }
  1. ページネーションコンポーネントには、
    • totalCount: 全体の投稿数
    • pageLimit: 1ページに表示する投稿数
    • currentPage: 現在何ページ目か
      をもたせています。
  2. totalCount > pageLimit で、1ページに表示する投稿数よりも全投稿数が多かったときのみページネーションを表示し、そうでない場合は null を返しているのですが、これはページ側でやったほうがいいのかもしれません?
  3. ページネーションのページ数 を定義して書き出します。
    const range = (start: number, end: number) => [...Array(end - start + 1)].map((_, i) => start + i);
    最初に、range 関数をつくっておきます。これは、start と end の数字から、連番を格納した配列を生成する関数です。
    {range(1, Math.ceil(totalCount / pageLimit)).map((number, index) => ( <li key={index} current={currentPage === number}> <Link href={`/page/${number}`}> <a>{number}</a> </Link> </li> ))}
    start は 常に1で、end は Math.ceil(totalCount / pageLimit) で最後のページ数を割り出し、そのページ数分、href={/page/${number}} に飛ぶようなリンクのリストを生成します。
    今いるページにカレントを付けたいので、currentPage === number だったら span を返すようにしています。
  4. Prev/Next リンクを設置
    • Prev: 現在いるページが2ページ目以降だったらレンダーします。
      {currentPage >= 2 && ( <Link href={`/page/${currentPage - 1}`}> <a>Prev</a> </Link> )}
    • Next: 現在いるページが最後のページよりも小さかったらレンダーします。
      {currentPage < Math.ceil(totalCount / pageLimit) && ( <Link href={`/page/${currentPage + 1}`}> <a>Next</a> </Link> )}

2. ページネーション用の動的ページを作成

1でつくったページネーションをクリックすると、/page/2 のようなページに飛びますので、このページを作ります。
/pages/[pagenumber].tsx はこの様になっています。

import PostList from "@components/PostList"; import matter from "gray-matter"; import Layout from "@components/Layout"; import Pagination from "@components/Pagination"; export default function BlogPageId({ posts, pagenumber, pageLimit, }: { posts: any; pagenumber: number; pageLimit: number; }) { return ( <Layout pageTitle={`${pagenumber}ページ目`} description={`ブログの${pagenumber}ページ目です!`}> <PostList posts={posts} pageStart={(pagenumber - 1) * pageLimit} pageLimit={(pagenumber - 1) * pageLimit + pageLimit} /> <Pagination totalCount={posts.length} pageLimit={pageLimit} currentPage={Number(pagenumber)} /> </Layout> ); } // データを取得 export const getStaticProps = async (ctx: { params: { pagenumber: string }; }) => { const config = await import(`../../siteconfig.json`); const pagenumber = ctx.params.pagenumber; const posts = ((context) => { const keys = context.keys(); const values = keys.map(context); const data = keys.map((key, index) => { let slug = key.replace(/^.*[\\\/]/, "").slice(0, -3); const value: any = values[index]; const document = matter(value.default); return { frontmatter: document.data, markdownBody: document.content, slug, }; }); return data; })(require.context("../../posts", true, /\.\/.*\.md$/)); return { props: { posts, pagenumber, pageLimit: config.pageLimit, }, }; }; // 動的なパスを取得 export const getStaticPaths = async () => { const config = await import(`../../siteconfig.json`); const range = (start: number, end: number) => [...Array(end - start + 1)].map((_, i) => start + i); const blogLength = ((context) => {return context.keys().length;})(require.context("../../posts", true, /\.md$/)); // 全投稿数を取得 const paths = range(1, Math.ceil(blogLength / config.pageLimit)).map((repo) => `/page/${repo}`); // 何ページあるか割り出し、動的パスに格納 return { paths, fallback: false }; };

ちょっと長いですが。。。PostList については今回記さないので、PostList 関連は飛ばしていきます。

  1. 動的なパスを取得 getStaticPaths で動的ページのパスを取得します。ページネーションコンポーネントでも使った range 関数で、1ページから最後が何ページなのかを割り出して、パスにいれます。
  2. データを取得 getStaticProps でページに入れるデータを取得します。ここで posts(mdファイルの情報), pagenumber(今いるページのパス), pageLimit(1ページあたりの最大表示数) を取得しておきます。
  3. ページネーションコンポーネントを設置
    <Pagination totalCount={posts.length} pageLimit={pageLimit} currentPage={Number(pagenumber)} />
    const pagenumber = ctx.params.pagenumber でこのページのパス名を返していますが、これは String 型なので Number に変換して使用します。

3. 投稿リストページにページネーションを設置

1でページネーションコンポーネントを、2で2ページ目以降などの動的ページをつくったので、これらをつかって投稿一覧ページを作ります。わたしのブログでは、トップページ=投稿一覧ページなので /pages/index.tsx が投稿一覧になりますが、そうでない場合は /pages/blog.tsx とかになるでしょう。
/pages/index.tsx はこの様になっています。

import matter from "gray-matter"; import Layout from "@components/Layout"; import PostList from "@components/PostList"; import Pagination from "@components/Pagination"; const Index = ({ posts, title, description, pageLimit, ...props }: { posts: any; title: string; description: string; pageLimit: number; }) => { return ( <Layout pageTitle={title} description={description}> <PostList posts={posts} pageStart={0} pageLimit={pageLimit} /> <Pagination totalCount={posts.length} pageLimit={pageLimit} currentPage={1} /> </Layout> ); }; export default Index; export const getStaticProps = async () => { const config = await import(`../siteconfig.json`); const posts = ((context) => { const keys = context.keys(); const values = keys.map(context); const data = keys.map((key, index) => { let slug = key.replace(/^.*[\\\/]/, "").slice(0, -3); const value: any = values[index]; const document = matter(value.default); return { frontmatter: document.data, markdownBody: document.content, slug, }; }); return data; })(require.context("../posts", true, /\.\/.*\.md$/)); return { props: { posts, title: config.default.title, description: config.default.description, pageLimit: config.pageLimit, }, }; };

ここでも2と同じ用に、リスト生成の PostList コンポーネントと、ページネーションの Pagination コンポーネントをおいて、getStaticProps でデータを取得して渡しています。 getStaticProps 箇所は2と重複しているので、まとめて記述できるといいな、、と思っているのですが、今回はこのようにしてページネーションを実装しました。