Sitio de documentación Next.js + MDX
Crea un sitio de documentación con Next.js 15, MDX y Tailwind - la arquitectura detrás de este cookbook.
Busca en todas las páginas de la documentación
Crea un sitio de documentación con Next.js 15, MDX y Tailwind - la arquitectura detrás de este cookbook.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
npx create-next-app@latest docs-site --typescript --tailwind --app
cd docs-sitenpm install next-mdx-remote gray-matterdocs/ en la raíz del proyecto para archivos .md, organizados como docs/<section>/<slug>.md.app/docs/[section]/[slug]/page.tsx.fs, analiza el frontmatter con gray-matter, renderiza con <MDXRemote> de next-mdx-remote/rsc.generateStaticParams para prerrenderizar todos los documentos en tiempo de compilación.lib/docs.tsimport fs from "node:fs/promises";
import path from "node:path";
import matter from "gray-matter";
const DOCS_DIR = path.join(process.cwd(), "docs");
export interface DocFrontmatter \{
title: string;
section: string;
order: number;
tags?: string[];
\}
export interface Doc \{
section: string;
slug: string;
frontmatter: DocFrontmatter;
content: string;
\}
export async function getAllDocs(): Promise<Doc[]> \{
const sections = await fs.readdir(DOCS_DIR, \{ withFileTypes: true \});
const docs: Doc[] = [];
for (const section of sections) \{
if (!section.isDirectory()) continue;
const files = await fs.readdir(path.join(DOCS_DIR, section.name));
for (const file of files) \{
if (!file.endsWith(".md")) continue;
const raw = await fs.readFile(
path.join(DOCS_DIR, section.name, file),
"utf8"
);
const \{ data, content \} = matter(raw);
docs.push(\{
section: section.name,
slug: file.replace(/\.md$/, ""),
frontmatter: data as DocFrontmatter,
content,
\});
\}
\}
return docs.sort((a, b) => a.frontmatter.order - b.frontmatter.order);
\}
export async function getDoc(section: string, slug: string): Promise<Doc | null> \{
try \{
const raw = await fs.readFile(
path.join(DOCS_DIR, section, `$\{slug\}.md`),
"utf8"
);
const \{ data, content \} = matter(raw);
return \{
section,
slug,
frontmatter: data as DocFrontmatter,
content,
\};
\} catch \{
return null;
\}
\}app/docs/[section]/[slug]/page.tsximport \{ notFound \} from "next/navigation";
import \{ MDXRemote \} from "next-mdx-remote/rsc";
import \{ getAllDocs, getDoc \} from "@/lib/docs";
export async function generateStaticParams() \{
const docs = await getAllDocs();
return docs.map((doc) => (\{
section: doc.section,
slug: doc.slug,
\}));
\}
interface PageProps \{
params: Promise<\{ section: string; slug: string \}>;
\}
export default async function DocPage(\{ params \}: PageProps) \{
const \{ section, slug \} = await params;
const doc = await getDoc(section, slug);
if (!doc) notFound();
return (
<article className="prose prose-slate mx-auto max-w-3xl py-12">
<h1>\{doc.frontmatter.title\}</h1>
<MDXRemote source=\{doc.content\} />
</article>
);
\}// components/mdx-components.tsx
import type \{ MDXComponents \} from "mdx/types";
export const mdxComponents: MDXComponents = \{
h2: (props) => <h2 className="mt-8 text-2xl font-bold" \{...props\} />,
code: (props) => (
<code className="rounded bg-slate-100 px-1 py-0.5 text-sm" \{...props\} />
),
\};Pásalos a <MDXRemote source=\{content\} components=\{mdxComponents\} />.
next-mdx-remote/rsc es un componente async React Server que compila MDX en el servidor y transmite el resultado. No hay tiempo de ejecución MDX del lado del cliente, no hay paso de bundler para tu contenido y no hay pipeline de compilación separada. En tiempo de compilación, generateStaticParams produce cada par (section, slug) para que Next.js prerrenderice estáticamente cada página de documento en HTML.
gray-matter divide cada archivo .md en un objeto de frontmatter y una cadena de contenido. La cadena de contenido se pasa verbatim a MDXRemote, que maneja el análisis, la transformación (a través de plugins remark/rehype) y el renderizado.
rehype-pretty-code y Shiki para resaltado de calidad de VS Code en tiempo de compilación:
<MDXRemote
source=\{content\}
options=\{\{ mdxOptions: \{ rehypePlugins: [[rehypePrettyCode, \{ theme: "github-dark" \}]] \} \}\}
/>remark-gfm para tablas, listas de tareas y tachado.h1, h2, pre, a, img a componentes React estilizados para una prosa consistente.docs/ en tiempo de compilación con getAllDocs(), agrupa por sección y renderiza un componente de navegación.rehype-slug y remark-extract-toc, renderiza una tabla de contenidos pegajosa en el diseño del artículo.DocFrontmatter y lanza matter(raw).data as DocFrontmatter. Considera la validación de zod para fallar ruidosamente en el frontmatter malformado.params es una Promise - escríbela como params: Promise<\{ section: string; slug: string \}> y await antes de usarla.MDXComponents de mdx/types.\{foo\} en prosa intentará evaluar foo como JavaScript y fallará en la compilación. Escapa con una barra invertida \\\{foo\\\}, envuelve en backticks o usa entidades HTML {.<Component /> en prosa se analiza como JSX. Si el componente no está en scope, MDX lanza. Envuelve en backticks o escapa < como <..md. Los enlaces de Markdown como [see](./other.md) no coinciden con tu estructura de rutas. Reescríbelos en tiempo de renderizado con un componente a personalizado o un plugin remark.MDXRemote es async. Solo funciona en Server Components. Ponerlo en un Client Component lanza en tiempo de ejecución.fs solo se ejecuta del lado del servidor. Importar lib/docs.ts desde un Client Component activa Module not found: node:fs. Mantenlo en RSC o manejadores de rutas.export const dynamic = "force-static", los documentos recién añadidos requieren una reconstrucción. Usa revalidate o elimina la caché si quieres reconstrucciones bajo demanda.gray-matter devuelve las claves que encuentra. Valida con zod o una aserción tipada para detectar los campos title u order faltantes temprano.| Herramienta | Estilo | Mejor para |
|---|---|---|
| next-mdx-remote | Amigable con RSC, flexible | Sitios de documentación personalizados en Next.js |
| Contentlayer | Capa de contenido type-safe | Deprecado - sin mantenimiento |
| Velite | Sucesor de Contentlayer | Contenido type-safe con validación |
| Fumadocs | Marco de documentación completo | Documentación lista para usar en Next.js |
| Nextra | Marco de documentación en Next.js | Opinado, configuración rápida |
| Mintlify | SaaS alojado | Documentos alojados sin configuración |
| Starlight | Basado en Astro | Sitios de documentación fuera del ecosistema Next.js |
@next/mdx compila archivos .mdx como rutas en tiempo de compilación - excelente si tu contenido vive en app/. next-mdx-remote compila MDX de cadenas arbitrarias, por lo que tu contenido puede vivir en cualquier lugar (una carpeta docs/, un CMS, una base de datos) y tus rutas permanecen desacopladas del diseño del sistema de archivos.
Sí. next-mdx-remote acepta cualquier cadena MDX. Intercambia fs.readFile por un fetch contra tu CMS (Contentful, Sanity, Notion) y pasa el markdown devuelto a <MDXRemote source=\{content\} />.
Instala rehype-pretty-code y shiki, luego pásalo a través de mdxOptions.rehypePlugins. Se ejecuta en tiempo de compilación, por lo que no hay costo en el lado del cliente:
<MDXRemote
source=\{content\}
options=\{\{
mdxOptions: \{
rehypePlugins: [[rehypePrettyCode, \{ theme: "github-dark" \}]],
\},
\}\}
/>Llama a getAllDocs() en tu layout, agrupa los resultados por section, ordena dentro de cada sección por order y renderiza como lista. Como es un Server Component, la barra lateral siempre está sincronizada con el sistema de archivos.
Usa rehype-slug para añadir IDs a los encabezados, luego camina por el AST de MDX con remark-extract-toc (o un pequeño plugin personalizado) y pasa el resultado a un componente de barra lateral. Para un enfoque más simple, extrae encabezados con una regex sobre el markdown sin procesar.
MDX está intentando analizar algo en tu prosa como JSX. Encuentra la etiqueta ofensiva - a menudo un genérico como <T> dentro de una firma de tipo fuera de un bloque de código - y envuélvela en backticks o escapa el <. Esta es la falla de compilación MDX más común.
Si tu fragmento de código vive dentro de un bloque de código cercado (triple-backtick), las llaves se renderizan bien. Si accidentalmente las dejaste caer en prosa, MDX las evaluó como expresiones. Mueve el contenido a un cerco de código o escapa con una barra invertida.
Escribe un componente a personalizado que reescriba los valores de href terminados en .md a la ruta coincidente:
const a = (\{ href, ...rest \}: any) => \{
const rewritten = href?.endsWith(".md")
? href.replace(/\.md$/, "")
: href;
return <a href=\{rewritten\} \{...rest\} />;
\};Pásalo a través de la prop components a <MDXRemote>.
Define una interfaz y lanza el resultado de matter:
interface DocFrontmatter \{ title: string; order: number \}
const \{ data \} = matter(raw);
const frontmatter = data as DocFrontmatter;Para seguridad en tiempo de ejecución, envuelve la proyección en un esquema zod .parse() para que el frontmatter incorrecto falle en la compilación.
En Next.js 15, la ruta dinámica params es una Promise. Escríbela y espera:
interface PageProps \{
params: Promise<\{ section: string; slug: string \}>;
\}
export default async function Page(\{ params \}: PageProps) \{
const \{ section, slug \} = await params;
\}Sí - importa un Client Component (marcado con "use client") y pásalo a través del mapa components. El MDX circundante permanece como un Server Component, pero el Client Component incrustado se hidrata normalmente.
Usa Nextra o Fumadocs si quieres un marco de documentación listo para usar con búsqueda, temas y versionado fuera de la caja. Construye el tuyo con next-mdx-remote si necesitas diseños personalizados, fuentes de contenido inusuales o integración estrecha con el resto de una aplicación Next.js (como este cookbook).
Revisado por Chris St. John·Última actualización: 19 jul 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥