# How to Set Up MDX with the Next.js App Router (Step-by-Step)

> A practical guide to configuring MDX in the Next.js App Router, covering file-based routing, remote MDX, custom components, and frontmatter parsing.

- **Published:** 2025-08-01
- **Author:** Clixo
- **Reading time:** 5 min read
- **Tags:** mdx, nextjs, app-router, content-systems
- **Canonical URL:** https://clixo.sh/blog/mdx-nextjs-app-router-setup-guide

You have a Next.js project on the App Router and you want to write content in MDX — mixing Markdown prose with custom React components. The official docs cover the basics, but the real setup questions come up fast: how do you handle frontmatter, remote MDX from a CMS, custom component overrides, and TypeScript types without the process blowing up at build time?

This guide walks through a production-ready MDX setup from scratch — no boilerplate skipped.

```mermaid
flowchart LR
  A["Install packages"] --> B["Configure next.config.mjs"]
  B --> C["Add mdx-components.tsx"]
  C --> D["Parse frontmatter"]
  D --> E["File-based routing"]
  E --> F["Custom components"]
  F --> G["Syntax highlighting"]
```

## Setting Up MDX in Next.js App Router

### Install the required packages

The App Router uses `@next/mdx` paired with `@mdx-js/react`. You will also want `next-mdx-remote` if you plan to fetch MDX content from a CMS or database at runtime rather than storing it as local files.

```bash
npm install @next/mdx @mdx-js/react @types/mdx
```

For remote MDX (content stored outside the repo):

```bash
npm install next-mdx-remote
```

### Configure next.config.mjs

Wrap your Next.js config with the MDX plugin and set the page extensions so `.mdx` files are treated as routes:

```js
import createMDX from '@next/mdx'

const withMDX = createMDX({
  options: {
    remarkPlugins: [],
    rehypePlugins: [],
  },
})

export default withMDX({
  pageExtensions: ['ts', 'tsx', 'md', 'mdx'],
})
```

### Add the MDX components provider

Create a file at `mdx-components.tsx` in the project root. This is required by the App Router — without it, `@next/mdx` will not apply custom component overrides.

```tsx
import type { MDXComponents } from 'mdx/types'

export function useMDXComponents(components: MDXComponents): MDXComponents {
  return {
    h2: ({ children }) => (
      <h2 className="text-2xl font-semibold mt-8 mb-4">{children}</h2>
    ),
    ...components,
  }
}
```

Any HTML element you map here will override the default rendering for every MDX file in the project.

## Handling Frontmatter

The App Router does not parse YAML frontmatter out of the box. Two reliable approaches:

**Option A — gray-matter at build time.** Read the `.mdx` file with `fs.readFileSync`, pass it through `gray-matter`, and render the body separately. This is the simplest path for local file-based blogs.

**Option B — remark-frontmatter + remark-mdx-frontmatter.** Add both as `remarkPlugins`. The frontmatter becomes an exported `metadata` object you can import directly from the MDX module. This works better when you want the frontmatter available inside the MDX itself.

```js
import remarkFrontmatter from 'remark-frontmatter'
import remarkMdxFrontmatter from 'remark-mdx-frontmatter'

// inside createMDX options:
remarkPlugins: [remarkFrontmatter, remarkMdxFrontmatter]
```

## File-Based Routing for Blog Posts

The simplest structure for a blog:

```
app/
  blog/
    [slug]/
      page.tsx
content/
  blog/
    my-first-post.mdx
```

In `page.tsx`, read the MDX file at request time:

```tsx
import { readFile } from 'fs/promises'
import path from 'path'
import matter from 'gray-matter'
import { MDXRemote } from 'next-mdx-remote/rsc'

export default async function BlogPost({ params }: { params: { slug: string } }) {
  const filePath = path.join(process.cwd(), 'content/blog', `${params.slug}.mdx`)
  const raw = await readFile(filePath, 'utf-8')
  const { content } = matter(raw)
  return <MDXRemote source={content} />
}
```

`next-mdx-remote/rsc` is the React Server Component version — no client bundle overhead.

## Custom Components Inside MDX

You can pass custom components as a prop to `MDXRemote`:

```tsx
const components = {
  Callout: ({ children }: { children: React.ReactNode }) => (
    <div className="border-l-4 border-blue-500 pl-4 my-4">{children}</div>
  ),
}

<MDXRemote source={content} components={components} />
```

Any component you register this way becomes available inside `.mdx` files without an import. Keep this list small — every component in the map is included in the render path.

## Generating Static Params

For static export or ISR, generate the list of slugs at build time:

```tsx
export async function generateStaticParams() {
  const files = await readdir(path.join(process.cwd(), 'content/blog'))
  return files
    .filter((f) => f.endsWith('.mdx'))
    .map((f) => ({ slug: f.replace(/\.mdx$/, '') }))
}
```

## Common Mistakes to Avoid

- **Forgetting `mdx-components.tsx` in the project root.** The file must be at root level, not inside `app/` or `src/`.
- **Using JSX syntax inside MDX without registering the component.** MDX will compile, but the component will be undefined at runtime.
- **Skipping `rehype-pretty-code` or `shiki`.** Raw code blocks in MDX have no syntax highlighting unless you wire up a rehype plugin.
- **Mixing `@next/mdx` file routing and `next-mdx-remote` on the same files.** Pick one approach and stay consistent.

## Syntax Highlighting

Install `rehype-pretty-code` and `shiki`:

```bash
npm install rehype-pretty-code shiki
```

Add it to your rehype plugins in `next.config.mjs`. Code blocks in your MDX will render with full token-level highlighting on the server, zero client JavaScript.

---

A well-structured MDX setup is the foundation of any serious content system built on Next.js. If you are building a content-heavy product — documentation, a blog platform, or a marketing site that engineers maintain — the choices you make here compound over time.

If you want to build it right from the start, [talk to the Clixo team](https://clixo.sh/#contact) about content system architecture for your product.

---

Clixo · 1141 W Bryn Mawr Ave, Itasca, IL 60143, US · [hello@clixo.sh](mailto:hello@clixo.sh)
[Start a build](https://clixo.sh/#contact) · [All services](https://clixo.sh/services) · [Agent guide (llms.txt)](https://clixo.sh/llms.txt)
