Next.jsにマークダウンのブログ機能を追加する方法

Next.jsでブログ機能を実装するコードを紹介します。getStaticPaths()とgetStaticProps()を使います。

pen-icon2021.06.26rewrite-icon2021.07.11

svg
Profile Pic

Writer:三好

フロントエンド・エンジニア。これまで欧州数ヶ国に滞在し、海外クライアントの案件を多く手がけてきたため、最新のウェブテクノロジーや日本語の情報が少ない静的サイトジェネレーター、JAMstack、ヘッドレスCMSなどの最新情報に精通。最新の知見を活かしながら、ウェブ関連分野の課題解決のお手伝いを行っています。


monoteinの事業について詳しくはこちらをご覧ください。


• ビギナー向け教本『はじめてつくるReactアプリ』など5冊を販売中。

Next.jsでのブログ機能の作り方

静的サイトに特化したGatsbyでは、「gatsby-node.jsに必要なコードを書いてGraphQLを使う」という風に、ブログ機能を作る手順がある程度確立されていますが、フルスタックのアプリケーションでも使える多機能なNext.jsでは、自分でコードを書いていく必要があります。

本記事ではcreate-next-appを使って、ブログ機能の作り方を簡潔に紹介します。

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

ブログ機能の下準備

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

npx create-next-app nextjs-blog

ブログ機能に必要なものは、「ブログ記事を一覧表示するページ」、そして「個別の記事を表示するページ」、そして記事データです。

では最初に「ブログ記事を一覧表示するページ」から作っていきます。

srcの中は次のような構成になっているので、記事を一覧表示するページとなるblog.js、そして記事データのマークダウンファイルを収納するフォルダdataを作ります。

src
├── 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つ目の記事。
// three.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ファイルを、フォルダのトップレベルに作ります(srcフォルダやpublicフォルダの中に作らないよう注意)。

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に読み込まれました。

定数blogsreturnpropsに渡してやると、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))
      }
    }
}

これで記事一覧ページのロジックは完成です。

個別記事ページを作る

記事一覧ページから移動する個別の記事ページを作ります。

まずはテンプレートとして使うファイルを次のように作ります。

├── 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

...

以上でNext.jsにブログ機能を作成することができました。

Next.jsについてよりくわしく知りたい方は、拙著Kindle本「はじめてつくるNext.jsサイト」を参考にしてください。

ビギナーでもサイトスピードを改善できるガイドを無料配布しています。

arrow『スピード改善ガイド』無料ダウンロード

ユーザーから見つかりやすく、そしてユーザーを離脱させない高速ウェブサイト制作を専門とするmonotein

「これまでのホームページでは成果が出なかった」、「最新テクノロジーを使ったサイトが欲しい」、「SPAやJAMstack、ヘッドレスCMSなどのモダンな体制に移行したい」などウェブに関してお困りのことがありましたら、お気軽にご相談ください。

「待ち時間0秒」を体験arrowデモサイトを見る

無料相談