Setting up MDX in Next.js with Code Highlighting
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 MDXrehype-autolink-headings
: Adds anchor links to headingsrehype-code-titles
: Enables code block titlesrehype-pretty-code
: Syntax highlighting powered by Shikirehype-slug
: Adds IDs to headingsremark-gfm
: GitHub Flavored Markdown supportremark-frontmatter
&remark-mdx-frontmatter
: YAML frontmatter supportshiki
: Syntax highlighter
2. Configuring Next.js
Create or update your next.config.ts
:
next.config.ts1import 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'910const prettyCodeOptions = {11 filterMetaString: (string: string) => string.replace(/\{.*\}/, ''),12 keepBackground: true,13}1415const withMDX = createMDX({16 options: {17 rehypePlugins: [18 rehypeSlug,19 rehypeAutolinkHeadings,20 [rehypePrettyCode, prettyCodeOptions],21 rehypeCodeTitles,22 ],23 remarkPlugins: [remarkGfm, remarkFrontmatter],24 },25})2627const nextConfig: NextConfig = {28 pageExtensions: ['ts', 'tsx', 'js', 'jsx', 'md', 'mdx'],29 // ... other config options30}3132export default withMDX(nextConfig)
3. Setting up MDX Components
Create an mdx-components.tsx
file to customize how MDX elements are rendered:
src/mdx-components.tsx1'use client'23import React from 'react'4import type { MDXComponents } from 'mdx/types'5import Link from 'next/link'6import { ExternalLink } from '@/components/ExternalLink'7import { CodeBlock } from '@/components/CodeBlock'89const 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(':')2324 // 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 || ''2829 return (30 <CodeBlock31 file={cleanFile}32 highlights={highlights}33 language={lang}34 mdxType="code"35 >36 {children}37 </CodeBlock>38 )39 },40}4142export function useMDXComponents(): MDXComponents {43 return components44}
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.tsx1const 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`1112export 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.tsx1'use client'23import styled from 'styled-components'45interface CodeBlockProps {6 children: React.ReactNode7 file?: string8 highlights?: string9 language?: string10 mdxType?: string11}1213const 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;2021 &:hover button {22 opacity: 1;23 }24`2526const 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`3435export function CodeBlock({36 children,37 file,38 highlights,39 language,40 mdxType,41}: CodeBlockProps) {42 if (mdxType !== 'code') return <>{children}</>4344 return (45 <div>46 {file && <CodeTitle>{file}</CodeTitle>}47 <Pre48 data-line-numbers49 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.tsx1'use client'23import { MDXProvider } from '@mdx-js/react'4import { useMDXComponents } from '../mdx-components'56export 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.tsx1import { MDXWrapper } from '@/components/MDXWrapper'2import { getMDXComponent } from 'next-contentlayer/mdx'34export default async function BlogPost({ params }: { params: { slug: string } }) {5 // Your logic to fetch MDX content6 const MDXContent = getMDXComponent(content)78 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:
- Basic code block:
1const hello = 'world'
- With filename:
example.ts1const hello = 'world'
- With line highlighting:
1const hello = 'world'2console.log(hello)3// This line is highlighted4// This line is highlighted5// 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!