Next.jsにページネーションをNPMパッケージなしで追加する方法
2021.12.252023.6.7
この記事は約3分で読めます
この記事の筆者:三好アキ
🔹 専門用語なしでプログラミングを教えるメソッドに定評があり、1200人以上のビギナーを、最新のフロントエンド開発入門に成功させる。
🔹 Amazonベストセラー1位を複数回獲得している『はじめてつくるReactアプリ with TypeScript』著者。
Amazon著者ページはこちら → amazon.co.jp/stores/author/B099Z51QF2
React、Next.js、TypeScriptなどのお役立ち情報や実践的コンテンツを、ビギナー向けにかみ砕いて無料配信中。登録はこちらから → 無料メルマガ登録
ページネーションとは?
記事数が増えたときに、1ページあたりに表示する記事の数を5つや10つなどに制限したいときに使うものがページネーションです。
1 2 3 4 5
本記事ではまず最初にNext.jsにマークダウンのブログ機能を実装し、その後ページネーションを作ります。
ロジック部分の説明だけなので、スタイルは各自お好みで適用してください。
なお前後の記事へと移動するリンクの作り方は、こちらの記事を参考にしてください。
Next.jsバージョン13で導入されたAppフォルダを使った最新のNext.js開発に興味のある方は、今月(2023年6月)リリースした下記書籍を参考にしてください。
マークダウンブログ機能の作成
まず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(コードの整理)
getStaticProps
とgetStaticPaths
の整理を最初に行います。
utils
フォルダを作り、その中にmdQueries.js
ファイルを作成します。
nextjs-blog
├── data
.
.
.
├── styles
└── utils ← 作成
└── mdQueries.js ← 作成
mdQueries.js
にはgetStaticProps
とgetStaticPaths
で行なっていた処理を移動し、同じ働きをするコードの共有を効率化します。
次のコードを打ちます。blog.js
と[slug].js
のgetStaticProps
、getStaticPaths
のコードとほぼ同じですが、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サイト」を参考にしてください。
前後の記事へと移動するリンクの作り方は、こちらの記事を参考にしてください。
またNext.jsを活用して近年注目を集めるJamstackについて知りたい方は、次の記事を参考にしてください。
メルマガ配信中
(from 三好アキ/エンジニア)
React、Next.js、TypeScriptなど最新のウェブ開発のお役立ち情報を、ビギナー向けにかみ砕いて無料配信中。
(*配信はいつでも停止できます)