ホーム > Gatsby > Automatically Generating a Table of Contents for Gatsby.js Articles
Gatsby

Automatically Generating a Table of Contents for Gatsby.js Articles

Thank you for your continued support.
This article contains advertisements that help fund our operations.

This article summarizes how to automatically generate a table of contents for articles written in Markdown in Gatsby.js.

Introduction

The method introduced here is something I built through trial and error.

In my case, I do the following:

  • Add an id to h2 tags
  • Insert Adsense ads before h2 tags
  • Add /en/ only to internal links for the English pages

Since the package didn't work well with these requirements, I came up with this method.

The Recommended Way

I think using gatsby-remark-table-of-contents is the quickest approach.

My Approach

Pass tocs to context in gatsby-node.js

//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
  // Find H2 elements and add them to the table of contents
  // (You can also include H3 headings by slightly modifying this part)
  childrens.forEach(children => {
    if (children.tagName === "h2") {
      const obj = findText(children, "h2")
      console.log(obj) // { element: 'h2', text: 'Introduction' }
      tocArray.push(obj)
      console.log(tocArray) // [ { element: 'h2', text: 'Introduction' }, { element: 'h2', text: 'The Recommended Way' }]
    }
  })
  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,
    },
  })
})

Restart the local server.

Create the Table of Contents Using pageContext in the Component

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>
  )
}

As long as tocs is properly retrieved through pageContext, the display is simple HTML, and you can apply CSS as you wish.

To link the headings and jump to the corresponding section on the page, the following href is used:

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

In the following steps, you'll add an id to the h2 tags, so at this point, the internal page links won't work yet.

Add id="heading text" to h2 Tags

npm install rehype-react

Documentation

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

const renderAst = new rehypeReact({
createElement: React.createElement,
// components: {
// I include several components here
// },
}).Compiler

const Post = ({ data, pageContext }) => {
const [isAdd, setIsAdd] = useState(false) // To prevent the function from running multiple times
const { htmlAst } = data.post
const tocs = pageContext.tocs

function addIdToH2(tree) {
if (isAdd) return // Run only once
visit(tree, "element", node => {
if (node.tagName === "h2") {
const textNode = node.children.find(child => child.type === "text") // Handling for exceptions in the type
if (textNode) {
const text = textNode.value.trim() // Handling cases where there are spaces in the heading
node.properties = { ...node.properties, id: text } // Assign the heading text to the id. If it's in Japanese, the id will be in Japanese. If there are duplicate headings, the internal page link won't work, but that isn't considered.
}
}
})
setIsAdd(true) // Run only once
}

useEffect(() => {
addIdToH2(htmlAst) // `htmlAst` is output on the 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 // added
// omitted
===

## Advantages

I think the biggest advantage of this method is the high degree of freedom.

It's easy to customize the headings and do as you like.

Also, since the table of contents is generated during the build process, we trade build time for faster rendering speed.

Plus, I was able to understand the mechanism behind it, which was great.

## Disadvantages

Since the id assignment is done on the browser side, the rendering speed might be slower.

However, based on measurements, it was within the margin of error, and I didn't notice any performance differences, so I don't worry about it much.

## Conclusion

I hope this method will be helpful for those who, like me, have reasons not to use plugins!
Please Provide Feedback
We would appreciate your feedback on this article. Feel free to leave a comment on any relevant YouTube video or reach out through the contact form. Thank you!