Pular para o conteúdo
Zeno .blog
Artigo 2 min de leitura

SPA, WhatsApp e a prévia de link que não aparece

Crawler de link não roda JavaScript. Como gerar um HTML por rota no build do Vite e fazer a foto certa aparecer.

índice deste post
  1. Por que acontece
  2. A solução: um HTML por rota, no build
  3. A imagem certa
  4. Testar antes de mandar para o cliente

O site está no ar, lindo, rápido. Você manda o link de um produto no WhatsApp e a prévia aparece com o título da página inicial e a foto genérica. Ou sem foto nenhuma.

Isso acontece com quase toda SPA — site React com React Router, por exemplo. O motivo é simples, e a solução cabe num plugin de 50 linhas.

Por que acontece

Numa SPA, o servidor entrega o mesmo index.html para todas as rotas. Quem troca título, descrição e imagem conforme a página é o JavaScript, depois de carregar.

O robô que monta a prévia — do WhatsApp, do LinkedIn, do Facebook — não executa JavaScript. Ele baixa o HTML, lê as meta tags og: e vai embora. Para ele, toda página do site é a home.

A solução: um HTML por rota, no build

Se o robô só lê HTML, cada rota precisa ter o seu, já com as meta tags certas. Não precisa de SSR nem de framework novo: um plugin do Vite, rodando no fim do build, copia o index.html para dist/<rota>/index.html trocando as tags.

// vite.config.ts (resumido)
function metaPorRota(rotas: Meta[]): Plugin {
  return {
    name: 'meta-por-rota',
    apply: 'build',
    closeBundle() {
      const modelo = readFileSync('dist/index.html', 'utf8')
      for (const rota of rotas) {
        let html = modelo.replace(/<title>[^<]*<\/title>/, `<title>${rota.titulo}</title>`)
        html = trocar(html, 'property="og:title"', rota.titulo)
        html = trocar(html, 'property="og:image"', rota.imagem)
        // … description, og:description, og:url, canonical
        const destino = resolve('dist', rota.caminho.slice(1), 'index.html')
        mkdirSync(dirname(destino), { recursive: true })
        writeFileSync(destino, html)
      }
    },
  }
}

O detalhe que mais importa está no trocar: se a tag não existir, o build para.

function trocar(html: string, seletor: string, valor: string) {
  const alvo = new RegExp(`(<meta[^>]*${seletor}[^>]*content=")[^"]*(")`, 'i')
  if (!alvo.test(html)) throw new Error(`meta-por-rota: não achei ${seletor}`)
  return html.replace(alvo, `$1${escapar(valor)}$2`)
}

Hospedagem estática como a do Cloudflare entrega dist/produtos/x/index.html quando alguém pede /produtos/x. Rota que não tem arquivo cai no index.html normal da SPA. O React Router continua funcionando igual — o arquivo novo só chega com as meta tags certas.

A imagem certa

Três regras para o og:image:

  1. Endereço absoluto, com https:// e domínio. Caminho relativo falha em metade dos robôs.
  2. 1200 × 630, em JPG ou PNG. É o tamanho que todo mundo recorta bem.
  3. Uma por página que vale a pena compartilhar. Para gerar em série, uso o Chrome que já está instalado, via puppeteer-core, montando um cartão em HTML e tirando o print — é o npm run og do zeno.dev.br.

Testar antes de mandar para o cliente

Primeiro, o que o robô vê:

curl -s https://seusite.com.br/produtos/x/ | grep -i "og:"

Depois, os inspetores oficiais, como o Post Inspector do LinkedIn. O WhatsApp guarda a prévia em cache por um tempo: para testar de novo depois de corrigir, mande o link com um parâmetro qualquer no fim, como ?v=2.

Este é o segundo passo da série — o primeiro é colocar o site no ar com domínio próprio.

Para revisar

Tente responder antes de abrir. Estas perguntas também entram na revisão espaçada .

? Por que a prévia de link de uma SPA mostra o título errado?

O robô que monta a prévia lê o HTML que o servidor entrega e não executa JavaScript. Numa SPA, esse HTML é o mesmo index.html para todas as rotas.

? Por que o og:image precisa ser um endereço absoluto?

Quem monta a prévia não resolve caminho relativo de forma confiável. Com https:// e o domínio, a imagem é sempre encontrada.

? Por que o plugin para o build quando não acha uma meta tag?

Porque publicar o site inteiro com a prévia errada é pior que um build vermelho. Se alguém mexer no index.html e apagar a tag, o erro aparece na hora.

// fontes e leitura

Este post faz parte da trilha Front-end para negócios pequenos . Lá estão o mapa do assunto e os próximos passos.