Setting up MDX in Next.js with Code Highlighting

Setting up MDX in Next.js with Code Highlighting

August 05, 20245 min read

title: 'Setting up MDX in Next.js with Code Highlighting' date: '2024-08-05' cover_image: 'https://res.cloudinary.com/crbaucom/image/upload/v1736453458/crbaucom-images/nextjs_mdx.png' description: 'A comprehensive guide to setting up MDX in Next.js with syntax highlighting, line numbers, and code block titles.' tags: ['nextjs', 'mdx', 'typescript', 'react'] section: 'blog'

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:

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

1. Installing Dependencies

First, install the necessary packages:

1yarn 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
1import type { NextConfig } from 'next'
2import createMDX from '@next/mdx'
3import remarkGfm from 'remark-gfm'
4import remarkFrontmatter from 'remark-frontmatter'
5import rehypeSlug from 'rehype-slug'
6import rehypeAutolinkHeadings from 'rehype-autolink-headings'
7import rehypeCodeTitles from 'rehype-code-titles'
8import rehypePrettyCode from 'rehype-pretty-code'
9
10const prettyCodeOptions = {
11 filterMetaString: (string: string) => string.replace(/\{.*\}/, ''),
12 keepBackground: true,
13}
14
15const withMDX = createMDX({
16 options: {
17 rehypePlugins: [
18 rehypeSlug,
19 rehypeAutolinkHeadings,
20 [rehypePrettyCode, prettyCodeOptions],
21 rehypeCodeTitles,
22 ],
23 remarkPlugins: [remarkGfm, remarkFrontmatter],
24 },
25})
26
27const nextConfig: NextConfig = {
28 pageExtensions: ['ts', 'tsx', 'js', 'jsx', 'md', 'mdx'],
29 // ... other config options
30}
31
32export 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
1'use client'
2
3import React from 'react'
4import type { MDXComponents } from 'mdx/types'
5import Link from 'next/link'
6import { ExternalLink } from '@/components/ExternalLink'
7import { CodeBlock } from '@/components/CodeBlock'
8
9const components: MDXComponents = {
10 a: ({ href, children }) => {
11 if (!href) return <>{children}</>
12 const isExternal = href.startsWith('http')
13 if (isExternal) {
14 return <ExternalLink href={href}>{children}</ExternalLink>
15 }
16 return <Link href={href}>{children}</Link>
17 },
18 code: ({ children, className }) => {
19 // Extract language and filename from className (e.g., "language-typescript:file.ts")
20 const [lang, fileWithHighlights] = (className || '')
21 .replace('language-', '')
22 .split(':')
23
24 // Extract highlights from the trailing {1,2-3}
25 const match = fileWithHighlights?.match(/(.*?)\s*{(.+)}$/)
26 const highlights = match?.[2] || ''
27 const cleanFile = match?.[1]?.trim() || fileWithHighlights || ''
28
29 return (
30 <CodeBlock
31 file={cleanFile}
32 highlights={highlights}
33 language={lang}
34 mdxType="code"
35 >
36 {children}
37 </CodeBlock>
38 )
39 },
40}
41
42export function useMDXComponents(): MDXComponents {
43 return components
44}

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
1const MDXStyles = styled.div`
2 code:not(pre code) {
3 background: ${props => props.theme.colors.codeBackground || '#2d2d2d'};
4 border-radius: 4px;
5 color: ${props => props.theme.colors.code || '#e6e6e6'};
6 font-family: monospace;
7 font-size: 0.9em;
8 padding: 0.2em 0.4em;
9 }
10`
11
12export function MDXWrapper({ children }: { children: React.ReactNode }) {
13 const components = useMDXComponents()
14 return (
15 <MDXProvider components={components}>
16 <MDXStyles>{children}</MDXStyles>
17 </MDXProvider>
18 )
19}

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
1'use client'
2
3import styled from 'styled-components'
4
5interface CodeBlockProps {
6 children: React.ReactNode
7 file?: string
8 highlights?: string
9 language?: string
10 mdxType?: string
11}
12
13const Pre = styled.pre`
14 border-radius: 8px;
15 font-size: 14px;
16 margin: 20px 0;
17 overflow-x: auto;
18 padding: 16px;
19 position: relative;
20
21 &:hover button {
22 opacity: 1;
23 }
24`
25
26const CodeTitle = styled.div`
27 background: #1e1e1e;
28 border-radius: 8px 8px 0 0;
29 color: #fff;
30 font-family: monospace;
31 font-size: 14px;
32 padding: 8px 16px;
33`
34
35export function CodeBlock({
36 children,
37 file,
38 highlights,
39 language,
40 mdxType,
41}: CodeBlockProps) {
42 if (mdxType !== 'code') return <>{children}</>
43
44 return (
45 <div>
46 {file && <CodeTitle>{file}</CodeTitle>}
47 <Pre
48 data-line-numbers
49 data-highlights={highlights}
50 data-language={language}
51 >
52 {children}
53 </Pre>
54 </div>
55 )
56}

5. Creating an MDX Wrapper

Create an MDX wrapper component to provide the MDX context:

src/components/MDXWrapper.tsx
1'use client'
2
3import { MDXProvider } from '@mdx-js/react'
4import { useMDXComponents } from '../mdx-components'
5
6export function MDXWrapper({ children }: { children: React.ReactNode }) {
7 const components = useMDXComponents()
8 return <MDXProvider components={components}>{children}</MDXProvider>
9}

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
1import { MDXWrapper } from '@/components/MDXWrapper'
2import { getMDXComponent } from 'next-contentlayer/mdx'
3
4export default async function BlogPost({ params }: { params: { slug: string } }) {
5 // Your logic to fetch MDX content
6 const MDXContent = getMDXComponent(content)
7
8 return (
9 <MDXWrapper>
10 <MDXContent />
11 </MDXWrapper>
12 )
13}

Using Code Blocks

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

  1. Basic code block:
1const hello = 'world'
  1. With filename:
example.ts
1const hello = 'world'
  1. With line highlighting:
1const hello = 'world'
2console.log(hello)
3// This line is highlighted
4// This line is highlighted
5// 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!