Web Development
How Next.js Processes MDX
By O. Wolfson
MDX (Markdown + JSX) is a powerful format for writing content with React components. Next.js provides seamless support for MDX through the @next/mdx
package, allowing developers to write and render MDX content in various ways.
In this article, we will explore how MDX is processed and rendered in Next.js using two examples:
Setting Up Next.js for MDX
To enable MDX in a Next.js app, install the required dependencies:
bashnpm install @next/mdx @mdx-js/loader
Update your next.config.js
to handle .mdx
files:
javascriptconst withMDX = require("@next/mdx")({
extension: /\.mdx?$/,
});
module.exports = withMDX({
pageExtensions: ["js", "jsx", "ts", "tsx", "md", "mdx"],
});
This configuration ensures that Next.js recognizes .mdx
files as valid page or component files.
Example 1: Simple Local MDX Import
This example demonstrates how to render a local .mdx
file as part of a React component:
File Structure
/content/test/example.mdx
(MDX content file)/examples/simple-mdx
(Page rendering the MDX content)
MDX File (example.mdx
):
mdx# Hello MDX This is a simple MDX example.
Page Component (/examples/simple-mdx
):
javascriptimport Content from "@/content/test/example.mdx";
export default function Page() {
return (
<div className="flex flex-col justify-center items-center h-96 ">
<Content />
</div>
);
}
How It Works
- The MDX file (
example.mdx
) is imported as a React component using the@next/mdx
loader. - During the build process, the MDX content is compiled into a React component.
- The
Content
component renders the MDX content as part of the React tree. - The custom styles or components defined in your MDX configuration (e.g.,
useMDXComponents
) are automatically applied when rendering local MDX.
Example 2: Remote MDX Content
This example demonstrates how to fetch MDX content from a remote source (e.g., a database or API) and dynamically render it.
File Structure
/utils/supabase/client.js
(Supabase client configuration)/examples/remote-mdx
(Page rendering the remote MDX content)
Supabase Table (mdx_content
):
json[
{
"id": 1,
"created_at": "2025-01-08T21:50:22.284318+00:00",
"title": "Test Content",
"content": "# Hello World\n\nThis is some **MDX content** from a remote source, Supabase."
}
]
Page Component (/examples/remote-mdx
):
javascript"use client";
import { createClient } from "@/utils/supabase/client";
import { evaluate } from "@mdx-js/mdx";
import { useEffect, useState } from "react";
import { Fragment } from "react";
import * as runtime from "react/jsx-runtime";
import { MDXProvider } from "@mdx-js/react";
import { useMDXComponents } from "@/mdx-components"; // Adjust path as needed
export default function RemoteMDXPage() {
const [Component, setComponent] = useState<React.ComponentType | null>(null);
const [error, setError] = useState<string | null>(null);
const [data, setData] = useState<{
id: number;
title: string;
content: string;
} | null>(null);
useEffect(() => {
async function fetchMDXFromSupabase() {
try {
const supabase = createClient();
// Fetch the MDX content column from the `mdx_content` table
const { data, error } = await supabase
.from("mdx_content")
.select("*")
.eq("id", 1)
.single();
setData(data);
if (error) {
throw new Error(`Supabase error: ${error.message}`);
}
const rawMDX = data?.content;
if (!rawMDX) {
throw new Error("MDX content not found");
}
// Compile and evaluate the MDX content into a React component
const { default: MDXComponent } = await evaluate(rawMDX, {
...runtime,
Fragment,
useMDXComponents: () => useMDXComponents({}), // Attach custom MDX components
});
setComponent(() => MDXComponent);
} catch (err) {
console.error("Error loading MDX:", err);
setError(err instanceof Error ? err.message : "Unknown error");
}
}
fetchMDXFromSupabase();
}, []);
if (error) {
return <div>Error: {error}</div>;
}
if (!data) {
return <div>Loading...</div>;
}
return (
<div className="flex flex-col justify-center items-center h-96">
<div className="flex flex-col gap-4">
<h1 className="text-5xl font-black">{data?.title}</h1>
{Component ? (
<MDXProvider components={useMDXComponents({})}>
<Component />
</MDXProvider>
) : (
<p>Loading...</p>
)}
</div>
</div>
);
}
How It Works
- The MDX content is stored in a Supabase table (
mdx_content
) with acontent
column containing MDX. - The content is fetched via the Supabase client using a query.
- The raw MDX string is compiled into a React component at runtime using the
@mdx-js/mdx
evaluate
function. - The
MDXProvider
wraps the rendered content, ensuring custom MDX components (like styledh1
,p
,code
, etc.) are applied. - The dynamically created React component is rendered once ready, styled consistently with locally imported MDX.
Comparison of the Two Approaches
Feature | Local MDX Import | Remote MDX Fetch |
---|---|---|
Data Source | Local .mdx file | Remote database or API |
Processing Time | Compile during build time | Compile dynamically at runtime |
Use Case | Static content, frequent reuse | Dynamic or user-specific content |
Performance | Faster (pre-compiled at build time) | Slower (requires fetching and compiling) |
Styling | Automatically applies custom styles | Requires MDXProvider for custom styles |
Flexibility | Limited to local files | Content can come from any remote source |
Conclusion
Next.js 13+ (currently 15) with @next/mdx
offers robust MDX support for both local and dynamic content. For static pages, local MDX imports are straightforward and highly performant. For dynamic content, fetching and rendering MDX at runtime provides incredible flexibility, enabling dynamic and user-driven experiences.
Choose the approach that best fits your application’s needs!