#code

4 min read

views

Generate Blog Heading Anchors in React-Markdown

Amir Ardalan
#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:

// Markdown.tsx
import ReactMarkdown from 'react-markdown';

export default function Markdown({ markdown }) {
	
	const MarkdownComponents: object = {
		// Code will go here
	}
		
	return (
		<ReactMarkdown
		  components={MarkdownComponents}
		>
		  {markdown.content}
		</ReactMarkdown>
	)

};
tsx
#Create generateSlug.ts in utils Folder

We will use this to generate the anchor slug.

// generateSlug.ts

const generateSlug = (str: string) => {
  str = str?.replace(/^\s+|\s+$/g, '');
  str = str?.toLowerCase();
  const from = 'àáãäâèéëêìíïîòóöôùúüûñç·/_,:;';
  const to = 'aaaaaeeeeiiiioooouuuunc------';

  for (let i = 0, l = from.length; i < l; i++) {
    str = str.replace(new RegExp(from.charAt(i), 'g'), to.charAt(i));
  }

  str = str
    ?.replace(/[^a-z0-9 -]/g, '')
    .replace(/\s+/g, '-')
    .replace(/-+/g, '-');

  return str;
};

export default generateSlug;
typescript
#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:

// Markdown.tsx
import ReactMarkdown from 'react-markdown';
import generateSlug from '@/utils/generateSlug';
h3: (props: H3Props) => {
const children = Array.isArray(props.children)
? props.children
: [props.children];
const heading = children
.flatMap((element) =>
typeof element === 'string'
? element
: element?.type !== undefined &&
typeof element.props.children === 'string'
? element.props.children
: []
)
.join('');
const slug = generateSlug(heading);
return (
<h3 id={slug}>
<a href={`#${slug}`} {...props}></a>
</h3>
);
},
...
tsx

Here we are targeting the h3 headings in the markdown and iterating through the returned node arrays via a flatMap method. The flatMap 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.

#Improve UX with CSS

You may want to add a scrolling animation to your page when a user clicks an inbound anchor link or clicks on one of the headings. This helps orient the user to the action that has occured (the page is moving to bring an anchor into view). The following CSS should do the trick:

html {
	scroll-behavior: smooth;
}
scss
#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.

Enjoy this post? Like and share!