~/blog/por-que-pre-renderizar-um-portfolio-spa

cat por-que-pre-renderizar-um-portfolio-spa.mdx

Por que um portfólio em SPA não serve como blog

3 min de leitura

Um site React client-side resolve bem um portfólio, mas quebra no momento em que você quer que seus textos sejam encontrados e compartilhados. Como resolvi isso sem reescrever tudo.

Meu portfólio era uma SPA em React com Vite. Para mostrar projetos, isso funciona perfeitamente: quem chega pelo link vê a página montar em milissegundos e navega sem recarregar nada.

Quando decidi transformá-lo em blog, o modelo deixou de servir, e o motivo não é performance.

O problema não é o Google, é o compartilhamento

A primeira reação de todo mundo é "mas o Google executa JavaScript". Executa mesmo. O problema está em outro lugar.

As meta tags de um SPA vivem em um único index.html:

<meta property="og:title" content="Carlos Eduardo Ferreira | Software Engineer" />
<meta property="og:description" content="Software Engineer no Rio de Janeiro..." />

Esse arquivo é o mesmo para todas as rotas. Então, quando alguém compartilha /blog/meu-artigo no LinkedIn, no X ou no WhatsApp, o crawler lê o HTML servido (nunca o resultado depois do React rodar) e mostra o preview genérico do portfólio. Todo artigo fica com o mesmo título e a mesma descrição.

Atualizar document.title em um efeito não resolve:

useEffect(() => {
  document.title = post.title; // o crawler social nunca chega aqui
}, [post.title]);

Crawlers de rede social quase nunca executam JavaScript. Para eles, seu blog inteiro é uma página só.

Três caminhos

Avaliei três opções:

CaminhoSEOEsforçoO que acontece com o código atual
Continuar client-sideFracoNenhumNada muda
Migrar para AstroÓtimoAltoShell e providers reescritos
Vite + pré-render no buildÓtimoMédioPreservado

Astro é a escolha canônica para sites de conteúdo, e eu recomendaria para quem começa do zero. No meu caso havia um shell que eu gostava, com sidebar colapsável, background reativo ao mouse e animações, e reescrevê-lo como layout Astro era jogar trabalho fora para resolver um problema de meta tags.

O que eu fiz

Mantive a SPA e adicionei um passo depois do build. O npm run build passou a ter três etapas:

{
  "build": "npm run build:client && npm run build:server && npm run prerender",
  "build:client": "vite build",
  "build:server": "vite build --ssr",
  "prerender": "node scripts/prerender.mjs"
}

A segunda etapa compila a mesma aplicação para rodar em Node. A terceira percorre cada rota conhecida, renderiza com renderToString, injeta as meta tags daquela rota e grava um arquivo HTML de verdade:

dist/public/
├── index.html
├── blog/index.html
├── blog/meu-artigo/index.html
├── til/index.html
├── rss.xml
└── sitemap.xml

O resultado: cada artigo tem título, descrição, og:image e JSON-LD próprios no HTML servido. A SPA continua existindo: ela hidrata por cima do HTML pré-renderizado e a navegação interna segue instantânea.

O detalhe que quase me pegou

O corpo dos artigos é carregado sob demanda, em um chunk separado. Isso é ótimo para o tamanho do bundle e terrível para hidratação: o servidor renderiza o texto completo, e o cliente, no primeiro render, ainda não tem esse módulo em memória. React reclama da divergência e descarta o HTML.

A solução foi esperar o módulo antes de hidratar:

async function start() {
  await preloadBodyForPath(window.location.pathname);

  if (container.firstElementChild) hydrateRoot(container, <App />);
  else createRoot(container).render(<App />);
}

Como o HTML pré-renderizado já está visível na tela, essa espera é invisível para quem lê.

O que eu levaria para o próximo projeto

Se o site vai ter conteúdo desde o começo, use um framework que gere HTML estático e pare de pensar nisso. Se o conteúdo chega depois, como no meu caso, pré-renderizar rotas conhecidas é um caminho curto e honesto, e você não precisa abandonar nada do que já construiu.

LER~/blog/por-que-pre-renderizar-um-portfolio-spa.mdxmdx · utf-8topo
cd ../blog