Next.jsにマークダウンのブログ機能を追加する方法
2021.06.262021.12.25
この記事は約3分で読めます
この記事の筆者:三好アキ
🔹 専門用語なしでプログラミングを教えるメソッドに定評があり、1200人以上のビギナーを、最新のフロントエンド開発入門に成功させる。
🔹 Amazonベストセラー1位を複数回獲得している『はじめてつくるReactアプリ with TypeScript』著者。
Amazon著者ページはこちら → amazon.co.jp/stores/author/B099Z51QF2
React、Next.js、TypeScriptなどのお役立ち情報や実践的コンテンツを、ビギナー向けにかみ砕いて無料配信中。登録はこちらから → 無料メルマガ登録
Next.jsでのブログ機能の作り方
Next.jsと同じReactベースのGatsbyでは、「gatsby-node.js
に必要なコードを書いてGraphQLを使う」という風に、ブログ機能を作る手順やデータを扱う方法が標準化されていますが、フルスタックのアプリケーションでも使える多機能なNext.jsでは、自分でコードを書いていく必要があります。
本記事ではcreate-next-app
を使って、ブログ機能の作り方を簡潔に紹介します。
ロジック部分の説明だけなので、スタイルは各自好みで設定してください。
なおNext.jsで作られた国内外のウェブサイトやアプリは、こちらの記事を参考にしてください。
また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
のバージョンによってはデフォルトですでに用意されている場合があります)。
next.config.js
には下のコードを書きます。
// next.config.js
module.exports = {
webpack: function (config) {
config.module.rules.push({
test: /\.md$/,
use: "raw-loader",
})
return config
},
}
これで下準備が完了です。
記事一覧ページを作る
Next.jsにはデータ操作のためのファンクションが用意されているので、そのうちの一つ、getStaticProps()
を使って、マークダウンのデータを読み込みます。
blog.js
を開き、次のコードを追加します。
// blog.js
const Blog = () => {
return (
<p>ブログ一覧ページ</p>
)
}
export default Blog
export async function getStaticProps() {
return {
props: {
},
}
}
getStaticProps()
には次のようにコードを追加します。
// blog.js
import matter from "gray-matter"
...
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$/))
return {
props: {
},
}
}
こうすることで、マークダウンファイル(.md
)が、このblog.js
に読み込まれました。
定数blogs
をreturn
のprops
に渡してやると、Blogコンポーネントにprops
として渡されます。
// blog.js
import matter from "gray-matter"
const Blog = (props) => {
return (
<p>ブログ一覧ページ</p>
)
}
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$/))
return {
props: {
blogs: blogs
},
}
}
ここでエラーが出る場合は、次のようにJSON操作のコードを加えてください。
...
return {
props: {
blogs: JSON.parse(JSON.stringify(blogs))
},
}
また、この時点ではブログの表示される順番が乱れているので、マークダウンファイルのid
の高い順に並び替えます。
sortingArticles
というファンクションを作り、次のように追加します。props
に渡すものもsortingArticles
に変更します。
// blog.js
...
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))
}
}
}
/blog
ページに、記事タイトルと個別記事へのリンクを表示させるコードを追加します。
// 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
...
これで記事一覧ページのロジックは完成です。
個別記事ページを作る
記事一覧ページから移動する個別の記事ページを作ります。
まずはテンプレートとして使うファイルを次のように作ります。
├── pages
│ ├── api
│ ├── blog ←作成
│ │ └── [slug].js ←作成
│ ├── _app.js
│ ├── blog.js
│ └── index.js
[slug].js
では、最初にgetStaticPaths()
を使ってマークダウンのファイル名からURLを生成します。
次のコードを追加します。
// [slug].js
const SingleBlog = () => {
return (
<p>個別の記事ページ</p>
)
}
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,
}
}
これでURL(= slug)の生成が完了です。
次にマークダウンのデータを読み込みます。ここは先ほどのblog.js
とほぼ同じです。
違いは必要な記事データ一つだけを読み込むことで、すべてのマークダウンファイルではないことです。
// [slug].js
import matter from "gray-matter"
...
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,
}
}
}
これでblog.js
と同じように、SingleBlogコンポーネントにprops
として、そのページで必要なマークダウンのデータが渡されました。
マークダウンの本文部分の処理には、先ほどダウンロードしたreact-markdown
が必要なので、追加します。
// [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 children={props.markdownBody} />
</div>
)
}
export default SingleBlog
...
<ReactMarkdown children={props.markdownBody} />
でエラーが出る場合は、次のようにしてください。
<ReactMarkdown>
{props.markdownBody}
</ReactMarkdown>
以上でNext.jsにブログ機能を作成することができました。
ページネーション機能を追加したい人は、次の記事を参考にしてください。
前後の記事へと移動するリンクの作り方は、こちらの記事を参考にしてください。
本記事では細かい説明を省略して足早に解説しましたが、Next.jsについてよりくわしく知りたい方は、拙著「はじめてつくるNext.jsサイト」を参考にしてください。
またNext.jsを活用して近年注目を集めるJamstackについて知りたい方は、次の記事を参考にしてください。
メルマガ配信中
(from 三好アキ/エンジニア)
React、Next.js、TypeScriptなど最新のウェブ開発のお役立ち情報を、ビギナー向けにかみ砕いて無料配信中。
(*配信はいつでも停止できます)