Table of contents is quite common and useful. I had an old solution found online and it works, but today I spent some time and created my own version that provides me much more flexibility.

An Old Yet Quicker Solution

Step 1
Gatsby uses GraphQL to query the data and it provides the field of table of content. Therefore simply add the field to the query that you use to get the post data:

  query BlogPostBySlug($slug: String!) {
    markdownRemark(fields: { slug: { eq: $slug } }) {
      id
      fields {
        slug
      }
      tableOfContents ( maxDepth: 2)
      ...
    }
  }

It also supports to define the depth, for example, I used 2 as I want to keep the table succinct.

Step 2
Now if you pass the data to your template, can just render it as raw HTML directly, something like this:

export default function TableOfContents({ post }) {
  if (post.tableOfContents === "") {
    return (
      <></>
    )
  }

  return (
    <>
    <div className={styles.tableOfContents}>
      <h2>Table of Contents</h2>
      <section 
        dangerouslySetInnerHTML={{ __html: post.tableOfContents }} />
    </div>
    </>
  )
}

Easy, right? You can stop if you are just looking for a simple solution.

The problem is that it generates the table of content in a complete section of raw HTML, you sort of losing the flexibility, that's why I re-implemented it in another way.

Alternatives I didn't try

Since Gatsby is essentially a React framework, so anything that works as a Javascript library will just work in Gatsby.

Here is a npm package that generates the table of contents.

The reason that I didn't use it because:

  1. You need include a toc section in each .md file you use, while implement in the component React will generate for you each post for free
  2. You can't customize the generated HTML easily either

Re-create One from Data

There is another way of getting the table of contents data:

  headings {
    depth
    value
  }

By adding above query in the GraphQL, you get data like this:

"headings": [
  {
    "depth": 2,
    "value": "Header 2"
  },
  {
    "depth": 3,
    "value": "Header 3"
  },
  {
    "depth": 3,
    "value": "another Header 3"
  },
  ...

So why not just generate a table of contents, but in an HTML way? A simple solution can just combine HTML and CSS together. For example, can iterate the data and create something like this:

<ul>
  <li class="header-2">Header 2</li>
  <li class="header-3">Header 3</li>
  <li class="header-3">another Header 3</li>
</ul>

Then changes the styles for each class, for example, apply different padding for different levels of headers. This one can work and it is quite flexible since now you are controling the elements. Instead of list element, you can use other HTML elements.
You can stop reading if you don't want something more complicated.

Add Some Flavor of Algorithm

Note this step maybe is an overkill, but I did it for fun and just want to prove it's doable.

So I want something looks exactly the same as the previous raw HTML returned from Gatsby GraphQL query, but generated from the data instead. Taking a look at the old HTML, it's something like this:

For a markdown content:

# Header 1
# Header 2

It has the HTML as

<ul>
  <li>Header 1</li>
  <li>Header 2</li>
</ul>

However, if you have a child header, such as this:

# Header 1
# Header 2
## Header 2-1

It generates HTML as:

<ul>
  <li>Header 1</li>
  <li>
    <p>Header 2</p>
    <ul>
      <li>Header 2-1</li>
    </ul>
</ul>

Things also got more tricky if you have a child header before (what kind of people does that?!):

## Header 0-1 
# Header 1

It looks like this:

<ul>
  <li>
    <ul>
      <li>Header 0-1</li>
    </ul>
  </li>
  <li>Header 1</li>
</ul>

The rule of thumb is that each level of the headers should be in the same <ul> element and each header is a <li>. However, if a header has child headers, itself needs to be in <p> and its child headers will be wrapped inside a <ul> as a list. So this is a very straightforward implementation of using a tree and recursively render the HTML. Maybe there are fancier solution, but mine just worked, yay.

Talk is Cheap

Here is the code: Gist

It basically does 3 things:

  1. Find the minimal depth (largest header level, e.g., depth 1 for header level 1), which will be the depth for the root node (Because if your largest header is level 2, you don't want to wrap an extra level of <ul><li></li</ul>)
  2. Build an array of root nodes (_createNodes)
  3. Render each root node (HeaderNode.render())

Check this new thing to the right side, isn't it cute? :D