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.
jsCopy codeconst 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.
jsCopy codeconst 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.
jsCopy codelet 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.
jsCopy codeexport 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.
jsCopy codeimport 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
.
jsCopy codeconst 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
Have you published a response to this? Send me a webmention by letting me know the URL.
Found no Webmentions yet. Be the first!
About The Author
Geospatial Developer
Hi, I'm Max (he/him). I am a geospatial developer, author and cyclist from Rosenheim, Germany. Support me
Continue Reading
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.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.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.