当サイトは、アフィリエイト広告を利用しています
contentfulにmdx形式で記事を格納しているGatsby製のブログで
記事から目次生成して表示する方法をまめる。
記事から目次を生成する方法は、調べたところ
の二通りある。※他にもあるかも。。
今回は、任意の場所に配置可能であり、また目次のレイアウトを独自に作れる
「自分で目次コンポーネントを作る」の方で実装した。
下記の前提条件の上で実装する
上記の条件を実装する方法は下記の記事で紹介しています。
Gatsbyでブログを書いている場合、「contentful × MDX形式」で記事を
管理するのはおススメです!
必要なパッケージをインストールする。
MDXで書いた記事の見出し要素(h1、h2、h3など)にidを入れてくれるプラグイン。
yarn add gatsby-remark-autolink-headers
GatsbyでMDX形式を扱うためのプラグイン。 繰り返しになりますが、使い方は下記記事で紹介してます。
インストールしたgatsby-remark-autolink-headersを
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`,],}
ここまででパッケージの設定は完了。
contetnfulの記事から記事とその記事の目次を取得して そのデータを元に目次生成をするコンポーネントを実装する
目次の元となる記事の記事の見出し要素(h1、h2、h3など)をSSGで生成する
template/配下の記事コンポーネント内のGraphQLのpageQueryで取得する。
下記のような感じでGraphiQLで記事のslugを条件にしてpageQueryで
childMdxの
を取得するGraphQLを作成する
※slugが未設定だとデータ取得できないため、「gatsby-page-state」を
仮条件として設定しています。
export const query = graphql`query Query($slug: String!) {allContentfulBlogPost(filter: { slug: { eq: $slug } }) {edges {node {body {childMdx {tableOfContentsbody}}}}}}`
slugを元に記事とその記事の目次データを取得している
作成したpageQueryをtemplate/配下の記事コンポーネントに反映させる
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].nodeconst { body } = postreturn (<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 {tableOfContentsbody}}}}}}`
これでtemplateコンポーネントで記事と同時にその記事の目次データを
取得できるようになった。
※まだ記事の目次データを取得しただけなので目次表示はされない
GraphiQLで取得した目次データを表示する目次表示用Componentを
作る。
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 MdxIndexconst hover = theme => {return [css`&:hover {background-color: ${theme.hoverColor};}cursor: pointer;`,]}
目次データは階層構造を持っていることについて
わかりにくいのでログで確認してみる。
上記のような目次の場合、目次データは下記のようになっている
[{"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)があり~のように
段落を階層的に持っているため再帰的に呼び出して表示させている。
目次表示コンポーネントができたので、記事コンポーネントから呼び出すように
追加する。
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].nodeconst { body } = postreturn (<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 {tableOfContentsbody}}}}}}`
完成した目次コンポーネントを表示させて動かしてみる
目次を押下するとその部分まで飛ぶようにできている。
完成したソースをまとめておく
~"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"},~
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].nodeconst { body } = postreturn (<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 {tableOfContentsbody}}}}}}`
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 MdxIndexconst hover = theme => {return [css`&:hover {background-color: ${theme.hoverColor};}cursor: pointer;`,]}
他の人の書いたブログや有名な技術ブログなどを見ると、目次があり
クリックするとそこの部分まで移動できるので、当ブログでもできようにしたいと
思い、実装してみた。
当ブログではこの目次にスムーズに移動させる方法や表示されている目次を
ハイライトする機能などをつけているので、その方法もどっかでまとめていく予定です。
目次にスムーズに移動させる方法を下記にまとめました!
Gatsbyの基本的な仕組みや環境設定方法など下記の書籍が参考になりました。
特に特典のセットアップPDFはversionが上がると更新してくれるようなので
Gatsbyのversionが上がった時なども今でも参考にしてます!