Simple way to add a Table of Contents on a Gatsby blog post page

ReactJS|OCTOBER 9, 2024|83 VIEWS
This article will guide you in building a table of contents in Gatsby that dynamically generates links to the headings within a page.

Introduction

As a blog post grows longer, it can be challenging to comprehend its overall structure from a top-down read. To enhance the read-through rate and help users quickly grasp the content upon landing on the page, I decided to implement a table of contents.

Given that this blog is built with Gatsby, I'll walk you through the process of creating a table of contents in Gatsby.

Although I found the gatsby-remark-table-of-contents library, it doesn't fully meet my needs. I want the table of contents to display in the sidebar without having to include it in every MDX file.

How to Create a Table of Contents

Initially, I believed I would need to introduce a library to generate the TOC, but I discovered that it can be easily accomplished in Gatsby without any additional dependencies.

There’s a field called tableOfContents in the mdx query. By including it in your query as shown below, you can retrieve the table of contents data.

query ($id: String) {
    mdx(id: { eq: $id }) {
      id
      tableOfContents(maxDepth: 2)
      frontmatter {
        title
        subtitle
        date(formatString: "MMMM D, YYYY")
        tags
      }
      fields {
        slug
      }
    }
  }

The maxDepth parameter determines the depth of the headings included in the table of contents. If you don't specify it, the tableOfContents will only include h1 tags. To include h2 tags as well, I set maxDepth: 2.

For example, let's consider a Markdown structure with the following headers:

# First
## FirstChild1
## FirstChild2
# Second

The value of tableOfContents would then retrieve the following JSON structure:

{
  "items": [
    {
      "url": "#first",
      "title": "First",
      "items": [
        { "url": "#firstchild1", "title": "FirstChild1" },
        { "url": "#firstchild2", "title": "FirstChild2" }
      ]
    },
    { "url": "#second", "title": "Second" }
  ]
}

Now you can display the table of contents on the page by creating your own TableOfContents component.

import React from "react";

const TableOfContents = ({ data, hideOnLarge }: any) => {
  return (
    <div className={hideOnLarge ? "pb-4 lg:hidden" : "p-4"}>
      <div className="mb-2 font-semibold uppercase text-red-500">
        Table of Contents
      </div>
      {data.items.map((item: any) => (
        <div className="mb-3">
          <a className="font-semibold" href={item.url}>
            {item.title}
          </a>
          {!!item.items && (
            <div className="ml-4 mt-2">
              {item.items.map((child: any) => (
                <div className="mb-1">
                  <a href={child.url}>{child.title}</a>
                </div>
              ))}
            </div>
          )}
        </div>
      ))}
    </div>
  );
};

export default TableOfContents;

As the final step, display the table of contents on your blog post page. In my blog, I will show it in the sidebar.

import * as React from "react";
import { graphql } from "gatsby";
import TableOfContents from "path/to/TableOfContents";

const BlogPost = ({ data, children }: any) => {
  return (
    <main>
      <div className="mx-auto max-w-screen-xl flex flex-col lg:flex-row">
        <div className="w-1/4 hidden lg:block border-r border-gray-100 sticky top-0 h-screen sidebar">
          {!!data.mdx.tableOfContents?.items && (
            <TableOfContents data={data.mdx.tableOfContents} />
          )}
        </div>
        <div className="p-4 lg:flex-1">
          <h1 className="text-2xl font-bold mb-4 mt-4">
            {data.mdx.frontmatter.title}
          </h1>
          <div className="mt-4 [&>p]:mb-4 [&>pre]:mb-4 post">{children}</div>
        </div>
      </div>
      <Footer />
    </main>
  );
};

export const query = graphql`
  query ($id: String) {
    mdx(id: { eq: $id }) {
      id
      tableOfContents(maxDepth: 2)
      frontmatter {
        title
        subtitle
        date(formatString: "MMMM D, YYYY")
        tags
      }
      fields {
        slug
      }
    }
  }
`;

export default BlogPost;

Generate the anchor links

Now, every time a user clicks on a link in the table of contents, the page should scroll to that specific section. Additionally, if a user pastes a URL that includes a section identifier, the page will automatically scroll to that section by default. For example: https://salamina.tech/blog/post/gatsbyjs-page-views-google-analytics/#3-enable-google-analytics-data-api.

You'll need the help of the gatsby-remark-autolink-headers library for this functionality.

npm install gatsby-remark-autolink-headers
// In your gatsby-config.ts
import type { GatsbyConfig } from "gatsby";

const config: GatsbyConfig = {
  plugins: [
    {
      resolve: "gatsby-plugin-mdx",
      options: {
        extensions: [".mdx", ".md"],
        gatsbyRemarkPlugins: [
          {
            resolve: `gatsby-remark-autolink-headers`,
          },
          {
            resolve: "gatsby-remark-prismjs",
            options: {
              showLineNumbers: false,
              noInlineHighlight: false,
            },
          },
        ],
      },
    },
  ],
};

export default config;

Please note: If you are using gatsby-remark-prismjs, ensure that it is listed after this plugin. Otherwise, you may encounter issues.

That's all. I was able to easily display the table of contents without the need for a TOC generation library, which reinforced the convenience of using Gatsby.