Stash da Ana
nextjsmdxstatic-sitetutorial

Gerando sites estáticos com Next.js e escrevendo conteúdo em MDX

4 min de leitura

O Next.js nasceu como framework de Server-Side Rendering — um servidor Node.js que monta o HTML a cada requisição. Para muitos projetos, especialmente blogs, esse servidor é peso desnecessário: o conteúdo só muda quando você publica. O modo output: 'export' resolve isso gerando o site inteiro em arquivos estáticos no momento do build. O resultado é uma pasta de HTML, CSS e JS que pode ser hospedada em qualquer coisa — GitHub Pages, Netlify, Vercel, um bucket S3 — sem nenhum runtime no servidor.

Ativando o export

Uma linha em next.config.ts:

import type { NextConfig } from 'next'
 
const nextConfig: NextConfig = {
  output: 'export',
}
 
export default nextConfig

A partir daí, next build cria a pasta out/ com tudo pronto para subir:

out/
├── index.html
├── blog/
│   ├── meu-primeiro-post/
│   │   └── index.html
│   └── outro-post/
│       └── index.html
└── _next/
    └── static/   ← JS e CSS com hashes de cache

Rotas dinâmicas precisam de slugs conhecidos

Páginas como /blog/[slug] exigem que o Next.js saiba quais slugs existem antes do build. É para isso que serve generateStaticParams:

// src/app/blog/[slug]/page.tsx
import { getAllSlugs } from '@/lib/posts'
 
export async function generateStaticParams() {
  return getAllSlugs().map((slug) => ({ slug }))
}

A função é chamada uma vez no build, devolve a lista de parâmetros, e o Next gera um HTML por elemento da lista. Nada é resolvido em runtime: um slug que não estava na lista no momento do build simplesmente não tem página.

Lendo os arquivos markdown no build

Com export estático, os dados precisam vir de fontes que existam em build time — arquivos locais, idealmente. No blog, são .md em content/posts/, lidos com fs e gray-matter:

import fs from 'fs'
import path from 'path'
import matter from 'gray-matter'
 
export function getPostBySlug(slug: string) {
  const filePath = path.join(process.cwd(), 'content/posts', `${slug}.md`)
  const raw = fs.readFileSync(filePath, 'utf-8')
  const { data, content } = matter(raw)
  return { meta: data, content }
}

gray-matter separa o frontmatter YAML (entre ---) do corpo. O retorno é um objeto com os metadados parseados em data e o markdown puro em content. Importante: fs só funciona em Server Components ou em código chamado durante o build; em qualquer arquivo que rode no browser, o import quebra a compilação.

Por que MDX

Markdown puro cobre 90% do texto de um post — parágrafos, listas, código, links. O 10% restante são as coisas que pedem comportamento ou design próprio: um aviso destacado, uma demonstração interativa, um componente de comparação. Inserir HTML cru no .md resolve, mas é feio e perde a tipagem.

MDX permite usar componentes React diretamente dentro do markdown:

## Exemplo de alerta
 
<Callout type="warning">
  Cuidado ao usar `fs` fora de Server Components — isso não funciona no browser.
</Callout>
 
O restante do parágrafo continua em Markdown normal.

O componente vive no meio do texto sem ruído.

Arquivo .mdx no editor ao lado da renderização no browser Esquerda: o .mdx cru. Direita: a página renderizada — o markdown vira HTML e o <Callout> vira o componente React real.

Integrando MDX com next-mdx-remote

Para renderizar MDX em projetos com App Router, next-mdx-remote é a opção mais usada:

import { MDXRemote } from 'next-mdx-remote/rsc'
import { CodeBlock } from '@/components/mdx/CodeBlock'
import { Callout } from '@/components/mdx/Callout'
 
const components = {
  pre: CodeBlock,
  Callout,
}
 
export function MdxContent({ source }: { source: string }) {
  return <MDXRemote source={source} components={components} />
}

O mapa components faz duas coisas: substitui tags HTML padrão (aqui pre, para um componente de código com syntax highlight) e registra componentes customizados utilizáveis no texto (Callout). Qualquer nome ausente do mapa cai no comportamento default do markdown.

O caminho completo

Resumindo o fluxo: getPostBySlug lê o arquivo, gray-matter separa frontmatter e conteúdo, MDXRemote compila o conteúdo em JSX, o React renderiza, e o next build materializa o HTML final em out/blog/<slug>/index.html. Tudo acontece uma única vez, no build. O visitante recebe HTML puro com JS de hidratação só para as partes interativas.

Estrutura da pasta out/ depois de um next build A pasta out/ é literalmente o site — pode ser servida por qualquer HTTP server estático ou subida intacta para um bucket.

Uma observação sobre generateStaticParams

Em projetos com muitos posts, é fácil chamar a função de leitura de arquivos várias vezes durante o build — uma em generateStaticParams, outra em generateMetadata, outra na própria página. Centralizar o índice (um getAllSlugs() que lê o diretório uma vez e devolve um array cacheado em memória) economiza I/O e mantém o build rápido. Para um blog pequeno, invisível; passando das centenas de posts, faz diferença.