Building a Table of Contents (TOC) from markdown for your React blog

Building a Table of Contents (TOC) from markdown for your React blog

Since I store blog posts in a self-hosted version of strapi, I've been looking for a way to automatically generate a table of contents from Markdown for all posts in my Next.js site.

The idea is that during the build process all captions are extracted from the article content (I use getStaticProps for all articles) and then display them fixed next to the content using a separate component.

Extracting headers with regex from markdown

After some research and trial and error I decided to use regex to extract the headers from the markdown text using the hash symbol.

Since there are links in the markdown text with anchor elements and codeblocks that also contains hash symbols which will be misinterpreted as headers, these are removed first from the whole text.

js
Copy code
const regexReplaceCode = /(```.+?```)/gms
  const regexRemoveLinks = /\[(.*?)\]\(.*?\)/g

  const markdownWithoutLinks = markdown.replace(regexRemoveLinks, "")
  const markdownWithoutCodeBlocks =  markdownWithoutLinks.replace(regexReplaceCode, "")

Then, using the hash symbol, the headings h1 to h6 are filtered from the text and added to an array named titles.

js
Copy code
const regXHeader = /#{1,6}.+/g
  const titles = markdownWithoutCodeBlocks.match(regXHeader)

Next, using the headings, levels of headings, titles, and anchor links are created and added to an array toc so that the headings can later be nested with child headings and anchor links can be added. The anchor links can then be used to jump from the table of contents to a heading.

js
Copy code
let globalID = 0
titles.map((tempTitle, i) => {
      const level = tempTitle.match(/#/g).length - 1
      const title = tempTitle.replace(/#/g, "").trim("")
      const anchor = `#${title.replace(/ /g, "-").toLowerCase()}`
      level === 1 ? (globalID += 1) : globalID

      toc.push({
        level: level,
        id: globalID,
        title: title,
        anchor: anchor,
      })
    })

The array toc is returned and I pass this for example as post.toc to the respective post, where post.toc in turn is passed as props to the ToC component.

js
Copy code
export async function getStaticProps({ params }) {
  const content = (await data?.posts[0]?.content) || ""
  const toc = getToc(content)

  return {
    props: {
      post: {
        content,
        toc
      },
    },
  }
}

Rendering the table of contents

Each element from the toc array is now added to the table of contents component. The levels variable is used to dynamically create indentation for subordinate headings with margin and the anchor is used for links.

js
Copy code
import styled from "styled-components"

const ToCListItem = styled.li`
  list-style-type: none;
  margin-bottom: 1rem;
  padding-left: calc(var(--space-sm) * 0.5);
  border-left: 3px solid var(--secondary-color);
  margin-left: ${(props) => (props.level > 1 ? `${props.level * 10}px` : "0")};
`

export default function TableOfContents({ toc }) {
  function TOC() {
    return (
      <ol className="table-of-contents">
        {toc.map(({ level, id, title, anchor }) => (
          <ToCListItem key={id} level={level}>
            <a href={anchor}>{title}</a>
          </ToCListItem>
        ))}
      </ol>
    )
  }

  return (
    <>
      <p>Table of contents</p>
      <divr>
        <TOC />
      </div>
    </>
  )
}

However, the anchor links do not work yet, since the corresponding section IDs still have to be added to the titles in Markdown content.

For rendering the actual post content I use react-markdown. With the help of custom renderers you can now edit all html elements in react-markdown. To add anchor links to the titles I use custom renderers for h1 to h6.

js
Copy code
const renderers = {
  h2: { children }) => {
    const anchor = `${children[0].replace(/ /g, "-").toLowerCase()}`
    return <h2 id={anchor}>{children}</h2>
  },
  h3: ({children }) => {.
    const anchor = `${children[0].replace(/ /g, "-").toLowerCase()}`
    return <h3 id={anchor}>{children}</h2>
  },
  h4: ({children }) => {.
    const anchor = `${children[0].replace(/ /g, "-").toLowerCase()}`
    return <h4 id={anchor}>{children}</h2>
  },
  h5: ({children }) => {.
    const anchor = `${children[0].replace(/ /g, "-").toLowerCase()}`
    return <h5 id={anchor}>{children}</h2>
  },
  h6: ({children }) => {.
    const anchor = `${children[0].replace(/ /g, "-").toLowerCase()}`
    return <h6 id={anchor}>{children}</h2>
  },

Lastly, I added a little scroll effect with the following css-property scroll-behavior: smooth;

Github Links

First published February 27, 2023

    0 Webmentions

    Have you published a response to this? Send me a webmention by letting me know the URL.

    Found no Webmentions yet. Be the first!

    Write a comment

    About The Author

    Max
    Max

    Geospatial Developer

    Hi, I'm Max (he/him). I am a geospatial developer, author and cyclist from Rosenheim, Germany. Support me

    0 Virtual Thanks Sent.

    Continue Reading

    1. How to create a custom cookie banner for your React application

      Recently I implemented a custom cookie banner solution on my Next.js site which you probably have seen a few seconds before. There are a lot of prebuilt cookie banners you can use for React or Next js sites but i wanted to create a custom cookie banner which also has some personal touch and keeps the design with the website in line.

      Continue reading...

    2. How to build a related posts component for your React blog

      Some blogs have these related articles or posts sections where visitors can have a preview at more content after they just read a post. That's what I wanted to create for my personal website which is built with React (Nextjs) and in this article I want to show you how you also can do it for any other react application.

      Continue reading...

    3. Using Google Adsense with GatsbyJS

      In general there are two possibilies to use Google Adsense on your GatsbyJS website Auto Ads and custom display blocks.Depending on whether you choose to include Adsense ads on certain spots or whether you will leave this job to the Google AI, you can choose one/and or the other.

      Continue reading...