Next.jsにページネーションをNPMパッケージなしで追加する方法

blog-hero-imgNPMパッケージは使わずにNext.jsにページネーションを追加します。

pen-icon2021.12.25rewrite-icon2023.6.7

Profile Pic

この記事の筆者:三好アキ(エンジニア)


ウェブデザイナーから『エンジニア』『プログラマー』へ成長したい人、独学で進んでいきたい人を応援しています。 HTMLとCSSの知識だけでアプリ開発を始められる入門書を多数執筆中📕📗👇


ウェブ制作の教本『はじめてつくるReactアプリ』など複数冊を執筆。



React、Next.js、TypeScriptなどのお役立ち情報や実践的コンテンツを、ビギナー向けにかみ砕いて配信中。登録はこちらから → 無料メルマガ登録

ページネーションとは?

記事数が増えたときに、1ページあたりに表示する記事の数を5つや10つなどに制限したいときに使うものがページネーションです。

1 2 3 4 5

本記事ではまず最初にNext.jsにマークダウンのブログ機能を実装し、その後ページネーションを作ります。

ロジック部分の説明だけなので、スタイルは各自お好みで適用してください。

なお前後の記事へと移動するリンクの作り方は、こちらの記事を参考にしてください。

Next.jsバージョン13で導入されたAppフォルダを使った最新のNext.js開発に興味のある方は、今月(2023年6月)リリースした下記書籍を参考にしてください。

nextbook

マークダウンブログ機能の作成

まずNext.jsにマークダウンのブログ機能を追加します。

内容は下の記事で解説したものなので、すでに読んでいる人はパスしてください。


create-next-appをダウンロードします。

npx create-next-app nextjs-blog

記事を一覧表示するページとなるblog.js、そして記事データのマークダウンファイルを収納するフォルダdataを作ります。

nextjs-blog
├── data      ←追加
├── pages   
│       ├── api                
│       │    └── hello.js       
│       │
│       ├── _app.js
│       ├── blog.js  ←追加
│       └── index.js   
│     

次に、dataの中にブログ記事のファイルを作ります。


├── data
│      ├── first.md    
│      ├── second.md  
│      ├── third.md   
│      ├── fourth.md  
│      ├── fifth.md   
│      └── sixth.md    

それぞれのマークダウンファイルには、下のように基本的な情報を書きます。

// first.md

---
id: "1"
title: "記事1"
date: "2021-06-21"
summary: "記事1の要約"
---

1つ目の記事。
// second.md

---
id: "2"
title: "記事2"
date: "2021-06-22"
summary: "記事2の要約"
---

2つ目の記事。
// third.md

---
id: "3"
title: "記事3"
date: "2021-06-23"
summary: "記事3の要約"
---

3つ目の記事。
// fourth.md

---
id: "4"
title: "記事4"
date: "2021-06-24"
summary: "記事4の要約"
---

4つ目の記事。
// fifth.md

---
id: "5"
title: "記事5"
date: "2021-06-25"
summary: "記事5の要約"
---

5つ目の記事。
// sixth.md

---
id: "6"
title: "記事6"
date: "2021-06-26"
summary: "記事6の要約"
---

6つ目の記事。

次にNext.jsでマークダウンファイルを扱うためのパッケージもダウンロードしておきます。

npm install raw-loader gray-matter react-markdown

トップレベル(ルート)のnext.config.jsに下のコードを書きます。

// next.config.js

module.exports = {
    webpack: function (config) {
        config.module.rules.push({
            test: /\.md$/,
            use: "raw-loader",
        })
        return config
    },
}

blog.jsを開き、次のコードを追加します。

// blog.js

import Link from 'next/link'
import matter from "gray-matter"

const Blog = (props) => {
    return (
        <>
            <p>ブログ一覧ページ</p> 
            {props.blogs.map((blog, index) => (
                <div  key={index}>
                    <h2>{blog.frontmatter.title}</h2>
                    <Link href={`/blog/${blog.slug}`}><a>Read More</a></Link>
                </div>
            ))}
        </>
         
    )
}

export default Blog

export async function getStaticProps() { 
    const blogs = ((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 = values[index]
            const document = matter(value.default)
            return {
                frontmatter: document.data,
                slug: slug
            }
        })
        return data
    })(require.context('../data', true, /\.md$/))
        
    const sortingArticles = blogs.sort((a, b) => {
        return b.frontmatter.id - a.frontmatter.id
    })

    return {
      props: {
        blogs: JSON.parse(JSON.stringify(sortingArticles))
      }
    }
}

個別の記事ページでは、まずテンプレートとして使うファイルを次のように作ります。


├── pages  
│       ├── api    
│       ├── blog  ←作成
│       │      └── [slug].js ←作成 
│       ├── _app.js
│       ├── blog.js      
│       └── index.js  

そして次のコードを書きます。

// [slug].js

import matter from "gray-matter"
import ReactMarkdown from 'react-markdown'

const SingleBlog = (props) => {
    return (
        <div>               
            <h1>{props.frontmatter.title}</h1>
            <p>{props.frontmatter.date}</p> 
            <ReactMarkdown>
                {props.markdownBody}
            </ReactMarkdown>
        </div>   
    )
}

export default SingleBlog

export async function getStaticPaths() {
    const blogSlugs = ((context) => {
        const keys = context.keys()
        const data = keys.map((key, index) => {
          let slug = key.replace(/^.*[\\\/]/, '').slice(0, -3)
        return slug
    })
    return data
    })(require.context('../../data', true, /\.md$/))

    const paths = blogSlugs.map((blogSlug) => `/blog/${blogSlug}`) 

    return {
        paths: paths, 
        fallback: false,
    }
}

export async function getStaticProps(context) {    
    const { slug } = context.params
    const data = await import(`../../data/${slug}.md`)
    const singleDocument = matter(data.default)
    
    return {
      props: {
        frontmatter: singleDocument.data,         
        markdownBody: singleDocument.content,      
      }
    }
}

以上で、Next.jsにマークダウンのブログ機能を作成することができました。

ステップ1(コードの整理)

getStaticPropsgetStaticPathsの整理を最初に行います。

utilsフォルダを作り、その中にmdQueries.js ファイルを作成します。


nextjs-blog
├── data   
.
.
.
├── styles 
└── utils         ← 作成   
            └── mdQueries.js  ← 作成  

mdQueries.jsにはgetStaticPropsgetStaticPathsで行なっていた処理を移動し、同じ働きをするコードの共有を効率化します。

次のコードを打ちます。blog.js[slug].jsgetStaticPropsgetStaticPathsのコードとほぼ同じですが、getSingleBlog'../data'のパスが変わっていることに注意してください。

// mdQueries.js

import matter from 'gray-matter'

export async function getAllBlogs() {
    const blogs = ((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 = values[index]
            const document = matter(value.default)
            return {
                frontmatter: document.data,
                slug: slug
            }
        })
        return data
    })(require.context('../data', true, /\.md$/))

    const orderedBlogs = blogs.sort((a, b) => {
        return b.frontmatter.id - a.frontmatter.id
    })

    return {
        orderedBlogs: JSON.parse(JSON.stringify(orderedBlogs))
    }
}

export async function getSingleBlog(context) {
    const { slug } = context.params
    const data = await import(`../data/${slug}.md`)
    const singleDocument = matter(data.default)

    return {
        singleDocument: singleDocument
    }
}

これらをblog.js[slug].jsで読み込んで使うので、それぞれ次のように修正します。

// blog.js

import Link from 'next/link'
import matter from "gray-matter"    // 削除
import { getAllBlogs } from "../utils/mdQueries"    // 追加

...

export default Blog

export async function getStaticProps() {
    const { orderedBlogs } = await getAllBlogs()   // 置き換え

    return {
      props: {
        blogs: orderedBlogs  // 置き換え
      }
    }
}
// [slug].js

import matter from "gray-matter"   // 削除
import ReactMarkdown from 'react-markdown'
import { getAllBlogs, getSingleBlog } from "../../utils/mdQueries"  // 追加

...

export default SingleBlog

export async function getStaticPaths() {
    const { orderedBlogs } = await getAllBlogs()    // 置き換え
    const paths = orderedBlogs.map((orderedBlog) => `/blog/${orderedBlog.slug}`)    // 修正

    return {
        paths: paths,  
        fallback: false,
    }
}

export async function getStaticProps(context) {    
    const { singleDocument } = await getSingleBlog(context)   // 置き換え

    return {
      props: {
        frontmatter: singleDocument.data,         
        markdownBody: singleDocument.content,  
      }
    }
}

ステップ2(コードの修正)

ページあたりに表示する記事の数(blogsPerPage)と、記事の合計数から必要なページの数(numberPages)を計算するコードをmdQueries.mdに追加します。

// mdQueries.md

import matter from 'gray-matter'

export const blogsPerPage = 5       // 追加

export async function getAllBlogs() {
    const blogs = ((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 = values[index]
            const document = matter(value.default)
            return {
                frontmatter: document.data,
                slug: slug
            }
        })
        return data
    })(require.context('../data', true, /\.md$/))

    const orderedBlogs = blogs.sort((a, b) => {
        return b.frontmatter.id - a.frontmatter.id
    })

    const numberPages = Math.ceil(orderedBlogs.length / blogsPerPage)      // 追加

    return {
        orderedBlogs: JSON.parse(JSON.stringify(orderedBlogs)),  // ,を忘れないように。
        numberPages: numberPages       // 追加
    }
}

...

pagesフォルダのblogフォルダの中にpageフォルダを作り、その中に[pagination].jsを作ります。


src
.
.
├── pages 
│           ├── blog 
│           │        ├── page   ←追加
│           │        │           └── [pagination].js   ←追加
│           .        └── [slug].js
│           .  
.
.

[pagination].jsのコードは基本的にblog.jsと同じですが、いくつかの修正と必要なコードを追加します。

// [pagination].js

import Link from 'next/link'
import { getAllBlogs, blogsPerPage } from "../../../utils/mdQueries"  // パスの修正とblogsPerPageの追加

const PaginationPage = (props) => {     // 変更
    return (
        <>
            <p>ブログ一覧ページ</p> 
            {props.blogs.map((blog, index) => (
                <div  key={index}>
                    <h2>{blog.frontmatter.title}</h2>
                    <Link href={`/blog/${blog.slug}`}><a>Read More</a></Link>
                </div>
            ))}
        </>
         
    )
}

export default PaginationPage  // 変更

// ⬇追加
export async function getStaticPaths() {
    const { numberPages } = await getAllBlogs()

    let paths = []
    Array.from({ length: numberPages }).slice(0, 1).forEach((_, i) => paths.push(`/blog/page/${i + 2}`))

    return {
        paths: paths,  
        fallback: false,
    }
}
// ⬆追加

export async function getStaticProps(context) {       // 追加
    const { orderedBlogs, numberPages } = await getAllBlogs()   // 追加

    const currentPage = context.params.pagination   // 追加
    const limitedBlogs = orderedBlogs.slice((currentPage -1) * blogsPerPage, currentPage * blogsPerPage)    // 追加

    return {
      props: {
        blogs: limitedBlogs,    // 修正
        numberPages: numberPages,    // 追加
      }
    }
}

次にblog.jsでも表示するブログ記事を5つに制限する必要がありますが、ここは常にid番号の高い記事5つしたいので、次のように書きます。

// blog.js

import { getAllBlogs, blogsPerPage } from "../utils/mdQueries"  // 追加

...

export async function getStaticProps() { 
    const { orderedBlogs, numberPages } = await getAllBlogs() // 修正

    const limitedBlogs = orderedBlogs.slice(0, blogsPerPage)   // 追加
    
    return {            
        props: {
            blogs: limitedBlogs,  // 修正
            numberPages: numberPages, // 追加
        },      
    }                   
}  

ステップ3(コンポーネントの作成)

componentsフォルダを作り、その中にpagination.jsを作成します。


nextjs-blog
├── components                  ←追加
│           └── pagination.js    ←追加
├── data   
.
.
.

次のコードを書きます。

// pagination.js

import Link from 'next/link'

export const Pagination = ({ numberPages }) => {
  return (
    <h2>
      {Array.from({ length: numberPages }, (_, i) => (
          <Link key={i + 1} href={ i === 0 ? `/blog` : `/blog/page/${i + 1}`}>
            <a>{i + 1}</a>
          </Link>
      ))}
    </h2>
  )
}

export default Pagination

あとはこれをblog.js[pagination].jsで読み込み、次のようにするとページネーションが表示されます。

// blog.js

import Pagination from '../components/pagination'

const Blog = (props)) => {
  return (
    ...
        <Pagination numberPages={props.numberPages} />
    ...
// [pagination].js

import Pagination from '../../../components/pagination'

const PaginationPage = (props)) => {
  return (
    ...
        <Pagination numberPages={props.numberPages} />
    ...

以上でNext.jsにページネーションを追加することができました。

本記事では細かい説明を省略して足早に解説しましたが、Next.jsについてよりくわしく知りたい方は、拙著「はじめてつくるNext.jsサイト」を参考にしてください。

nextbook

【Amazonで見る】

前後の記事へと移動するリンクの作り方は、こちらの記事を参考にしてください。

またNext.jsを活用して近年注目を集めるJamstackについて知りたい方は、次の記事を参考にしてください。