MDX Configuration Guide

Required Dependencies

npm install @next/mdx @mdx-js/loader @mdx-js/react @types/mdx
npm install rehype-highlight rehype-slug remark-gfm

Next.js Configuration

Create/update next.config.mjs:

import createMDX from '@next/mdx'
import remarkGfm from 'remark-gfm'
import rehypeHighlight from 'rehype-highlight'
import rehypeSlug from 'rehype-slug'

const withMDX = createMDX({
  extension: /\.mdx?$/,
  options: {
    remarkPlugins: [remarkGfm],
    rehypePlugins: [rehypeHighlight, rehypeSlug],
  },
})

export default withMDX({
  pageExtensions: ['ts', 'tsx', 'js', 'jsx', 'md', 'mdx'],
  images: {
    domains: ['github.com', 'raw.githubusercontent.com'],
  },
})

MDX Components

Create src/components/mdx/index.tsx:

import { FC, PropsWithChildren } from 'react'
import Image from 'next/image'
import Link from 'next/link'

const components = {
  h1: ({ children }: PropsWithChildren) => (
    <h1 className="text-4xl font-bold mt-8 mb-4">{children}</h1>
  ),
  h2: ({ children }: PropsWithChildren) => (
    <h2 className="text-3xl font-bold mt-8 mb-4">{children}</h2>
  ),
  h3: ({ children }: PropsWithChildren) => (
    <h3 className="text-2xl font-bold mt-6 mb-3">{children}</h3>
  ),
  p: ({ children }: PropsWithChildren) => (
    <p className="my-4 leading-7">{children}</p>
  ),
  a: ({ href, children }: any) => (
    <Link 
      href={href} 
      className="text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-200"
    >
      {children}
    </Link>
  ),
  img: ({ src, alt }: any) => (
    <div className="my-8">
      <Image
        src={src}
        alt={alt}
        width={800}
        height={400}
        className="rounded-lg"
      />
    </div>
  ),
  code: ({ children, className }: any) => (
    <code className={`${className} rounded-md px-2 py-1 bg-gray-100 dark:bg-gray-800`}>
      {children}
    </code>
  ),
  pre: ({ children }: PropsWithChildren) => (
    <pre className="bg-gray-100 dark:bg-gray-800 p-4 rounded-lg my-4 overflow-x-auto">
      {children}
    </pre>
  ),
}

export default components

Usage in App

In src/app/layout.tsx:

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

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body>
        <MDXProvider components={components}>
          {children}
        </MDXProvider>
      </body>
    </html>
  )
}

Utilities for MDX Content

Create src/utils/mdx.ts:

import fs from 'fs'
import path from 'path'
import matter from 'gray-matter'

const postsDirectory = path.join(process.cwd(), 'src/content/posts')
const projectsDirectory = path.join(process.cwd(), 'src/content/projects')

export function getAllPostSlugs() {
  const fileNames = fs.readdirSync(postsDirectory)
  return fileNames.map(fileName => {
    return {
      params: {
        slug: fileName.replace(/\.mdx$/, '')
      }
    }
  })
}

export function getPostData(slug: string) {
  const fullPath = path.join(postsDirectory, `${slug}.mdx`)
  const fileContents = fs.readFileSync(fullPath, 'utf8')
  const { data, content } = matter(fileContents)
  
  return {
    slug,
    content,
    ...data
  }
}

export function getAllPosts() {
  const fileNames = fs.readdirSync(postsDirectory)
  const allPostsData = fileNames.map(fileName => {
    const slug = fileName.replace(/\.mdx$/, '')
    return getPostData(slug)
  })
  
  return allPostsData.sort((a, b) => {
    if (a.date < b.date) {
      return 1
    } else {
      return -1
    }
  })
}

// Similar functions for projects
export function getAllProjectSlugs() {
  const fileNames = fs.readdirSync(projectsDirectory)
  return fileNames.map(fileName => {
    return {
      params: {
        slug: fileName.replace(/\.mdx$/, '')
      }
    }
  })
}

export function getProjectData(slug: string) {
  const fullPath = path.join(projectsDirectory, `${slug}.mdx`)
  const fileContents = fs.readFileSync(fullPath, 'utf8')
  const { data, content } = matter(fileContents)
  
  return {
    slug,
    content,
    ...data
  }
}

export function getAllProjects() {
  const fileNames = fs.readdirSync(projectsDirectory)
  const allProjectsData = fileNames.map(fileName => {
    const slug = fileName.replace(/\.mdx$/, '')
    return getProjectData(slug)
  })
  
  return allProjectsData.sort((a, b) => {
    if (a.featured === b.featured) {
      return 0
    }
    return a.featured ? -1 : 1
  })
}