Fix: MDX Not Working — Components Not Rendering, Imports Failing, or Frontmatter Not Parsed
Quick Answer
How to fix MDX issues — Next.js App Router setup with @next/mdx and next-mdx-remote, custom component mapping, frontmatter parsing with gray-matter, remark and rehype plugins, and TypeScript configuration.
The Problem
A React component inside an MDX file doesn’t render:
import { Button } from '../components/Button';
# My Post
<Button>Click me</Button>Error: Cannot use import statement in a moduleOr frontmatter values are undefined even though they’re defined in the file:
---
title: My Article
date: 2024-03-15
---
# {frontmatter.title}// In the page component
const { title } = frontmatter; // undefinedOr custom components passed via components prop render as plain HTML instead:
<MDXContent components={{ h1: CustomHeading }} />
// <h1> tags in MDX still render as default HTMLOr the MDX file fails to parse with a remark/rehype plugin error:
Error: Cannot find module 'remark-gfm'Why This Happens
MDX is a compilation step, not a runtime feature. MDX files are transformed to JavaScript during the build or at request time. The exact behavior depends heavily on which MDX integration you’re using:
@next/mdxcompiles at build time — MDX files are treated as pages or components. Imports work natively. But frontmatter is not automatically extracted — you handle it separately.next-mdx-remoteand@mdx-js/mdxcompile at runtime — useful for MDX stored in a database or CMS. The compilation happens server-side and the result is serialized to the client. This is what makes dynamic frontmatter extraction possible.- Component substitution requires the
componentsprop — Recharts passes down to every MDX-rendered element. Missing acomponentsprop means MDX falls back to default HTML elements and your custom components are ignored. - Remark/rehype plugins must be ESM-compatible —
remark-gfmv3+,rehype-highlight, and most modern plugins are pure ESM. Usingrequire()or misconfigurednext.config.js(CommonJS) causes import failures.
Fix 1: Set Up MDX with Next.js App Router
Option A — @next/mdx (build-time, file-based):
npm install @next/mdx @mdx-js/loader @mdx-js/react
npm install --save-dev @types/mdx// next.config.mjs
import createMDX from '@next/mdx';
import remarkGfm from 'remark-gfm';
import rehypeHighlight from 'rehype-highlight';
const withMDX = createMDX({
options: {
remarkPlugins: [remarkGfm],
rehypePlugins: [rehypeHighlight],
},
});
export default withMDX({
pageExtensions: ['js', 'jsx', 'ts', 'tsx', 'md', 'mdx'],
});// app/mdx-components.tsx — required for App Router
// This file maps HTML elements to custom React components
import type { MDXComponents } from 'mdx/types';
import Link from 'next/link';
export function useMDXComponents(components: MDXComponents): MDXComponents {
return {
h1: ({ children }) => (
<h1 className="text-4xl font-bold mt-8 mb-4">{children}</h1>
),
h2: ({ children }) => (
<h2 className="text-3xl font-semibold mt-6 mb-3">{children}</h2>
),
a: ({ href, children }) => (
<Link href={href ?? '#'} className="text-blue-600 underline">
{children}
</Link>
),
code: ({ children }) => (
<code className="bg-gray-100 rounded px-1 py-0.5 font-mono text-sm">
{children}
</code>
),
// Merge with any components passed at the call site
...components,
};
}// app/blog/my-post/page.mdx — MDX page in App Router
import { Button } from '@/components/Button';
export const metadata = {
title: 'My Post',
description: 'A great post',
};
# Hello World
This is an MDX page with a React component:
<Button variant="primary">Click me</Button>Option B — next-mdx-remote (runtime, CMS/database-driven):
npm install next-mdx-remote gray-matter// lib/mdx.ts — read and compile MDX from the filesystem or database
import fs from 'fs';
import path from 'path';
import matter from 'gray-matter';
import { compileMDX } from 'next-mdx-remote/rsc';
import remarkGfm from 'remark-gfm';
import { Button } from '@/components/Button';
import { Callout } from '@/components/Callout';
// Components available in MDX files
const components = { Button, Callout };
export async function getMDXContent(slug: string) {
const filePath = path.join(process.cwd(), 'content', `${slug}.mdx`);
const source = fs.readFileSync(filePath, 'utf-8');
// compileMDX handles both content and frontmatter
const { content, frontmatter } = await compileMDX<{
title: string;
date: string;
tags: string[];
}>({
source,
options: {
parseFrontmatter: true,
mdxOptions: {
remarkPlugins: [remarkGfm],
},
},
components,
});
return { content, frontmatter };
}// app/blog/[slug]/page.tsx — App Router page
import { getMDXContent } from '@/lib/mdx';
export default async function BlogPost({ params }: { params: { slug: string } }) {
const { content, frontmatter } = await getMDXContent(params.slug);
return (
<article>
<h1>{frontmatter.title}</h1>
<time>{frontmatter.date}</time>
<div className="prose">{content}</div>
</article>
);
}Fix 2: Parse Frontmatter Correctly
Frontmatter access patterns vary by integration:
// With gray-matter (any integration)
import matter from 'gray-matter';
import fs from 'fs';
const source = fs.readFileSync('content/post.mdx', 'utf-8');
const { data: frontmatter, content } = matter(source);
// frontmatter = { title: 'My Post', date: '2024-03-15', tags: ['js'] }
// content = MDX content without the frontmatter block
// With next-mdx-remote compileMDX
const { frontmatter } = await compileMDX({
source,
options: { parseFrontmatter: true },
});
// With @next/mdx — frontmatter is NOT automatically extracted
// Export metadata instead:// content/post.mdx with @next/mdx
export const metadata = {
title: 'My Post',
date: '2024-03-15',
};
# {metadata.title}
Post content here.// Read metadata from @next/mdx files
// The metadata export is available as a module export
const { metadata } = await import(`../content/${slug}.mdx`);Generate static params from MDX files:
// app/blog/[slug]/page.tsx
import fs from 'fs';
import path from 'path';
import matter from 'gray-matter';
const CONTENT_DIR = path.join(process.cwd(), 'content');
export async function generateStaticParams() {
const files = fs.readdirSync(CONTENT_DIR);
return files
.filter(f => f.endsWith('.mdx'))
.map(f => ({ slug: f.replace('.mdx', '') }));
}
export async function generateMetadata({ params }: { params: { slug: string } }) {
const source = fs.readFileSync(
path.join(CONTENT_DIR, `${params.slug}.mdx`), 'utf-8'
);
const { data } = matter(source);
return { title: data.title, description: data.description };
}Fix 3: Custom Component Mapping
Map HTML elements and custom components inside MDX:
// components/MDXComponents.tsx
import Image from 'next/image';
import Link from 'next/link';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { oneDark } from 'react-syntax-highlighter/dist/esm/styles/prism';
// Code block with syntax highlighting
function CodeBlock({ children, className }: {
children: string;
className?: string;
}) {
const language = className?.replace('language-', '') ?? 'text';
return (
<SyntaxHighlighter language={language} style={oneDark} showLineNumbers>
{children}
</SyntaxHighlighter>
);
}
// Callout component (usable directly in MDX)
function Callout({ type = 'info', children }: {
type: 'info' | 'warning' | 'danger';
children: React.ReactNode;
}) {
const styles = {
info: 'bg-blue-50 border-blue-400 text-blue-800',
warning: 'bg-yellow-50 border-yellow-400 text-yellow-800',
danger: 'bg-red-50 border-red-400 text-red-800',
};
return (
<div className={`border-l-4 p-4 rounded my-4 ${styles[type]}`}>
{children}
</div>
);
}
export const mdxComponents = {
// HTML element overrides
h1: ({ children }: any) => <h1 className="text-4xl font-bold">{children}</h1>,
h2: ({ children }: any) => <h2 className="text-3xl font-semibold">{children}</h2>,
a: ({ href, children }: any) => <Link href={href}>{children}</Link>,
img: ({ src, alt }: any) => (
<Image src={src} alt={alt ?? ''} width={800} height={400} className="rounded" />
),
pre: ({ children }: any) => <>{children}</>,
code: CodeBlock,
// Custom components (available without importing in MDX files)
Callout,
};// Use in next-mdx-remote
const { content } = await compileMDX({
source,
components: mdxComponents,
options: { parseFrontmatter: true },
});
// Use in app/mdx-components.tsx for @next/mdx
import { mdxComponents } from '@/components/MDXComponents';
export function useMDXComponents(components: MDXComponents): MDXComponents {
return { ...mdxComponents, ...components };
}// In MDX files — custom components work without importing
# My Post
<Callout type="warning">
This feature is experimental.
</Callout>Fix 4: Configure Remark and Rehype Plugins
Plugins extend MDX parsing and output:
npm install remark-gfm remark-toc rehype-slug rehype-autolink-headings rehype-pretty-code// next.config.mjs — ESM required for most modern plugins
import createMDX from '@next/mdx';
import remarkGfm from 'remark-gfm';
import remarkToc from 'remark-toc';
import rehypeSlug from 'rehype-slug';
import rehypeAutolinkHeadings from 'rehype-autolink-headings';
import rehypePrettyCode from 'rehype-pretty-code';
const withMDX = createMDX({
options: {
remarkPlugins: [
remarkGfm, // GitHub Flavored Markdown (tables, strikethrough, etc.)
[remarkToc, { heading: 'Contents', maxDepth: 3 }],
],
rehypePlugins: [
rehypeSlug, // Add `id` to headings
[rehypeAutolinkHeadings, { behavior: 'wrap' }],
[rehypePrettyCode, {
theme: 'github-dark',
keepBackground: false,
}],
],
},
});
export default withMDX({ pageExtensions: ['ts', 'tsx', 'md', 'mdx'] });Plugin compatibility notes:
remark-gfmv3+ is ESM-only — useimport, notrequirerehype-pretty-codereplacesrehype-highlightfor Shiki-based highlightingrehype-prism-plusis an alternative that works with Prism themes
Fix 5: TypeScript Configuration
MDX TypeScript support requires type declarations:
// types/mdx.d.ts
declare module '*.mdx' {
import type { MDXProps } from 'mdx/types';
import type { ReactElement } from 'react';
export default function MDXContent(props: MDXProps): ReactElement;
export const metadata: Record<string, unknown>;
}// tsconfig.json — ensure MDX files are included
{
"compilerOptions": {
"jsx": "preserve",
"moduleResolution": "bundler",
"allowJs": true,
"strict": true
},
"include": ["**/*.ts", "**/*.tsx", "**/*.mdx"]
}Type-safe frontmatter with next-mdx-remote:
// Define frontmatter schema
interface PostFrontmatter {
title: string;
description: string;
date: string;
tags: string[];
draft?: boolean;
}
// Pass as type parameter to compileMDX
const { content, frontmatter } = await compileMDX<PostFrontmatter>({
source,
options: { parseFrontmatter: true },
components,
});
// frontmatter is now fully typed
frontmatter.title; // string
frontmatter.draft; // boolean | undefinedFix 6: MDX with Astro and Other Frameworks
// Astro — MDX is built-in via @astrojs/mdx
// astro.config.mjs
import { defineConfig } from 'astro/config';
import mdx from '@astrojs/mdx';
import remarkGfm from 'remark-gfm';
export default defineConfig({
integrations: [
mdx({
remarkPlugins: [remarkGfm],
extendMarkdownConfig: true, // Inherit Markdown config
}),
],
});// src/pages/blog/[slug].astro
---
import { getCollection } from 'astro:content';
export async function getStaticPaths() {
const posts = await getCollection('blog');
return posts.map(post => ({
params: { slug: post.slug },
props: { post },
}));
}
const { post } = Astro.props;
const { Content, headings } = await post.render();
---
<article>
<h1>{post.data.title}</h1>
<Content />
</article>// Remix — use @mdx-js/rollup
// vite.config.ts
import mdx from '@mdx-js/rollup';
import remarkGfm from 'remark-gfm';
export default defineConfig({
plugins: [
mdx({ remarkPlugins: [remarkGfm] }),
remix(),
],
});Still Not Working?
Cannot use import statement in a module in Next.js — your next.config.js is using CommonJS (module.exports) but remark/rehype plugins are ESM. Rename next.config.js to next.config.mjs and use import/export syntax throughout. If you can’t switch to ESM, use dynamic import() in an async config function.
MDX component renders as [object Object] or throws — the components object was passed incorrectly. When using compileMDX, components go in the third argument object: { components }. When using MDXRemote (client-side), the components prop is on the component itself: <MDXRemote source={source} components={components} />.
Custom h1, h2 components work but code doesn’t — code blocks in MDX render as <pre><code>. If you only map code, inline code works but fenced code blocks may not (they come wrapped in pre). You need to map both pre and code, or use a plugin like rehype-pretty-code that transforms the structure before your component mapping runs.
Frontmatter dates become strings instead of Date objects — gray-matter parses YAML, which converts date strings to JavaScript Date objects. But JSON serialization (when passing data between server and client) converts Date back to strings. Serialize dates explicitly: date: frontmatter.date.toISOString() or store them as strings in your frontmatter (date: '2024-03-15').
For related content issues, see Fix: Next.js App Router Not Working and Fix: Astro Not Working.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: i18next Not Working — Translations Missing, Language Not Switching, or Namespace Errors
How to fix i18next issues — react-i18next setup, translation file loading, namespace configuration, language detection, interpolation, pluralization, and Next.js integration.
Fix: Mapbox GL JS Not Working — Map Not Rendering, Markers Missing, or Access Token Invalid
How to fix Mapbox GL JS issues — access token setup, React integration with react-map-gl, markers and popups, custom layers, geocoding, directions, and Next.js configuration.
Fix: React PDF Not Working — PDF Not Rendering, Worker Error, or Pages Blank
How to fix react-pdf and @react-pdf/renderer issues — PDF viewer setup, worker configuration, page rendering, text selection, annotations, and generating PDFs in React.
Fix: Million.js Not Working — Compiler Errors, Components Not Optimized, or React Compatibility Issues
How to fix Million.js issues — compiler setup with Vite and Next.js, block() optimization rules, component compatibility constraints, automatic mode, and debugging performance gains.