Redraw do blog: de Next.js 12 para 16
Como reescrevi o blog do zero, saindo do Pages Router com styled-components para App Router com Tailwind CSS 4
Por que reescrever?
O blog rodava em Next.js 12 com Pages Router, styled-components e Preact aliado em produção. Funcionava, mas o ecossistema mudou muito desde 2021: Server Components, App Router, Tailwind CSS 4, React 19. Atualizar incrementalmente seria mais trabalho do que começar do zero.
A decisão foi simples: reescrever com a stack atual e aproveitar para repensar o design.
O que mudou na stack
| Antes | Depois |
|---|---|
| Next.js 12 (Pages Router) | Next.js 16 (App Router) |
| styled-components | Tailwind CSS 4 |
| Preact em produção | React 19 nativo |
_app.tsx + _document.tsx |
app/layout.tsx |
getStaticProps / getStaticPaths |
generateStaticParams + generateMetadata |
| next-pwa | Removido |
| @svgr/webpack | lucide-react |
App Router na prática
A maior mudança é conceitual. No Pages Router, cada arquivo em pages/ é uma rota com funções de data fetching (getStaticProps). No App Router, a estrutura de pastas define o layout e o roteamento, e os componentes são Server Components por padrão.
O que isso muda no dia a dia:
// Antes: pages/[slug].tsx
export async function getStaticPaths() { ... }
export async function getStaticProps({ params }) { ... }
export default function Post({ post }) { ... }
// Depois: app/blog/[slug]/page.tsx
export async function generateStaticParams() { ... }
export async function generateMetadata({ params }) { ... }
export default async function BlogPost({ params }) { ... }
O componente da página agora pode ser async, buscando dados direto no corpo da função, sem precisar de props intermediárias. Menos boilerplate.
Metadata API
Antes eu montava <Head> manualmente com next/head. Agora o Next.js tem uma API nativa que gera <title>, Open Graph, Twitter Cards e canonical URLs:
export const metadata: Metadata = {
title: {
default: 'Matheus Felipe · Full Stack Developer',
template: '%s · Matheus Felipe',
},
openGraph: { siteName: 'Matheus Felipe', type: 'website' },
}
Cada página pode exportar generateMetadata() para valores dinâmicos. O framework cuida de mesclar com o layout pai.
Tailwind CSS 4
Styled-components funcionava bem, mas significava manter arquivos style.ts ao lado de cada componente. Tailwind simplifica: o estilo fica inline no JSX e não precisa de runtime CSS-in-JS.
A versão 4 do Tailwind trouxe mudanças importantes. A configuração agora é via CSS, não mais tailwind.config.js:
@import "tailwindcss";
@theme {
--color-bg: #000000;
--color-primary: #fafafa;
--color-secondary: #a1a1aa;
--color-accent: #6366f1;
}
Um detalhe que me pegou: o reset do Tailwind 4 (preflight) já cuida de margin, padding e box-sizing. Se você adicionar um * { margin: 0; } manual no CSS, vai sobrescrever utilitários como mx-auto e quebrar a centralização do layout. Perdi um tempo com isso.
Dark mode sem flash
O tema dark/light precisa ser aplicado antes do primeiro paint, senão o usuário vê um flash branco. A solução é um script bloqueante no <head>:
const themeScript = `
(function() {
try {
var t = localStorage.getItem('theme');
var theme = (t === 'light' || t === 'dark') ? t : 'dark';
document.documentElement.classList.add(theme);
} catch(e) {
document.documentElement.classList.add('dark');
}
})();
`
O CSS default do body assume dark. O script só precisa adicionar a classe, não remove nada. E o <html> usa suppressHydrationWarning porque o servidor renderiza sem a classe, mas o script a adiciona antes do React hidratar.
SEO
O blog antigo tinha SEO básico. Na reescrita, cobri tudo que o Lighthouse pede:
- Sitemap dinâmico (
app/sitemap.ts), gerado a partir dos posts - robots.txt dinâmico (
app/robots.ts) - RSS feed (
app/feed.xml/route.ts) - JSON-LD:
WebSite,Person,ArticleeBreadcrumbList - Canonical URLs em todas as páginas
- Open Graph + Twitter Cards com
generateMetadata
Tudo nativo do Next.js, sem plugins.
O papel da IA na migração
Usei LLM como copiloto durante a reescrita. O valor maior foi na parte mecânica: migrar frontmatter dos posts, gerar boilerplate de SEO (JSON-LD, sitemap), e fazer o scaffolding inicial da estrutura do App Router. São tarefas repetitivas onde a IA economiza tempo sem comprometer a qualidade.
As decisões de design, arquitetura e os ajustes finos continuam sendo humanos. A IA não sabe como o seu blog precisa se sentir.
Pipeline de conteúdo
O pipeline de Markdown ficou praticamente igual: gray-matter para frontmatter + remark com remark-gfm e remark-html. Os posts continuam como arquivos .md no repositório, sem CMS, sem banco de dados.
O frontmatter ficou mais simples:
# Antes
createdAt:
iso: '2021-07-31T03:00:00.000Z'
formated: '31/07/2021'
# Depois
createdAt: '2021-07-31'
A formatação de data agora é feita no componente com toLocaleDateString.
O que eu faria diferente
- Syntax highlighting:
remark-prismdeu problemas com Turbopack. Da próxima vez, usariashikidesde o início, que funciona melhor com o ecossistema atual. - Começar pelo mobile: desenhei pensando em desktop e ajustei para mobile depois. O inverso funciona melhor.
Resultado
O blog agora roda em Next.js 16, carrega mais rápido (sem runtime CSS-in-JS), tem SEO completo, e o código é mais simples de manter. O design ficou minimalista, com conteúdo em foco e sem distrações.
O código é open source: github.com/xmatheus/blog