📝 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:
bash1npm 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.
js1import type { NextConfig } from "next";2import createMDX from "@next/mdx";3import createNextIntlPlugin from "next-intl/plugin";45const nextConfig: NextConfig = {6 // Enable MDX and Markdown support in page extensions7 pageExtensions: ["js", "jsx", "md", "mdx", "ts", "tsx"],8};910const withMDX = createMDX({});11const withNextIntl = createNextIntlPlugin();1213export 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>:
tsx1// mdx-components.tsx2import type { MDXComponents } from "mdx/types";3import Image, { ImageProps } from "next/image";4import Code from "./components/code/Code"; // Custom React Component5import InlineCode from "./components/code/InlineCode"; // Custom React Component6import PostLink from "./components/link/PostLink"; // Custom React Component7import { Button } from "./components/ui/button"; // Custom React Component89export function useMDXComponents(components: MDXComponents): MDXComponents {10 return {11 img: (props) => (12 <Image13 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-structure1app/2 blog/3 [slug]/4 page.tsx # Renders the MDX post5content/6 welcome.mdx # MDX content7 about.mdx8mdx-components.ts # Custom MDX component rendering9package.json
📄 5. Load MDX Posts Dynamically
Create the dynamic route [slug] to display your offline MDX posts:
tsx1// app/blog/[slug]/page.tsx23export 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`);1011 return <Post />;12}1314// Static generation15export function generateStaticParams() {16 return [{ slug: "welcome" }, { slug: "about" }];17}1819export const dynamicParams = false;
🚀 Benefits of This Setup
- ✅ Offline-ready: No external CMS or API
- ✅ Static generation: Fast and SEO-friendly
- ✅ Flexible customization with React components
- ✅ Great for technical blogs or documentation