Site de Documentação Next.js + MDX
Crie um site de documentação com Next.js 15, MDX e Tailwind - a arquitetura por trás deste cookbook.
Busque em todas as páginas da documentação
Crie um site de documentação com Next.js 15, MDX e Tailwind - a arquitetura por trás deste 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/ na raiz do projeto para arquivos .md, organizados como docs/<section>/<slug>.md.app/docs/[section]/[slug]/page.tsx.fs, analise o frontmatter com gray-matter, renderize com <MDXRemote> de next-mdx-remote/rsc.generateStaticParams para pré-renderizar todos os docs em tempo de compilação.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\} />
),
\};Passe-os para <MDXRemote source=\{content\} components=\{mdxComponents\} />.
next-mdx-remote/rsc é um Server Component React assíncrono que compila MDX no servidor e transmite o resultado. Não há runtime MDX no lado do cliente, nenhuma etapa de empacotamento para seu conteúdo e nenhum pipeline de compilação separado. Em tempo de compilação, generateStaticParams produz cada par (section, slug) para que o Next.js pré-renderize estaticamente cada página de documento em HTML.
gray-matter divide cada arquivo .md em um objeto frontmatter e uma string de conteúdo. A string de conteúdo é passada literalmente para MDXRemote, que lida com a análise, transformação (via plugins remark/rehype) e renderização.
rehype-pretty-code e Shiki para realce de qualidade VS Code em tempo de compilação:
<MDXRemote
source=\{content\}
options=\{\{ mdxOptions: \{ rehypePlugins: [[rehypePrettyCode, \{ theme: "github-dark" \}]] \} \}\}
/>remark-gfm para tabelas, listas de tarefas e riscado.h1, h2, pre, a, img para componentes React estilizados para uma prosa consistente.docs/ em tempo de compilação com getAllDocs(), agrupe por seção e renderize um componente de navegação.rehype-slug e remark-extract-toc, renderize um TOC fixo no layout do artigo.DocFrontmatter e faça o cast de matter(raw).data as DocFrontmatter. Considere a validação com zod para falhar explicitamente em frontmatter malformado.params é uma Promise - tipifique-a como params: Promise<\{ section: string; slug: string \}> e use await antes de usá-la.MDXComponents de mdx/types.\{foo\} na prosa tentará avaliar foo como JavaScript e falhará a compilação. Escape com uma barra invertida \\\{foo\\\}, envolva em crases ou use entidades HTML {.<Component /> na prosa é analisado como JSX. Se o componente não estiver em escopo, o MDX lança um erro. Envolva em crases ou escape < como <..md relativos. Links de markdown como [ver](./other.md) não correspondem à sua estrutura de rota. Reescreva-os no momento da renderização com um componente a personalizado ou um plugin remark.MDXRemote é assíncrono. Ele só funciona em Server Components. Colocá-lo em um Client Component lança um erro em tempo de execução.fs só roda no lado do servidor. Importar lib/docs.ts de um Client Component aciona Module not found: node:fs. Mantenha-o em RSC ou em manipuladores de rota.export const dynamic = "force-static", os docs recém-adicionados exigirão uma reconstrução. Use revalidate ou exclua o cache se desejar reconstruções sob demanda.gray-matter retorna quaisquer chaves que encontra. Valide com zod ou uma asserção tipada para capturar campos title ou order ausentes precocemente.| Ferramenta | Estilo | Ideal Para |
|---|---|---|
| next-mdx-remote | Compatível com RSC, flexível | Sites de documentação personalizados no Next.js |
| Contentlayer | Camada de conteúdo type-safe | Descontinuado - sem manutenção |
| Velite | Sucessor do Contentlayer | Conteúdo type-safe com validação |
| Fumadocs | Framework de documentação completo | Documentação "pronta para usar" no Next.js |
| Nextra | Framework de documentação no Next.js | Opinioso, configuração rápida |
| Mintlify | SaaS hospedado | Documentação hospedada sem configuração |
| Starlight | Baseado em Astro | Sites de documentação fora do ecossistema Next.js |
@next/mdx compila arquivos .mdx como rotas em tempo de compilação - ótimo se seu conteúdo estiver em app/. next-mdx-remote compila MDX de strings arbitrárias, então seu conteúdo pode viver em qualquer lugar (uma pasta docs/, um CMS, um banco de dados) e suas rotas permanecem desacopladas do layout do sistema de arquivos.
Sim. next-mdx-remote aceita qualquer string MDX. Troque fs.readFile por um fetch contra seu CMS (Contentful, Sanity, Notion) e passe o markdown retornado para <MDXRemote source=\{content\} />.
Instale rehype-pretty-code e shiki, então passe-o via mdxOptions.rehypePlugins. Ele é executado em tempo de compilação, então não há custo no lado do cliente:
<MDXRemote
source=\{content\}
options=\{\{
mdxOptions: \{
rehypePlugins: [[rehypePrettyCode, \{ theme: "github-dark" \}]],
\},
\}\}
/>Chame getAllDocs() em seu layout, agrupe os resultados por section, ordene dentro de cada seção por order e renderize como uma lista. Como é um Server Component, a barra lateral está sempre sincronizada com o sistema de arquivos.
Use rehype-slug para adicionar IDs aos títulos, então percorra a AST do MDX com remark-extract-toc (ou um pequeno plugin personalizado) e passe o resultado para um componente de barra lateral. Para uma abordagem mais simples, extraia os títulos com uma regex no markdown bruto.
O MDX está tentando analisar algo em sua prosa como JSX. Encontre a tag ofensiva - muitas vezes genérica como <T> dentro de uma assinatura de tipo fora de um bloco de código - e envolva-a em crases ou escape o <. Esta é a falha de compilação MDX mais comum.
Se seu trecho de código estiver dentro de um bloco de código delimitado (crase tripla), as chaves são renderizadas normalmente. Se você acidentalmente as soltou na prosa, o MDX as avaliou como expressões. Mova o conteúdo para um bloco de código ou escape com uma barra invertida.
Escreva um componente a personalizado que reescreve valores de href que terminam em .md para a rota correspondente:
const a = (\{ href, ...rest \}: any) => \{
const rewritten = href?.endsWith(".md")
? href.replace(/\.md$/, "")
: href;
return <a href=\{rewritten\} \{...rest\} />;
\};Passe-o através da prop components para <MDXRemote>.
Defina uma interface e faça o cast do resultado de matter:
interface DocFrontmatter \{ title: string; order: number \}
const \{ data \} = matter(raw);
const frontmatter = data as DocFrontmatter;Para segurança em tempo de execução, envolva o cast em um .parse() de esquema zod para que frontmatter incorreto falhe a compilação.
No Next.js 15, os params de rota dinâmica são uma Promise. Tipifique-a e use await:
interface PageProps \{
params: Promise<\{ section: string; slug: string \}>;
\}
export default async function Page(\{ params \}: PageProps) \{
const \{ section, slug \} = await params;
\}Sim - importe um Client Component (marcado com "use client") e passe-o através do mapa components. O MDX circundante permanece um Server Component, mas o Client Component incorporado hidrata normalmente.
Use Nextra ou Fumadocs se você quiser um framework de documentação "pronto para usar" com pesquisa, temas e versionamento prontos para uso. Crie o seu próprio com next-mdx-remote se precisar de layouts personalizados, fontes de conteúdo incomuns ou integração estreita com o resto de um aplicativo Next.js (como este cookbook).
Revisado por Chris St. John·Última atualização: 19 de jul. de 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥