Setting up MDX in Next.js with Code Highlighting

Setting up MDX in Next.js with Code Highlighting

August 05, 20245 min read

Setting up MDX in Next.js with Code Highlighting

MDX is a powerful format that lets you write JSX in your markdown content. When combined with Next.js, it creates a fantastic blogging platform. In this post, I'll show you how to set up MDX in Next.js with syntax highlighting, line numbers, and code block titles.

Prerequisites

You'll need a Next.js project with TypeScript. If you don't have one, create it:

npx create-next-app@latest my-blog --typescript

1. Installing Dependencies

First, install the necessary packages:

yarn add @mdx-js/loader @mdx-js/react @next/mdx rehype-autolink-headings rehype-code-titles rehype-pretty-code rehype-slug remark-gfm remark-frontmatter remark-mdx-frontmatter shiki

Let's break down what each package does:

  • @mdx-js/loader & @mdx-js/react: Core MDX functionality
  • @next/mdx: Next.js integration for MDX
  • rehype-autolink-headings: Adds anchor links to headings
  • rehype-code-titles: Enables code block titles
  • rehype-pretty-code: Syntax highlighting powered by Shiki
  • rehype-slug: Adds IDs to headings
  • remark-gfm: GitHub Flavored Markdown support
  • remark-frontmatter & remark-mdx-frontmatter: YAML frontmatter support
  • shiki: Syntax highlighter

2. Configuring Next.js

Create or update your next.config.ts:

next.config.ts
import type { NextConfig } from 'next'
import createMDX from '@next/mdx'
import remarkGfm from 'remark-gfm'
import remarkFrontmatter from 'remark-frontmatter'
import rehypeSlug from 'rehype-slug'
import rehypeAutolinkHeadings from 'rehype-autolink-headings'
import rehypeCodeTitles from 'rehype-code-titles'
import rehypePrettyCode from 'rehype-pretty-code'

const prettyCodeOptions = {
  filterMetaString: (string: string) => string.replace(/\{.*\}/, ''),
  keepBackground: true,
}

const withMDX = createMDX({
  options: {
    rehypePlugins: [
      rehypeSlug,
      rehypeAutolinkHeadings,
      [rehypePrettyCode, prettyCodeOptions],
      rehypeCodeTitles,
    ],
    remarkPlugins: [remarkGfm, remarkFrontmatter],
  },
})

const nextConfig: NextConfig = {
  pageExtensions: ['ts', 'tsx', 'js', 'jsx', 'md', 'mdx'],
  // ... other config options
}

export default withMDX(nextConfig)

3. Setting up MDX Components

Create an mdx-components.tsx file to customize how MDX elements are rendered:

src/mdx-components.tsx
'use client'

import React from 'react'
import type { MDXComponents } from 'mdx/types'
import Link from 'next/link'
import { ExternalLink } from '@/components/ExternalLink'
import { CodeBlock } from '@/components/CodeBlock'

const components: MDXComponents = {
  a: ({ href, children }) => {
    if (!href) return <>{children}</>
    const isExternal = href.startsWith('http')
    if (isExternal) {
      return <ExternalLink href={href}>{children}</ExternalLink>
    }
    return <Link href={href}>{children}</Link>
  },
  code: ({ children, className }) => {
    // Extract language and filename from className (e.g., "language-typescript:file.ts")
    const [lang, fileWithHighlights] = (className || '')
      .replace('language-', '')
      .split(':')

    // Extract highlights from the trailing {1,2-3}
    const match = fileWithHighlights?.match(/(.*?)\s*{(.+)}$/)
    const highlights = match?.[2] || ''
    const cleanFile = match?.[1]?.trim() || fileWithHighlights || ''

    return (
      <CodeBlock
        file={cleanFile}
        highlights={highlights}
        language={lang}
        mdxType="code"
      >
        {children}
      </CodeBlock>
    )
  },
}

export function useMDXComponents(): MDXComponents {
  return components
}

4. Creating the CodeBlock Component

First, let's style inline code blocks. Add these styles to your MDX wrapper or global styles:

src/components/MDXWrapper.tsx
const MDXStyles = styled.div`
  code:not(pre code) {
    background: ${props => props.theme.colors.codeBackground || '#2d2d2d'};
    border-radius: 4px;
    color: ${props => props.theme.colors.code || '#e6e6e6'};
    font-family: monospace;
    font-size: 0.9em;
    padding: 0.2em 0.4em;
  }
`

export function MDXWrapper({ children }: { children: React.ReactNode }) {
  const components = useMDXComponents()
  return (
    <MDXProvider components={components}>
      <MDXStyles>{children}</MDXStyles>
    </MDXProvider>
  )
}

This will style inline-code elements with:

  • A darker background
  • Rounded corners
  • Monospace font
  • Slight padding
  • Proper color contrast

Now create a CodeBlock component to handle syntax highlighting:

src/components/CodeBlock.tsx
'use client'

import styled from 'styled-components'

interface CodeBlockProps {
  children: React.ReactNode
  file?: string
  highlights?: string
  language?: string
  mdxType?: string
}

const Pre = styled.pre`
  border-radius: 8px;
  font-size: 14px;
  margin: 20px 0;
  overflow-x: auto;
  padding: 16px;
  position: relative;

  &:hover button {
    opacity: 1;
  }
`

const CodeTitle = styled.div`
  background: #1e1e1e;
  border-radius: 8px 8px 0 0;
  color: #fff;
  font-family: monospace;
  font-size: 14px;
  padding: 8px 16px;
`

export function CodeBlock({
  children,
  file,
  highlights,
  language,
  mdxType,
}: CodeBlockProps) {
  if (mdxType !== 'code') return <>{children}</>

  return (
    <div>
      {file && <CodeTitle>{file}</CodeTitle>}
      <Pre
        data-line-numbers
        data-highlights={highlights}
        data-language={language}
      >
        {children}
      </Pre>
    </div>
  )
}

5. Creating an MDX Wrapper

Create an MDX wrapper component to provide the MDX context:

src/components/MDXWrapper.tsx
'use client'

import { MDXProvider } from '@mdx-js/react'
import { useMDXComponents } from '../mdx-components'

export function MDXWrapper({ children }: { children: React.ReactNode }) {
  const components = useMDXComponents()
  return <MDXProvider components={components}>{children}</MDXProvider>
}

6. Using MDX in Your Pages

Now you can create MDX files in your pages or app directory. Here's an example of how to use it in the App Router:

src/app/blog/[slug]/page.tsx
import { MDXWrapper } from '@/components/MDXWrapper'
import { getMDXComponent } from 'next-contentlayer/mdx'

export default async function BlogPost({ params }: { params: { slug: string } }) {
  // Your logic to fetch MDX content
  const MDXContent = getMDXComponent(content)

  return (
    <MDXWrapper>
      <MDXContent />
    </MDXWrapper>
  )
}

Using Code Blocks

Now you can use code blocks in your MDX files with various features:

  1. Basic code block:
const hello = 'world'
  1. With filename:
example.ts
const hello = 'world'
  1. With line highlighting:
const hello = 'world'
console.log(hello)
// This line is highlighted
// This line is highlighted
// This line is highlighted

Conclusion

You now have a fully functional MDX setup in your Next.js project with:

  • Syntax highlighting
  • Line numbers
  • Code block titles
  • Line highlighting
  • GitHub-flavored markdown
  • Automatic heading links

This setup provides a great foundation for a technical blog or documentation site. The syntax highlighting is powered by Shiki, which provides VS Code-quality highlighting, and the MDX integration allows you to use React components directly in your markdown files.

Happy coding!