Generate Blog Heading Anchors in React-Markdown
What Are Heading Anchors?
If you are not already familiar with heading anchors, check out any README.md
on GitHub or hover over a heading within most blog posts and you will likely see a small link icon or hashtag to the left of the title, indicating that you can click to anchor to that specific heading.
If you'd like to link someone to a specific part of a blog post, you can simply click the title you want to send them to and the address bar is automatically populated with an anchor that will link directly to that section.
Why Not Download an Existing Package?
There are excellent rehype and remark plugins such as remark-autolink-headings and rehype-slug. If your goal is to get this feature working as quickly as possible, those are great options.
The issue arises when you do this for every little feature in your blog. If you are loading in 5-10+ plugins into react-markdown
you are going to be shipping enormous javascript bundles to the client.
Extending react-markdown
is super simple and your visitors will feel the difference in first contentful paint times.
Create Markdown.tsx
In this example, I am passing in the raw markdown to our component with the markdown
prop:
1// Markdown.tsx 2import ReactMarkdown from 'react-markdown' 3 4export default function Markdown({ markdown }) { 5 6 const MarkdownComponents: object = { 7 // Code will go here 8 } 9 10 return ( 11 <ReactMarkdown 12 components={MarkdownComponents} 13 > 14 {markdown.content} 15 </ReactMarkdown> 16 ) 17 18} 19 20
Create generateSlug.ts
in utils
Folder
We will use this to generate the anchor slug.
1// generateSlug.ts 2 3 4const generateSlug = (str: string) => { 5 6 str = str?.replace(/^\s+|\s+$/g, '') 7 str = str?.toLowerCase() 8 const from = 'àáãäâèéëêìíïîòóöôùúüûñç·/_,:;' 9 const to = 'aaaaaeeeeiiiioooouuuunc------' 10 11 for (let i = 0, l = from.length; i < l; i++) { 12 str = str.replace(new RegExp(from.charAt(i), 'g'), to.charAt(i)) 13 } 14 15 str = str?.replace(/[^a-z0-9 -]/g, '') 16 .replace(/\s+/g, '-') 17 .replace(/-+/g, '-') 18 19 return str 20} 21 22export default generateSlug 23
Import generateSlug.ts
and Customize the Heading Node
In my blog, I am using the H3
heading for blog post headings. Change this to whatever fits your needs:
1// Markdown.tsx 2import ReactMarkdown from 'react-markdown' 3import generateSlug from '@/utils/generateSlug' 4 5const MarkdownComponents: object = { 6 h3: (props: any) => { 7 const arr = props.children 8 let heading = '' 9 10 for (let i = 0; i < arr.length; i++) { 11 if (arr[i]?.type !== undefined) { 12 for (let j = 0; j < arr[i].props.children.length; j++) { 13 heading += arr[i]?.props?.children[0] 14 } 15 } else heading += arr[i] 16 } 17 18 const slug = generateSlug(heading) 19 return <h3 id={slug}><a href={`#${slug}`} {...props}></a></h3> 20 } 21} 22... 23
Here we are targeting the h3
headings in the markdown and looping through the returned node arrays. The for loop will look for anything inside the title that isn't a normal string, such as a code
object.
We initially create an empty string assigned to the variable heading
and concatenate any code objects with the strings. Once we have a heading string, we pass it to the generateSlug
function, and finally we tell the ReactMarkdown
component to output an h3 element with an anchor inside of it that contains our newly generated slug.
Final Thoughts
You can wrap the markdown component in a class to target with CSS or pass a css prop directly to the ReactMarkdown
component. From there it's up to you to style your link/hashtag icon on hover to indicate your titles are now anchors.