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.
Table Of Contents
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
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!