ホーム > Gatsby > Gatsby.jsの記事の目次を自動で生成する
Gatsby

Gatsby.jsの記事の目次を自動で生成する

いつもご利用ありがとうございます。
この記事には広告が掲載されており、その広告費によって運営しています。

Gatsby.js のマークダウンで書いた記事に目次を自動で生成する方法についてまとめました。

はじめに

これから紹介する方法は、気合いで作っています。

自分の場合、

  • h2 タグに id を付与
  • h2 タグの前にアドセンス広告
  • 英語ページの内部リンクにだけ/en/を追加

というようなことをしているため、パッケージを使ったらうまく動作しなかったため今回のやり方に至りました。

本来のやり方

gatsby-remark-table-of-contentsを使うのが最も早いと思います。

自分のやり方

gatsby-node.js で context に tocs を渡す

//ele: 'h1'|'h2'|'h3'|'p'| etc...
function findText(children, ele) {
  if (children.tagName === ele) {
    const text = children.children[0].value
    const object = {
      element: ele,
      text: text,
    }
    return object
  }
}

function generateToc(htmlAst) {
  const tocArray = []
  const childrens = htmlAst.children
  // H2要素を見つけて目次に追加
  // (H3も見出しにしたい場合少し以下の部分を変えればいける
  childrens.forEach(children => {
    if (children.tagName === "h2") {
      const obj = findText(children, "h2")
      console.log(obj) //{ element: 'h2', text: 'はじめに' }
      tocArray.push(obj)
      console.log(tocArray) // [ { element: 'h2', text: 'はじめに' },{ element: 'h2', text: '本来のやり方' }]
    }
  })
  return tocArray
}

postResult.edges.forEach(({ node }) => {
  const path = `${node.fields.slug}`
  const htmlAst = node.htmlAst
  const tocs = generateToc(htmlAst)
  createPage({
    path: path,
    component: blogPost,
    context: {
      slug: path,
      tocs: tocs,
    },
  })
})

ローカルサーバーをリスタートさせる。

コンポーネントで pageContext を使って tocs を元に目次作成

const PostTemplate = ({ data, pageContext }) => {
  const tocs = pageContext.tocs
  return (
    <Layout>
      // <Toc tocs={tocs} />
      <ol>
        {tocs.map((toc, index) => (
          <li key={`toc` + index} className="toc-h2">
            <a href={`#${toc.text}`}>{toc.text}</a>
          </li>
        ))}
      </ol>
    </Layout>
  )
}

pageContext で tocs さえ無事取得出来ていれば、表示は単純な HTML で、自由に CSS を与えられます。

見出しをクリックして、ページ内リンクで飛ばすために、以下のように href を与えています。

<a href={`#${toc.text}`}>{toc.text}</a>

この後 h2 タグに id を付与する記述があるので、今の段階ではページ内リンクは動かないです。

H2 タグに id="見出しのテキスト"を付与する

npm install rehype-react

ドキュメント

import rehypeReact from "rehype-react"
import React, { useEffect, useState } from "react"

const renderAst = new rehypeReact({
  createElement: React.createElement,
  // components: {
  //   自分はここにいくつかComponentが入る
  // },
}).Compiler

const Post = ({ data, pageContext }) => {
  const [isAdd, setIsAdd] = useState(false) //何度も関数が動くケースがあったのを対策
  const { htmlAst } = data.post
  const tocs = pageContext.tocs

  function addIdToH2(tree) {
    if (isAdd) return //1回だけしか動かさない
    visit(tree, "element", node => {
      if (node.tagName === "h2") {
        const textNode = node.children.find(child => child.type === "text") //typeの例外に対応
        if (textNode) {
          const text = textNode.value.trim() //見出しに半角スペースなどがある際に対応
          node.properties = { ...node.properties, id: text } //idに見出しのテキストと同じものを付与。日本語なら日本語になる。見出しが同じものだった場合はページ内リンク動かないが、それは想定しない
        }
      }
    })
    setIsAdd(true) //1回だけしか動かさない
  }

  useEffect(() => {
    addIdToH2(htmlAst) // htmlAstは、tree上に出力される
  }, [htmlAst])

  return (
    <Layout>
      <ol>
        {tocs.map((toc, index) => (
          <li key={`toc` + index} className="toc-h2">
            <a href={`#${toc.text}`}>{toc.text}</a>
          </li>
        ))}
      </ol>
      <div className="my-20 post-body">{renderAst(htmlAst)}</div>
    </Layout>
  )
}

export const query = graphql`
  query PostBySlug($slug: String!) {
    post: markdownRemark(
      fields: { slug: { eq: $slug } }
    ) {
      id
      htmlAst //追加
      //以下略

メリット

自由度が高いことがこの方法の一番のメリットだと思います。

見出しをカスタマイズしやすく、自由に出来ます。

また、見出しを配列にするのがビルドの際なので、ビルド時間を犠牲に表示速度を得ました。

あと、色々仕組みを理解できたのが良かったです。

デメリット

ID の付与をブラウザ側の処理に持たせているため表示速度が遅いはずです。

計測上誤差の範囲内だったのと、体感で差がわからなかったので正直あんまり気にしていないです。

まとめ

自分のようになんらかの事情があってプラグインを使えない人の参考になれば幸いです!