Mdx
"MDX authoring with Next.js: Markdown + JSX, custom components, frontmatter extraction, @next/mdx, mdx-bundler, contentlayer integration, rehype/remark plugins, and syntax highlighting with Shiki or Prism."
MDX lets you embed React components directly inside Markdown, bridging the gap between content authoring and interactive UI. Content lives in `.mdx` files that look like Markdown but support JSX expressions, imports, and component composition. The compilation pipeline — remark (AST transforms on Markdown), rehype (AST transforms on HTML), and the MDX compiler — converts `.mdx` into React components at build time or runtime. This enables rich, interactive documentation and blog posts while keeping the authoring experience simple for writers who think in Markdown. ## Key Points - Define `mdx-components.tsx` at the project root for App Router — Next.js auto-discovers it and applies it to all MDX files in the app directory. - Use `rehype-pretty-code` with Shiki for build-time syntax highlighting instead of client-side Prism; it produces zero-JS highlighted code blocks. - Keep MDX files in a `content/` directory separate from `app/` to decouple content from routing logic. - Create a component map (`useMDXComponents`) that maps HTML elements to your design system — this gives consistent styling without requiring authors to use custom components. - Use `gray-matter` to parse frontmatter before passing to the MDX compiler so you can use frontmatter values in page metadata. - Add `remark-gfm` to support GitHub Flavored Markdown features: tables, strikethrough, task lists, and autolinks. - Validate frontmatter with Zod schemas to catch missing fields at build time. - Co-locate images with MDX files and use `next/image` via custom component mapping for automatic optimization. - **Using `dangerouslySetInnerHTML` for MDX output**: mdx-bundler and @next/mdx already produce React elements. Injecting raw HTML bypasses React's reconciliation and opens XSS vectors. - **Skipping `rehype-slug`**: Without it, headings have no `id` attributes, breaking anchor links and table-of-contents navigation. - **Embedding large data in MDX files**: MDX compiles to JavaScript. Large inline JSON bloats the bundle. Fetch data in the page component and pass it as props instead. - **Using `eval` or `new Function` to run MDX client-side**: Use `mdx-bundler/client` or `@mdx-js/react` which handle sandboxing. Rolling your own evaluation is fragile and insecure. ## Quick Example ```bash npm install @next/mdx @mdx-js/loader @mdx-js/react ``` ```bash npm install mdx-bundler esbuild ```
skilldb get seo-content-skills/MdxFull skill: 307 linesMDX — Markdown + JSX
Core Philosophy
MDX lets you embed React components directly inside Markdown, bridging the gap between content authoring and interactive UI. Content lives in .mdx files that look like Markdown but support JSX expressions, imports, and component composition. The compilation pipeline — remark (AST transforms on Markdown), rehype (AST transforms on HTML), and the MDX compiler — converts .mdx into React components at build time or runtime. This enables rich, interactive documentation and blog posts while keeping the authoring experience simple for writers who think in Markdown.
Setup
@next/mdx (Built-in Next.js Integration)
npm install @next/mdx @mdx-js/loader @mdx-js/react
// next.config.mjs
import createMDX from "@next/mdx";
const withMDX = createMDX({
options: {
remarkPlugins: [],
rehypePlugins: [],
},
});
/** @type {import('next').NextConfig} */
const nextConfig = {
pageExtensions: ["ts", "tsx", "md", "mdx"],
};
export default withMDX(nextConfig);
// mdx-components.tsx (required at project root for App Router)
import type { MDXComponents } from "mdx/types";
import { Code } from "@/components/code";
import { Callout } from "@/components/callout";
export function useMDXComponents(components: MDXComponents): MDXComponents {
return {
h1: ({ children }) => (
<h1 className="text-4xl font-bold mt-8 mb-4">{children}</h1>
),
pre: ({ children, ...props }) => <Code {...props}>{children}</Code>,
Callout,
...components,
};
}
mdx-bundler (Runtime Compilation)
npm install mdx-bundler esbuild
// lib/mdx.ts
import { bundleMDX } from "mdx-bundler";
import path from "path";
import fs from "fs/promises";
import remarkGfm from "remark-gfm";
import rehypeSlug from "rehype-slug";
import rehypeAutolinkHeadings from "rehype-autolink-headings";
export async function getCompiledMDX(slug: string) {
const filePath = path.join(process.cwd(), "content", `${slug}.mdx`);
const source = await fs.readFile(filePath, "utf-8");
const { code, frontmatter } = await bundleMDX({
source,
mdxOptions(options) {
options.remarkPlugins = [...(options.remarkPlugins ?? []), remarkGfm];
options.rehypePlugins = [
...(options.rehypePlugins ?? []),
rehypeSlug,
[rehypeAutolinkHeadings, { behavior: "wrap" }],
];
return options;
},
});
return { code, frontmatter };
}
// app/blog/[slug]/page.tsx
import { getCompiledMDX } from "@/lib/mdx";
import { MDXContent } from "@/components/mdx-content";
export default async function BlogPost({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const { code, frontmatter } = await getCompiledMDX(slug);
return (
<article className="prose dark:prose-invert max-w-none">
<h1>{frontmatter.title as string}</h1>
<MDXContent code={code} />
</article>
);
}
// components/mdx-content.tsx
"use client";
import { useMemo } from "react";
import { getMDXComponent } from "mdx-bundler/client";
import { Callout } from "./callout";
import { CodeBlock } from "./code-block";
const mdxComponents = {
Callout,
pre: CodeBlock,
};
export function MDXContent({ code }: { code: string }) {
const Component = useMemo(() => getMDXComponent(code), [code]);
return <Component components={mdxComponents} />;
}
Key Techniques
Frontmatter Extraction with Gray-Matter
// lib/frontmatter.ts
import matter from "gray-matter";
import fs from "fs/promises";
import path from "path";
export interface PostFrontmatter {
title: string;
description: string;
date: string;
tags: string[];
published: boolean;
}
export async function getPostFrontmatter(
slug: string
): Promise<PostFrontmatter> {
const filePath = path.join(process.cwd(), "content/posts", `${slug}.mdx`);
const raw = await fs.readFile(filePath, "utf-8");
const { data } = matter(raw);
return {
title: data.title,
description: data.description,
date: data.date,
tags: data.tags ?? [],
published: data.published ?? false,
};
}
Syntax Highlighting with Shiki
npm install shiki rehype-pretty-code
// next.config.mjs
import createMDX from "@next/mdx";
import rehypePrettyCode from "rehype-pretty-code";
const withMDX = createMDX({
options: {
rehypePlugins: [
[
rehypePrettyCode,
{
theme: "github-dark",
keepBackground: true,
onVisitLine(node: { children: unknown[] }) {
if (node.children.length === 0) {
node.children = [{ type: "text", value: " " }];
}
},
onVisitHighlightedLine(node: { properties: { className: string[] } }) {
node.properties.className.push("highlighted");
},
},
],
],
},
});
export default withMDX(nextConfig);
Custom MDX Components
// components/callout.tsx
import React from "react";
interface CalloutProps {
type?: "info" | "warning" | "error" | "tip";
title?: string;
children: React.ReactNode;
}
const styles = {
info: "bg-blue-50 border-blue-200 text-blue-900 dark:bg-blue-950 dark:border-blue-800 dark:text-blue-100",
warning: "bg-yellow-50 border-yellow-200 text-yellow-900 dark:bg-yellow-950 dark:border-yellow-800 dark:text-yellow-100",
error: "bg-red-50 border-red-200 text-red-900 dark:bg-red-950 dark:border-red-800 dark:text-red-100",
tip: "bg-green-50 border-green-200 text-green-900 dark:bg-green-950 dark:border-green-800 dark:text-green-100",
};
export function Callout({ type = "info", title, children }: CalloutProps) {
return (
<div className={`border-l-4 p-4 my-6 rounded-r ${styles[type]}`}>
{title && <p className="font-semibold mb-1">{title}</p>}
<div className="text-sm">{children}</div>
</div>
);
}
Table of Contents Generation
// lib/toc.ts
import { remark } from "remark";
import { visit } from "unist-util-visit";
import type { Heading } from "mdast";
export interface TocItem {
id: string;
text: string;
depth: number;
}
export async function extractToc(content: string): Promise<TocItem[]> {
const items: TocItem[] = [];
const tree = remark().parse(content);
visit(tree, "heading", (node: Heading) => {
const text = node.children
.filter((c) => c.type === "text")
.map((c) => ("value" in c ? c.value : ""))
.join("");
const id = text.toLowerCase().replace(/\s+/g, "-").replace(/[^\w-]/g, "");
items.push({ id, text, depth: node.depth });
});
return items;
}
Remark/Rehype Plugin Pipeline
// lib/mdx-plugins.ts
import remarkGfm from "remark-gfm";
import remarkMath from "remark-math";
import rehypeKatex from "rehype-katex";
import rehypeSlug from "rehype-slug";
import rehypeAutolinkHeadings from "rehype-autolink-headings";
import rehypePrettyCode from "rehype-pretty-code";
export const remarkPlugins = [remarkGfm, remarkMath];
export const rehypePlugins = [
rehypeSlug,
[rehypeAutolinkHeadings, { behavior: "append" }],
rehypeKatex,
[rehypePrettyCode, { theme: "one-dark-pro" }],
] as const;
Best Practices
- Define
mdx-components.tsxat the project root for App Router — Next.js auto-discovers it and applies it to all MDX files in the app directory. - Use
rehype-pretty-codewith Shiki for build-time syntax highlighting instead of client-side Prism; it produces zero-JS highlighted code blocks. - Keep MDX files in a
content/directory separate fromapp/to decouple content from routing logic. - Create a component map (
useMDXComponents) that maps HTML elements to your design system — this gives consistent styling without requiring authors to use custom components. - Use
gray-matterto parse frontmatter before passing to the MDX compiler so you can use frontmatter values in page metadata. - Add
remark-gfmto support GitHub Flavored Markdown features: tables, strikethrough, task lists, and autolinks. - Validate frontmatter with Zod schemas to catch missing fields at build time.
- Co-locate images with MDX files and use
next/imagevia custom component mapping for automatic optimization.
Anti-Patterns
- Importing heavy client components in MDX without boundaries: Every component imported into MDX ships to the client unless explicitly server-rendered. Wrap interactive components in dynamic imports.
- Using
dangerouslySetInnerHTMLfor MDX output: mdx-bundler and @next/mdx already produce React elements. Injecting raw HTML bypasses React's reconciliation and opens XSS vectors. - Skipping
rehype-slug: Without it, headings have noidattributes, breaking anchor links and table-of-contents navigation. - Embedding large data in MDX files: MDX compiles to JavaScript. Large inline JSON bloats the bundle. Fetch data in the page component and pass it as props instead.
- Using
evalornew Functionto run MDX client-side: Usemdx-bundler/clientor@mdx-js/reactwhich handle sandboxing. Rolling your own evaluation is fragile and insecure. - Neglecting dark mode in custom components: Always include
dark:variants in Tailwind classes for callouts, code blocks, and other styled MDX components. - Not memoizing
getMDXComponent: The function parses and creates a component on every call. Always wrap it inuseMemokeyed on the code string.
Install this skill directly: skilldb add seo-content-skills
Related Skills
Contentlayer
"Contentlayer and Velite for type-safe content management: transforming Markdown/MDX into typed data, schema validation, computed fields, Next.js integration, hot reload, and migration between content tools."
Core Web Vitals
Core Web Vitals optimization: LCP, INP, and CLS measurement, diagnosis, and improvement strategies for better search rankings and user experience.
Fumadocs
"fumadocs documentation framework: Next.js App Router native, MDX content collections, full-text search, OpenAPI integration, TypeScript-first, customizable UI components, and content source adapters."
Next SEO
"Next.js SEO and metadata management: meta tags, Open Graph, Twitter cards, JSON-LD structured data, canonical URLs, robots directives, and sitemap generation using the Metadata API and next-seo."
Nextra
"Nextra documentation framework: MDX-powered Next.js docs and blog sites, sidebar navigation, full-text search, i18n, themes (docs and blog), frontmatter configuration, and custom components."
Programmatic SEO
Programmatic SEO strategies: generating thousands of search-optimized pages from structured data, template design, internal linking, and indexing at scale.