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.

  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.

  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.

let globalID = 0, 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

        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.

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

  return {
    props: {
      post: {

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.

import styled from "styled-components"

const ToCListItem =`
  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">
        {{ level, id, title, anchor }) => (
          <ToCListItem key={id} level={level}>
            <a href={anchor}>{title}</a>

  return (
      <p>Table of contents</p>
        <TOC />

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.

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

About The Author

Max Dietrich
Max Dietrich

Geospatial Developer

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

0 Virtual Thanks Sent.

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!


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. How to deploy your Gatsby site on your own server

    With Gatsby 4 bringing in Server-Side Rendering (SSR) and Deferred Static Generation (DSG) you need an alternative methode to just hosting static files. Each page using SSR or DSG will be rendererd after a user requests it so there has be a server in the background which will handle these requests and build the pages if needed.

    Continue reading...