Next.jsのブログに前後記事へのリンクを追加する方法

blog-hero-imgNPMパッケージは使わずに、Next.jsに前後の記事へ移動するリンクを追加します。

pen-icon2021.12.25rewrite-icon2023.6.7

Profile Pic

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


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


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



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

前後の記事へのリンク

本記事ではまず最初に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(前後の記事の指定)

前後の記事へ移動するには、現在のページの一つ前と一つ後のページを特定する必要があります。

そこで必要なのが各マークダウンファイルのfrontmatter部分のidなので、まずマークダウンの全データを取得し、現在の記事のidの一つ前と一つ後を選びます。

次のコードを追加します。

// [slug].js

...

export async function getStaticProps(context) {    
    const singleDocument = await getSingleBlog(context)   

    ⬇追加
    const { orderedBlogs } = await getAllBlogs()
    const prev = orderedBlogs.filter(orderedBlog => orderedBlog.frontmatter.id === singleDocument.data.id - 1)
    const next = orderedBlogs.filter(orderedBlog => orderedBlog.frontmatter.id === singleDocument.data.id + 1)
    ⬆追加

    return {
      props: {
        frontmatter: singleDocument.data,     
        markdownBody: singleDocument.content,  
        prev,  // 追加(prev: prevも可)
        next,  // 追加(next: nextも可)
      }
    }
}

これで前後のページのデータがprevnextに格納されました。

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

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


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

そこに次のコードを打ちます。

// prevNext.js

import Link from 'next/link'

const PrevNext =(props) => {
    return (
        <div>
            {0 < props.prev.length && 
                <Link href={`/blog/${props.prev[0].slug}`}>
                    <a><h3> {props.prev[0].frontmatter.title}</h3></a>
                </Link>
            }
            {0 < props.next.length && 
                <Link href={`/blog/${props.next[0].slug}`}>
                    <a><h3>{props.next[0].frontmatter.title}</h3></a>
                </Link>
            }
        </div>
    )
}

export default PrevNext 

これを[slug].jsで読み込み、propsprevnextを渡します。

[slug].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>
            <PrevNext prev={prev} next={next} />
        </div>   
    )
}

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)   

    const { orderedBlogs } = await getAllBlogs()
    const prev = orderedBlogs.filter(orderedBlog => orderedBlog.frontmatter.id === singleDocument.data.id - 1)
    const next = orderedBlogs.filter(orderedBlog => orderedBlog.frontmatter.id === singleDocument.data.id + 1)

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

以上でNext.jsに前後の記事へのリンクを追加することができました。

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

nextbook

【Amazonで見る】

Next.jsのページネーション機能は、こちらの記事を参考にしてください。

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