📝 How to Use MDX Locally in Next.js (App Router) – Offline Integration Guide

In this post, I’ll guide you step by step on how to integrate MDX in your Next.js project (App Router) to load .mdx files from your local source code — without needing a CMS or external content service.

This setup is ideal for offline blogs, documentation pages, or any markdown-based content that needs to work with React components.

✅ Tech Stack Used:

  • @next/mdx
  • @mdx-js/loader
  • @mdx-js/react
  • @types/mdx

🔧 1. Install Required Packages

To get started, install the necessary MDX dependencies:

bash
1npm install @next/mdx @mdx-js/loader @mdx-js/react @types/mdx

⚙️ 2. Configure MDX in next.config.mjs

Next, configure MDX in your Next.js config file.

💡 If you’re not using next-intl for localization, you can skip the withNextIntl wrapper.

js
1import type { NextConfig } from "next";
2import createMDX from "@next/mdx";
3import createNextIntlPlugin from "next-intl/plugin";
4
5const nextConfig: NextConfig = {
6 // Enable MDX and Markdown support in page extensions
7 pageExtensions: ["js", "jsx", "md", "mdx", "ts", "tsx"],
8};
9
10const withMDX = createMDX({});
11const withNextIntl = createNextIntlPlugin();
12
13export default withNextIntl(withMDX(nextConfig));

🧩 3. Create MDX Components

You can customize how MDX content renders by overriding components. For example, let’s replace all <img> tags with Next.js <Image>:

tsx
1// mdx-components.tsx
2import type { MDXComponents } from "mdx/types";
3import Image, { ImageProps } from "next/image";
4import Code from "./components/code/Code"; // Custom React Component
5import InlineCode from "./components/code/InlineCode"; // Custom React Component
6import PostLink from "./components/link/PostLink"; // Custom React Component
7import { Button } from "./components/ui/button"; // Custom React Component
8
9export function useMDXComponents(components: MDXComponents): MDXComponents {
10 return {
11 img: (props) => (
12 <Image
13 style={{ width: "100%", height: "auto" }}
14 {...(props as ImageProps)}
15 alt={props.alt}
16 />
17 ),
18 h1: ({ children }) => (
19 <h1 className="text-4xl text-accent font-bold mb-3">{children}</h1>
20 ),
21 h2: ({ children }) => (
22 <h2 className="text-lg font-bold mb-3">{children}</h2>
23 ),
24 h3: ({ children }) => (
25 <h3 className="text-base font-bold mb-3">{children}</h3>
26 ),
27 ul: (props) => (
28 <ul className="list-disc pl-6 pb-4 w-full mb-0" {...props} />
29 ),
30 ol: (props) => <ol className="list-decimal pl-6 pb-4 w-full" {...props} />,
31 p: ({ children }) => <h1 className="mb-3 text-justify">{children}</h1>,
32 blockquote: (props) => (
33 <blockquote className="border-l-3 pl-3 my-3" {...props} />
34 ),
35 a: PostLink,
36 pre: ({ children }) => <pre className="mb-3">{children}</pre>,
37 hr: () => <hr className="mb-4" />,
38 code: (props) => {
39 const { className, children } = props;
40 if (className) {
41 return <Code {...props} />;
42 }
43 return <InlineCode>{children}</InlineCode>;
44 },
45 button: ({ children }) => <Button className="mb-3">{children}</Button>,
46 ...components,
47 };
48}

🛠️ Place this file in your project root or inside src/.


📁 4. Recommended Folder Structure

Organize your project folders for better scalability:

folder-structure
1app/
2 blog/
3 [slug]/
4 page.tsx # Renders the MDX post
5content/
6 welcome.mdx # MDX content
7 about.mdx
8mdx-components.ts # Custom MDX component rendering
9package.json

📄 5. Load MDX Posts Dynamically

Create the dynamic route [slug] to display your offline MDX posts:

tsx
1// app/blog/[slug]/page.tsx
2
3export default async function Page({
4 params,
5}: {
6 params: Promise<{ slug: string }>;
7}) {
8 const { slug } = await params;
9 const { default: Post } = await import(`@/content/${slug}.mdx`);
10
11 return <Post />;
12}
13
14// Static generation
15export function generateStaticParams() {
16 return [{ slug: "welcome" }, { slug: "about" }];
17}
18
19export const dynamicParams = false;

🚀 Benefits of This Setup


📚 References


💡 Final Thoughts

This setup allows you to build a full-featured blog or documentation site using .mdx files in Next.js App Router — 100% offline. It’s a perfect balance of markdown simplicity and React component power.