Criando um blog com MDX, Next.js e Tailwind: por que markdown nunca é demais
Escrevendo um post cheio de componentes interativos, percebi que passava mais tempo mexendo no código do post do que no texto. Foi aí que resolvi reconstruir o blog com MDX. Neste post, mostro passo a passo como montar um blog com Next.js, MDX e Tailwind.
·5 min de leitura
Nesta página
Enquanto escrevia um post cheio de componentes interativos, percebi que estava gastando mais tempo no código dos componentes do que no texto em si. Foi aí que me bateu a dúvida: "será que não existe um jeito melhor de juntar código e conteúdo?"
Depois de testar algumas opções, resolvi refazer o blog usando MDX. Encaixar isso na minha stack deu mais trabalho do que eu esperava. Quebrei a cabeça com algumas configurações, mas no fim cheguei na versão que você está lendo agora.
Neste post, vou mostrar o processo todo, da configuração inicial aos ajustes finais, para você montar o seu próprio blog com Next.js, MDX e Tailwind CSS sem passar pelas mesmas dores de cabeça. Mas antes, por que MDX?
Por que MDX?
Se você já escreveu em Markdown, sabe como ele facilita a vida. A sintaxe é simples e dá para formatar texto sem mexer com HTML ou com editores visuais cheios de firula. Agora imagina misturar Markdown com componentes React. É exatamente isso que o MDX faz.
O MDX (Markdown + JSX) permite escrever os posts em Markdown e, no meio do texto, usar componentes React. Além de parágrafos, listas e imagens, você pode colocar botões, gráficos animados, alertas ou qualquer outro componente que quiser. Era essa flexibilidade que eu precisava para ilustrar melhor os conceitos e deixar os posts mais interessantes.
Configurando o projeto
Vamos começar criando o projeto com Next.js. Para o suporte ao MDX, vamos usar o @next/mdx, a biblioteca oficial do Next.js para isso.
Criando o projeto
No terminal, rode o comando abaixo:
bash
npx create-next-app@latest
shell
What is your project named? mdx-blog
Would you like to use TypeScript? Yes
Would you like to use ESLint? Yes
Would you like to use Tailwind CSS? Yes
Would you like your code inside a `src/` directory? Yes
Would you like to use App Router? (recommended) Yes
Would you like to use Turbopack for `next dev`? No
Would you like to customize the import alias (`@/*` by default)? No
Por que tantas dependências? (maldito JavaScript 😒)
@next/mdx, @mdx-js/loader, @mdx-js/react, @types/mdx: fazem o Next.js entender arquivos MDX, que misturam Markdown com JSX.
gray-matter: extrai os metadados de arquivos Markdown/MDX a partir do frontmatter, um bloco de YAML (ou JSON/TOML) no topo do arquivo.
remark-frontmatter, remark-mdx-frontmatter: fazem o parser do MDX reconhecer o bloco de frontmatter e transformam esses dados em um objeto acessível dentro do MDX.
Integrando o MDX ao Next.js
Com o projeto criado, vamos integrar o MDX ao Next.js:
next.config.ts
import createMDX from "@next/mdx";
import remarkFrontmatter from "remark-frontmatter";
import remarkMDXFrontmatter from "remark-mdx-frontmatter";
Adiciona suporte a arquivos MDX no Next.js com o @next/mdx.
Habilita a leitura do frontmatter com os plugins remark-frontmatter e remark-mdx-frontmatter.
Define as extensões de página (.mdx, .md, .js, .jsx, .ts, .tsx), para que arquivos nesses formatos possam ser tratados como páginas pelo Next.js.
Páginas dinâmicas para os posts
Com tudo configurado, podemos começar a estruturar o blog. Vamos usar esta organização de pastas:
text
┣ 📂 public/
┣ 📂 posts/ # posts e páginas escritos em MDX
┃ ┣ example.mdx
┣ 📂 src/
┃ ┣ 📂 app/
┃ ┃ ┣ 📂 blog/ # página principal do blog (lista de posts)
┃ ┃ ┃ ┣ 📂 [slug]/ # rota dinâmica para exibir cada post
┃ ┃ ┃ ┃ ┣ page.tsx
┃ ┃ ┃ ┣ page.tsx
┃ ┃ ┣ globals.css
┃ ┃ ┣ layout.tsx
┃ ┃ ┣ page.tsx
┃ ┣ 📂 components/
┃ ┣ 📂 widgets/ # componentes usados dentro do MDX
┃ ┣ 📂 utils/ # funções auxiliares, como o processamento do frontmatter
┃ ┣ 📂 lib/ # configurações do MDX, carregamento de arquivos etc.
┃ ┣ mdx-components.tsx # falamos desse carinha mais adiante
┣ .gitignore # arquivos e pastas ignorados pelo git
┣ package.json # dependências e scripts do projeto
┣ next.config.ts # configuração do Next.js, incluindo o suporte a MDX
┣ README.md # documentação do projeto
O que é o [slug]?
No Next.js, [slug] é uma rota dinâmica, que gera páginas automaticamente a partir de um identificador. No nosso caso, o identificador é o nome do arquivo MDX. Em vez de criar uma página na mão para cada post, o Next.js carrega e renderiza o conteúdo certo de acordo com a URL acessada.
No arquivo page.tsx da rota [slug], vamos criar um componente que pega o nome do post pela URL (por exemplo, /blog/post-exemplo), busca o arquivo correspondente (post-exemplo.mdx) e mostra o conteúdo na tela.
O código fica mais ou menos assim:
src/app/blog/[slug]/page.tsx
import { notFound } from "next/navigation";
export default async function PostPage({
params,
}: {
params: { slug: string };
}) {
try {
// Importa o post dinamicamente com base no slug da URL
const { default: Post } = await import(
`../../../../posts/${params.slug}.mdx`
);
return (
<article>
<div>
<span>{/* Title */}</span>
<span>{/* PublishedOn */}</span>
</div>
<Post />
</article>
);
} catch (error) {
// Se o arquivo não existir, mostra a página 404
notFound();
}
}
Assim, todo post novo que você colocar em posts/ vai estar disponível em /blog/nome-do-post, sem nenhuma configuração extra.
Ainda falta uma coisa: como acessar os metadados dos posts? A página precisa mostrar pelo menos o título e a data de publicação, e essas informações estão no frontmatter.
Criando o posts-helpers.ts
Para deixar o código organizado, vamos criar um arquivo posts-helpers.ts dentro da pasta utils/. Ele vai ter as funções auxiliares para lidar com os posts, como listar todos e carregar o conteúdo de um post específico.
src/utils/posts-helpers.ts
import fs from "fs/promises";
import path from "path";
import matter from "gray-matter";
type BlogPost = {
slug: string;
title: string;
abstract: string;
publishedOn: string; // Pode ser `Date`, se preferir trabalhar com objetos de data
};
/**
* Retorna a lista de posts do blog ordenada pela data de publicação (mais recentes primeiro).
*/
export async function getBlogPostList(): Promise<BlogPost[]> {
const fileNames = await readDirectory("posts"); // Lista os arquivos da pasta `posts/`
A grande vantagem do MDX é poder usar componentes React dentro dos posts. Para isso funcionar no Next.js, precisamos de um arquivo especial chamado mdx-components.tsx, que define quais componentes ficam disponíveis nos arquivos MDX.
src/mdx-components.tsx
import type { MDXComponents } from "mdx/types";
import ChromaticCircle from "@/widgets/chromatic-circle";
export function useMDXComponents(components: MDXComponents): MDXComponents {
return {
...components,
ChromaticCircle, // Deixa o componente disponível nos arquivos MDX
};
}
Com o componente registrado, já dá para usar ele em um post MDX:
posts/example.mdx
## Círculo cromático
Se você trabalha com cores ...
<ChromaticCircle type="selector" />
Só um detalhe: o Next.js só encontra o mdx-components.tsx se ele estiver na raiz da pasta src/. A estrutura fica assim:
text
📂 src/
┣ 📂 components/
┣ 📂 widgets/ <-- componentes para o MDX
┃ ┗ 📜 chromatic-circle.tsx
┗ 📜 mdx-components.tsx
Próximos passos
A partir daqui, é só criar seus arquivos .mdx na pasta posts/ e começar a escrever. O que montamos aqui é o esqueleto: página de listagem, página de post e suporte a componentes. Estilo, tags, feed RSS e o resto ficam por sua conta.
Se você montar o seu, me manda o link. Quero ver o que você vai colocar no meio do texto.