当サイトは、アフィリエイト広告を利用しています

【Gatsby × contentful】MDX形式記事の目次をつける方法

作成日:2022月11月07日
更新日:2022年11月17日

contentfulにmdx形式で記事を格納しているGatsby製のブログで
記事から目次生成して表示する方法をまめる。

記事から目次を生成する方法は、調べたところ

  • gatsby-remark-table-of-contentsを使う
  • 自分で目次コンポーネントを作る

の二通りある。※他にもあるかも。。 今回は、任意の場所に配置可能であり、また目次のレイアウトを独自に作れる
「自分で目次コンポーネントを作る」の方で実装した。

前提

下記の前提条件の上で実装する

  • Gatsby製ブログの記事をcontentfulで管理していること
  • Gatsby製ブログの記事をMDX形式にしていること

上記の条件を実装する方法は下記の記事で紹介しています。
Gatsbyでブログを書いている場合、「contentful × MDX形式」で記事を
管理するのはおススメです!

パッケージのインストール

必要なパッケージをインストールする。

MDXで書いた記事の見出し要素(h1、h2、h3など)にidを入れてくれるプラグイン。  

GitBash
yarn add gatsby-remark-autolink-headers

gatsby-plugin-mdx

GatsbyでMDX形式を扱うためのプラグイン。 繰り返しになりますが、使い方は下記記事で紹介してます。

プラグインをgatsby-config.jsに設定する

インストールしたgatsby-remark-autolink-headersを
gatsby-config.jsに追記して使えるようにする

gatsby-config.js
require("dotenv").config({
path: `.env.${process.env.NODE_ENV}`,
})
module.exports = {
~
plugins: [
`gatsby-plugin-image`,
`gatsby-transformer-sharp`,
`gatsby-plugin-sharp`,
{
resolve: `gatsby-source-contentful`,
options: {
spaceId: process.env.spaceId,
accessToken: process.env.accessToken,
},
},
{
resolve: `gatsby-plugin-mdx`,
options: {
gatsbyRemarkPlugins: [
// 他にRemarkプラグインがある場合はここに追加していく
`gatsby-remark-autolink-headers`,
],
},
},
`gatsby-plugin-react-helmet`,
],
}
  • gatsby-plugin-mdxのオプションに追加する。

ここまででパッケージの設定は完了。

記事の目次表示を実装してみる

contetnfulの記事から記事とその記事の目次を取得して そのデータを元に目次生成をするコンポーネントを実装する

templateコンポーネントで目次データを取得する

目次の元となる記事の記事の見出し要素(h1、h2、h3など)をSSGで生成する
template/配下の記事コンポーネント内のGraphQLのpageQueryで取得する。

記事データと同時に目次データを取得するGraphQLを作成する

下記のような感じでGraphiQLで記事のslugを条件にしてpageQueryで
childMdxの

  • body
  • tableOfContents

を取得するGraphQLを作成する

2022-11-03-21-39-08

※slugが未設定だとデータ取得できないため、「gatsby-page-state」を
仮条件として設定しています。

GraphiQLで作成したものを元にPageQueryを作成する

pageQuery
export const query = graphql`
query Query($slug: String!) {
allContentfulBlogPost(filter: { slug: { eq: $slug } }) {
edges {
node {
body {
childMdx {
tableOfContents
body
}
}
}
}
}
}
`
  • ($slug: String!)はクエリ変数
  • tableOfContentsが目次データ
  • bodyは記事

slugを元に記事とその記事の目次データを取得している

template/配下の記事コンポーネントに反映させる

作成したpageQueryをtemplate/配下の記事コンポーネントに反映させる

templates/contentfulPost.jsx
import React from "react"
import { graphql, Link } from "gatsby"
import MDXConvert from "../components/mdxConvert"
import { css } from "@emotion/react"
const ContentfulPost = ({ data }) => {
const post = data.allContentfulBlogPost.edges[0].node
const { body } = post
return (
<div>
<MDXConvert>{body.childMdx.body}</MDXConvert>
</div>
)
}
export default ContentfulPost
// gatsby-node.jsのcreatePageから渡されたslugを元にGraphQLで記事と
// その目次データ取得する
export const query = graphql`
query Query($slug: String!) {
allContentfulBlogPost(filter: { slug: { eq: $slug } }) {
edges {
node {
body {
childMdx {
tableOfContents
body
}
}
}
}
}
}
`

これでtemplateコンポーネントで記事と同時にその記事の目次データを
取得できるようになった。
※まだ記事の目次データを取得しただけなので目次表示はされない

目次表示用Componentを作る

GraphiQLで取得した目次データを表示する目次表示用Componentを
作る。

components/mdxIndex/index.jsx
import React from "react"
import { css } from "@emotion/react"
import { Link } from "gatsby"
const MdxIndex = ({ items }) => {
//itemsのデータ構造確認用
console.log(JSON.stringify(items, null, 3))
return (
<ul>
{items.map(item => (
<li key={item.url}>
<Link css={[hover]} to={item.url}>
{/* {item.url} */}
{item.title}
</Link>
{/* 再帰的に呼び出す */}
{item.items && <MdxIndex items={item.items} />}
</li>
))}
</ul>
)
}
export default MdxIndex
const hover = theme => {
return [
css`
&:hover {
background-color: ${theme.hoverColor};
}
cursor: pointer;
`,
]
}
  • propsの目次データ(items)をLinkタグで作成してクリックで移動できるようにする
  • 目次データは階層構造なので、再帰的に呼び出す必要がある

GraphiQLで取得した目次データの構造を見てみる

目次データは階層構造を持っていることについて
わかりにくいのでログで確認してみる。

2022-11-07-01-17-49

上記のような目次の場合、目次データは下記のようになっている

目次データ構造の確認
[
{
"url": "#vscodeでデバッグする",
"title": "VSCodeでデバッグする",
"items": [
{
"url": "#gatsby-nodejsのデバッグ",
"title": "gatsby-node.jsのデバッグ",
"items": [
{
"url": "#ブレークポイントが無効化される場合がある",
"title": "ブレークポイントが無効化される場合がある"
}
]
},
{
"url": "#画面表示時に実行されるコードのデバッグ方法",
"title": "画面表示時に実行されるコードのデバッグ方法"
}
]
},
{
"url": "#chromeデベロッパーツールでデバッグする",
"title": "Chromeデベロッパー・ツールでデバッグする",
"items": [
{
"url": "#ノード用chromedevtoolsでgatsby-nodejsをデバッグする",
"title": "ノード用ChromeDevToolsでgatsby-node.jsをデバッグする",
"items": [
{
"url": "#デバッグを起動する",
"title": "デバッグを起動する",
"items": [
{
"url": "#自分でつけたブレークポイントまで動かす場合",
"title": "自分でつけたブレークポイントまで動かす場合"
},
{
"url": "#最初の行にブレークポイントを自動的に配置して止める場合",
"title": "最初の行にブレークポイントを自動的に配置して止める場合"
}
]
},
{
"url": "#ノード用のchromedevtoolsを開く",
"title": "ノード用のChromeDevToolsを開く"
},
{
"url": "#フォルダを追加する",
"title": "フォルダを追加する"
}
]
},
{
"url": "#画面表示時に実行されるコードのデバッグ方法-1",
"title": "画面表示時に実行されるコードのデバッグ方法"
}
]
},
{
"url": "#まとめ",
"title": "まとめ",
"items": [
{
"url": "#参考",
"title": "参考"
},
{
"url": "#参考書籍",
"title": "参考書籍"
}
]
}
]

要はh1タグの中にh2タグの配列(items)があり、h2タグの中にh3タグの配列(items)があり~のように
段落を階層的に持っているため再帰的に呼び出して表示させている。

目次表示コンポーネントを記事コンポーネントから呼び出す

目次表示コンポーネントができたので、記事コンポーネントから呼び出すように
追加する。

templates/contentfulPost.jsx
import React from "react"
import { graphql, Link } from "gatsby"
import MDXConvert from "../components/mdxConvert"
import MdxIndex from "../components/MdxIndex"
import { css } from "@emotion/react"
const ContentfulPost = ({ data }) => {
const post = data.allContentfulBlogPost.edges[0].node
const { body } = post
return (
<div>
<MdxIndex items={body.childMdx.tableOfContents.items} />
<MDXConvert>{body.childMdx.body}</MDXConvert>
</div>
)
}
export default ContentfulPost
// gatsby-node.jsのcreatePageから渡されたslugを元にGraphQLで記事と
// その目次データ取得する
export const query = graphql`
query Query($slug: String!) {
allContentfulBlogPost(filter: { slug: { eq: $slug } }) {
edges {
node {
body {
childMdx {
tableOfContents
body
}
}
}
}
}
}
`
  • GraphiQLで取得したtableOfContentsのitemsを目次表示コンポーネントに渡すようにする。

実際に動かしてみる

完成した目次コンポーネントを表示させて動かしてみる

目次完了

目次を押下するとその部分まで飛ぶようにできている。

完成ソース

完成したソースをまとめておく

package.json

package.json
~
"dependencies": {
"@babel/preset-react": "^7.17.12",
"@emotion/babel-plugin": "^11.9.2",
"@emotion/react": "^11.9.0",
"@emotion/styled": "^11.8.1",
"@mdx-js/mdx": "1.6.22",
"@mdx-js/react": "1.6.22",
"dotenv": "^16.0.2",
"gatsby": "^4.14.1",
"gatsby-plugin-emotion": "^7.14.0",
"gatsby-plugin-feed": "^4.14.0",
"gatsby-plugin-gatsby-cloud": "^4.14.0",
"gatsby-plugin-google-analytics": "^4.14.0",
"gatsby-plugin-image": "^2.24.0",
"gatsby-plugin-manifest": "^4.14.0",
"gatsby-plugin-mdx": "3.20.0",
"gatsby-plugin-offline": "^5.14.1",
"gatsby-plugin-react-helmet": "^5.14.0",
"gatsby-plugin-sharp": "^4.14.1",
"gatsby-remark-autolink-headers": "^5.24.0",
"gatsby-remark-copy-linked-files": "^5.14.0",
"gatsby-remark-images": "^6.14.0",
"gatsby-remark-prismjs": "^6.14.0",
"gatsby-remark-responsive-iframe": "^5.14.0",
"gatsby-remark-smartypants": "^5.14.0",
"gatsby-source-contentful": "^7.21.1",
"gatsby-source-filesystem": "^4.14.0",
"gatsby-transformer-sharp": "^4.14.0",
"prismjs": "^1.28.0",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"react-helmet": "^6.1.0",
"typeface-merriweather": "0.0.72",
"typeface-montserrat": "0.0.75"
},
~

記事コンポーネント

templates/contentfulPost.jsx
import React from "react"
import { graphql, Link } from "gatsby"
import MDXConvert from "../components/mdxConvert"
import MdxIndex from "../components/MdxIndex"
import { css } from "@emotion/react"
const ContentfulPost = ({ data }) => {
const post = data.allContentfulBlogPost.edges[0].node
const { body } = post
return (
<div>
<MdxIndex items={body.childMdx.tableOfContents.items} />
<MDXConvert>{body.childMdx.body}</MDXConvert>
</div>
)
}
export default ContentfulPost
// gatsby-node.jsのcreatePageから渡されたslugを元にGraphQLで記事と
// その目次データ取得する
export const query = graphql`
query Query($slug: String!) {
allContentfulBlogPost(filter: { slug: { eq: $slug } }) {
edges {
node {
body {
childMdx {
tableOfContents
body
}
}
}
}
}
}
`

目次表示コンポーネント

components/mdxIndex/index.jsx
import React from "react"
import { css } from "@emotion/react"
import { Link } from "gatsby"
const MdxIndex = ({ items }) => {
//itemsのデータ構造確認用
console.log(JSON.stringify(items, null, 3))
return (
<ul>
{items.map(item => (
<li key={item.url}>
<Link css={[hover]} to={item.url}>
{/* {item.url} */}
{item.title}
</Link>
{/* 再帰的に呼び出す */}
{item.items && <MdxIndex items={item.items} />}
</li>
))}
</ul>
)
}
export default MdxIndex
const hover = theme => {
return [
css`
&:hover {
background-color: ${theme.hoverColor};
}
cursor: pointer;
`,
]
}

まとめ

他の人の書いたブログや有名な技術ブログなどを見ると、目次があり
クリックするとそこの部分まで移動できるので、当ブログでもできようにしたいと
思い、実装してみた。
当ブログではこの目次にスムーズに移動させる方法や表示されている目次を
ハイライトする機能などをつけているので、その方法もどっかでまとめていく予定です。

目次にスムーズに移動させる方法を下記にまとめました!

参考書籍

Gatsbyの基本的な仕組みや環境設定方法など下記の書籍が参考になりました。
特に特典のセットアップPDFはversionが上がると更新してくれるようなので
Gatsbyのversionが上がった時なども今でも参考にしてます!

参考

新着記事

top