Support Syntax Highlighting and Heading Links with MDX & Rehype

8 min read

Our blog will be more beneficial for people if we can organize our posts with a hierarchy of semantic headings and provide links directly to those headings. For technical posts, we're also going to want to provide some nice syntax highlighting for code examples.

We can solve for both of these needs (and many more) using Rehype plugins with the MDXRemote component.

We'll tackle the heading links first.

Create Sample Headings

Let's start by updating a post to include a few levels of headings. I'll be updating the hello-world.md sample post to look like this.

---
title: 'Hello, World!'
date: '01/21/2022'
---

Maybe there's a **happy little bush** that lives right there. There's nothing wrong with having a tree as a friend. Mountains are so simple, they're hard.

<!-- end -->

## Section One

Trees get lonely too, so we'll give him a little friend. This is where you take out all your _hostilities and frustrations_. It's better than kicking the puppy dog around and all that so. Only think about one thing at a time. Don't get greedy. No pressure. Just relax and watch it happen.

### Sub Section 1.1

If we're going to have animals around we all have to be concerned about them and take care of them. A [little happy sunlight](https://unsplash.com/photos/PsLrgT55DeM) shining through there. Let your imagination be your guide. They say everything looks better with odd numbers of things. But sometimes I put even numbers—just to upset the critics.

<HotTip>
It takes dark in order to show light. Nothing wrong with washing your brush. You can do anything here - the only pre-requisite is that it makes you happy. You're meant to have fun in life.
</HotTip>

### Sub Section 1.2

You could sit here for weeks with your one hair brush trying to do that - or you could do it with one stroke with an almighty brush. It's a very cold picture, I may have to go get my coat. It’s about to freeze me to death. We wash our brush with odorless thinner. Maybe there's a little something happening right here. Don't be afraid to make these big decisions. Once you start, they sort of just make themselves. When you buy that first tube of paint it gives you an artist license.

## Section Two

We'll make some happy little bushes here. Now, we're going to fluff this cloud. This is probably the greatest thing that's ever happened in my life.

I'm gonna add just a tiny little amount of Prussian Blue. Let's put some happy trees and bushes back in here. Now let's put some happy little clouds in here. I guess I'm a little weird. I like to talk to trees and animals. That's okay though; I have more fun than most people. Don't fight it, use what happens. This is a happy place, little squirrels live here and play.

I've added 2 arbitrary h2s and given the first h2 a couple of child h3 elements.

Install the Rehype Plugins

We're going to install rehype-slug and rehype-autolink-headings as a dev dependencies.

npm install -D rehype-slug rehype-autolink-headings

The rehype-slug plugin will attach id attributes to our headings, and rehype-autolink-headings will be responsible for creating links based on those id values.

Configure the Rehype Plugins

Now that we have the rehype plugin installed, let's bring it into our utils.ts file and apply it to our serialize step.

import fs from 'fs'
import { join } from 'path'
import matter from 'gray-matter'
import { serialize } from 'next-mdx-remote/serialize'
import { MDXRemoteSerializeResult } from 'next-mdx-remote'
+import rehypeSlug from 'rehype-slug'
+import rehypeAutolinkHeadings from 'rehype-autolink-headings'

With those imported, we'll update the getPostBySlug function to use the plugins in the serialize step.

export async function getPostBySlug(slug: string) {
  const source = readFileBySlug(slug)
  const { content, data } = matter(source)
- const mdxSource = await serialize(content)
+ const mdxSource = await serialize(content, {
+   mdxOptions: {
+     rehypePlugins: [
+       [rehypeSlug as any],
+       [rehypeAutolinkHeadings as any, { behavior: 'wrap' }],
+     ],
+   },
+ })
  return { mdxSource, data }
}

Our call to serialize will now get the second, options argument and inside the options object, we populate the rehypePlugins array with our plugins. There are several behavior options for rehype-auto-link-headings, I'm choosing to go with the wrap option here. You can read more about the different options here.

Info
The use of as any here is needed to avoid TypeScript issues. The types for these aren't very straightforward and at the time I was preparing this code, there didn't seem to be a consensus on the right way to do this. It isn't ideal, but these work just fine.

With that configuration in place, when we run our app again and load up the /blog/hello-world route, we should now see the headings rendered as links. Clicking a heading should scroll the page to that part of the page and add the generated hash for the heading to the URL like http://localhost:3000/blog/hello-world#sub-section-12.

Adding Syntax Highlighting

Let's make use of another rehype plugin to get syntax highlighting for code examples.

Adding Sample Code

Choose one of the sample blog posts (I'll continue to use hello-world.md) and add a markdown code block like this one.

    ```js
    function sample(a, b) {
      const sum = a + b
      return sum
    }
    ```

On its own, this will render the following markup.

<pre><code class="language-js">function sample(a, b) {
  const sum = a + b
  return sum
}
</code></pre>

Thanks to the default styling we get from TailwindCSS it doesn't look bad. It's lacking the syntax coloring that folks have come to expect, so we can add additional markup with our plugin.

Install and Configure the Rehype Plugin

Let's add rehype-highlight as a dev dependency.

npm install -D rehype-highlight

Now we can update utils.ts. Start by adding the import.

import fs from 'fs'
import { join } from 'path'
import matter from 'gray-matter'
import { serialize } from 'next-mdx-remote/serialize'
import { MDXRemoteSerializeResult } from 'next-mdx-remote'
import rehypeSlug from 'rehype-slug'
import rehypeAutolinkHeadings from 'rehype-autolink-headings'
+import rehypeHighlight from 'rehype-highlight'

Then we can add it to our serialize step in the getPostBySlug function.

export async function getPostBySlug(slug: string) {
  const source = readFileBySlug(slug)
  const { content, data } = matter(source)
  const mdxSource = await serialize(content, {
    mdxOptions: {
      rehypePlugins: [
        [rehypeSlug as any],
        [rehypeAutolinkHeadings as any, { behavior: 'wrap' }],
+       [rehypeHighlight as any],
      ],
    },
  })
  return { mdxSource, data }
}

Once that configuration is in place, we can look at our page again. There won't be a visible difference in the code sample yet, but the markup will now look like this.

<pre><code class="hljs language-js"><span class="hljs-keyword">function</span> <span class="hljs-title hljs-function">sample</span>(<span class="hljs-params">a, b</span>) {
  <span class="hljs-keyword">const</span> sum = a + b
  <span class="hljs-keyword">return</span> sum
}
</code></pre>

We now get the highlightjs markup applied to our code block.

Add the HighlihtJS CSS

In order to style this, we'll grab a CSS file for the syntax theme we'd like to apply. I like to use the Night Owl theme by Sarah Drasner in VSCode, so I'd like to apply it here as well. We can load in the Night Owl theme for highlight.js from a cdn.

We'll make this change in pages/blog/[slug].tsx.

Let's start by importing the Head component provided by Next.

import type { GetStaticPaths, GetStaticProps } from 'next'
+import Head from 'next/head'
import { getAllPostSlugs, getPostBySlug } from '../../utils'
import { MDXRemoteSerializeResult } from 'next-mdx-remote/dist/types'
import { MDXRemote } from 'next-mdx-remote'
import { HotTip } from '../../components/HotTip'

Now we can update our Post component. We'll wrap the entire thing in a React Fragment, then add the Head component as a sibling to our wrapping div. Inside Head, we'll apply the cdn link for our CSS file.

export default function Post({ slug, mdxSource, data }: PostProps) {
  const displayDate = new Date(data?.date).toDateString()
  return (
+   <>
+     <Head>
+       <link
+         rel="stylesheet"
+   href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.1.2/styles/night-owl.min.css"
+        />
+     </Head>
      <div className="mt-4">
        <h1 className="text-5xl text-center font-black">{data?.title}</h1>
            <div className="mt-3 text-center">
              <time
                className="text-lg text-gray-800 font-semibold"
                dateTime={displayDate}
              >
                {displayDate}
              </time>
            </div>
        <article className="prose prose-xl prose-sky mx-auto mt-2 py-4">
          <MDXRemote components={components} {...mdxSource} />
        </article>
      </div>
+   </>
  )
}

Now if you reload that post again, you should see the code block with some nice syntax highlighting applied.

Wrap Up

We used a few rehype plugins here to elevate our blog posts with a minimal amount of code and no real change to the way we write our Markdown. Combining MDX with these plugins can be really powerful. I encourage you to explore the ecosystem of available plugins and see what other useful functionality you can apply to your blog.

Hot Tip
Check back next week for the next article in the series, Add a Skip Link Utility as a Tailwind Plugin!