TL;DR: MDX lets you write Markdown and drop in React components anywhere. In Next.js (App Router), the cleanest, edge‑friendly approach is build‑time compilation via
@next/mdx
with a small set of remark/rehype plugins. Render MDX as server components, wire up Shiki viarehype-pretty-code
for gorgeous code blocks, add slugged/autolinked headings, and keep image handling simple with a custom<Image/>
override.
Why MDX for a Next.js blog/docs site?
- Markdown + React: write content like docs, but sprinkle in live components (callouts, charts, interactive sandboxes).
- App Router friendly: MDX can be compiled at build‑time and rendered as React Server Components (RSC), perfect for Edge/Cloudflare deployments.
- Control: choose your own remark/rehype plugins; no CMS or heavy framework required.
The three common approaches (pick one)
-
Build‑time imports with
@next/mdx
(recommended)- MDX is compiled during the build → fast runtime, edge‑safe, zero serverless cold starts.
- Works great with static export or SSG pages.
-
next-mdx-remote
(RSC variant) +xdm
- Good when content comes from a DB or remote source at request time.
- Slightly more moving parts; less ideal on Edge unless you prebuild.
-
Contentlayer
- Batteries included content graph and types. Nice developer experience, but adds a service layer and can fall behind Next.js releases.
If you don’t need runtime compilation, go with (1).
Directory shape
app/
blog/
[slug]/page.tsx
mdx-components.tsx
content/
blog/
building-with-mdx/
index.mdx
assets/
diagram.png
lib/
posts.ts
next.config.mjs
Step 1 — Install the pieces
pnpm add @next/mdx @mdx-js/react remark-gfm rehype-slug rehype-autolink-headings rehype-pretty-code remark-frontmatter remark-mdx-frontmatter
We use
remark-mdx-frontmatter
so frontmatter becomes named exports you can read directly from the MDX module.
Step 2 — Wire up @next/mdx
in next.config.mjs
// next.config.mjs
import createMDX from "@next/mdx";
import remarkGfm from "remark-gfm";
import remarkFrontmatter from "remark-frontmatter";
import remarkMdxFrontmatter from "remark-mdx-frontmatter";
import rehypeSlug from "rehype-slug";
import rehypeAutolinkHeadings from "rehype-autolink-headings";
import rehypePrettyCode from "rehype-pretty-code";
const withMDX = createMDX({
options: {
remarkPlugins: [
remarkGfm,
[remarkFrontmatter, { type: "yaml", marker: "-" }],
remarkMdxFrontmatter,
],
rehypePlugins: [
rehypeSlug,
[rehypeAutolinkHeadings, { behavior: "wrap" }],
[
rehypePrettyCode,
{
theme: {
light: "github-light",
dark: "github-dark-dimmed",
},
keepBackground: false,
defaultLang: "text",
},
],
],
// Important for frontmatter export names in ESM
format: "mdx",
},
});
/** @type {import('next').NextConfig} */
const nextConfig = {
pageExtensions: ["ts", "tsx", "md", "mdx"],
experimental: {
mdxRs: true, // Next’s Rust-based MDX compiler. Disable if you hit a bundler edge-case.
},
};
export default withMDX(nextConfig);
Heads‑up: If you hit bundler issues (e.g., Turbopack + MDX plugins), toggle
experimental.mdxRs
or temporarily fall back to Webpack.
Step 3 — Author an MDX post with frontmatter
---
title: "Building with MDX"
description: "Mix Markdown with React components."
date: "2025-08-01"
tags: ["mdx", "nextjs"]
cover: "./assets/diagram.png"
readingTime: 7
---
# Building with MDX
Some intro text.
<MyCallout>MDX can render **real components** like this.</MyCallout>
Note the relative image path in
cover
and a shortreadingTime
—we’ll read these exports in our page.
Step 4 — Server‑only MDX components mapping
In the App Router, skip context providers; just pass a components
map to the compiled MDX module.
// app/mdx-components.tsx (RSC)
import Image from "next/image";
import type { MDXComponents } from "mdx/types";
export function mdxComponents(slug?: string): MDXComponents {
return {
h2: (props) => <h2 className="mt-10 scroll-m-20 text-2xl font-semibold" {...props} />,
h3: (props) => <h3 className="mt-8 scroll-m-20 text-xl font-semibold" {...props} />,
pre: (props) => (
<pre className="my-4 rounded-lg border p-4 overflow-x-auto" {...props} />
),
code: (props) => <code className="rounded px-1 py-0.5" {...props} />,
// Make relative images in MDX work nicely
img: ({ src = "", alt = "", ...rest }) => {
const s = String(src);
const resolved = s