Pular para o conteúdo
Cursos

DevClub

LógicaFront-endBack-endMobile

IA Club

IA na prática
Estudar programaçãoEstudar IA
LiçãoIntermediáriocódigo testado

Open Graph no HTML: o preview do link no WhatsApp

O que o Google e o WhatsApp leem do seu HTML: title, meta description, hierarquia de títulos, o cartão do link compartilhado e os ícones da aba.

Rodolfo Mori14 min de leitura

Open Graph é um punhado de meta tags no <head> que diz ao WhatsApp, ao LinkedIn e ao Discord qual título, qual texto e qual imagem mostrar quando alguém cola o seu link numa conversa. Sem elas, cada aplicativo adivinha — e adivinha pelo pedaço errado da página.

Todos os exemplos aqui saem de uma página só: a do clube de leitura da Livraria Cardume, uma livraria de bairro que divulga a roda de leitura no grupo do WhatsApp dos clientes. A mesma página aparece em três versões, e cada versão gera um cartão diferente.

O cartão de visita que o robô busca

Quando alguém compartilha a Livraria Cardume, o WhatsApp não abre a página e “olha” para ela como uma pessoa. Ele manda um robô buscar o endereço e procura um cartão de visita já preenchido: título, resumo e imagem. Se o cartão não existe, tenta montar um com qualquer informação que encontrar.

Esse cartão é formado pelas propriedades Open Graph no <head>. Elas não mudam o conteúdo visível; orientam o crawler que constrói o preview. Compare as três versões da mesma página e anote qual meta tag mudou antes de olhar o cartão resultante. Essa relação de causa e efeito é a microprática que separa a tag necessária de uma lista copiada sem entender.

Antes de escrever qualquer tag, vale olhar o outro lado. Coloquei as três versões num servidor local que anota cada pedido e quem fez:

js
import { createServer } from 'node:http';
import { readFile } from 'node:fs/promises';
import { extname } from 'node:path';

const TIPOS = { '.html': 'text/html; charset=utf-8', '.png': 'image/png', '.svg': 'image/svg+xml' };

createServer(async (req, res) => {
  const ua = req.headers['user-agent'] || '';
  const robo = ua.match(/WhatsApp|LinkedInBot|Discordbot|Googlebot/i)?.[0] || 'navegador';
  const arquivo = '.' + new URL(req.url, 'http://x').pathname;
  try {
    const corpo = await readFile(arquivo);
    // a v4 é servida de propósito sem charset no cabeçalho — o teste vem adiante
    const tipo = arquivo.includes('sem-charset') ? 'text/html' : TIPOS[extname(arquivo)];
    res.writeHead(200, { 'Content-Type': tipo, 'Content-Length': corpo.length });
    res.end(corpo);
    console.log(`200 ${req.url}  ${corpo.length} bytes  <- ${robo}`);
  } catch {
    res.writeHead(404, { 'Content-Type': 'text/plain' });
    res.end('nao encontrado');
    console.log(`404 ${req.url}  0 bytes  <- ${robo}`);
  }
}).listen(4123);

O bloco do catch não é enfeite: o log de 404 é metade do que este artigo tem para mostrar. Com o servidor no ar, pedi a página e a imagem com o User-Agent de cada aplicativo:

bash
for ua in 'WhatsApp/2.2412.4 N' \
          'LinkedInBot/1.0 (compatible; Mozilla/5.0)' \
          'Mozilla/5.0 (compatible; Discordbot/2.0)'; do
  curl -s -o /dev/null -A "$ua" http://127.0.0.1:4123/v3.html
  curl -s -o /dev/null -A "$ua" http://127.0.0.1:4123/capa-clube-1200x630.png
done
200 /v3.html 1677 bytes <- WhatsApp 200 /capa-clube-1200x630.png 3633 bytes <- WhatsApp 200 /v3.html 1677 bytes <- LinkedInBot 200 /capa-clube-1200x630.png 3633 bytes <- LinkedInBot 200 /v3.html 1677 bytes <- Discordbot 200 /capa-clube-1200x630.png 3633 bytes <- Discordbot

Quem disparou esses pedidos fui eu, com o curl vestindo o User-Agent de cada aplicativo — o log acima é a visão do servidor, não uma captura do robô do WhatsApp em campo. O que ele deixa claro é a forma da visita, e são duas coisas para guardar.

Um cartão custa dois pedidos, não um: primeiro o HTML, depois o arquivo apontado pelo og:image, em conexões separadas. Por isso a imagem precisa estar pública e responder 200 sozinha — se ela der 404, o cartão sai sem imagem mesmo com todas as tags certas no <head>.

E o robô de preview pega o HTML que veio pela rede e vai embora. Ele não executa o seu JavaScript — diferente do Googlebot, que renderiza a página. Meta tag inserida por document.head.append() depois que a página carregou não existe para o WhatsApp: para ele, o <head> é o que o servidor mandou.

Três versões da mesma página, três cartões

Escrevi um leitor de cartão: ele baixa o HTML, lê só o <head>, aplica a cascata de fallback e tenta baixar a imagem. Não é o robô do WhatsApp — é a mesma sequência de decisões, num programa que dá para abrir e ler linha a linha.

js
import { JSDOM } from 'jsdom';

const alvo = process.argv[2];
const html = await (await fetch(alvo)).text();
const { document } = new JSDOM(html).window;

const meta = (chave) =>
  document.querySelector(`meta[property="${chave}"]`)?.content ||
  document.querySelector(`meta[name="${chave}"]`)?.content ||
  null;

const titulo = meta('og:title') || meta('twitter:title') || document.title || null;
const descricao = meta('og:description') || meta('twitter:description') || meta('description') || null;
const imagem = meta('og:image') || meta('twitter:image') || null;

A cascata é a parte que ninguém decora e que explica tudo: og:title ganha do <title>, e o <title> só é usado quando não existe og:title. Vale o mesmo para a descrição.

Falta a imagem, que é o único campo em que o leitor precisa sair da página e baixar um arquivo. Esta é a segunda metade do mesmo arquivo:

js
let situacaoImagem = 'sem og:image — cartão sai só com texto';
if (imagem && !/^https?:/.test(imagem)) {
  situacaoImagem = `"${imagem}" é caminho relativo — robô não resolve, cartão sai sem imagem`;
} else if (imagem) {
  const resposta = await fetch(imagem);
  const bytes = Buffer.from(await resposta.arrayBuffer());
  const largura = bytes.readUInt32BE(16); // IHDR do PNG: largura e altura
  const altura = bytes.readUInt32BE(20);
  situacaoImagem = `${largura}x${altura} (${(largura / altura).toFixed(2)}:1), ${bytes.length} bytes`;
}

console.log(`--- ${alvo}`);
console.log(`titulo:    ${titulo ?? '(nenhum)'}`);
console.log(`descricao: ${descricao ?? '(nenhuma)'}`);
console.log(`imagem:    ${situacaoImagem}`);
console.log(`lang:      ${document.documentElement.lang || '(nao declarado)'}`);
console.log(`h1:        ${document.querySelectorAll('h1').length} na pagina`);
console.log(`auditoria: titulo ${titulo?.length ?? 0} chars, descricao ${descricao?.length ?? 0} chars`);

Rodando o leitor nas três versões:

bash
for v in v1 v2 v3; do node previa.mjs http://127.0.0.1:4123/$v.html; echo; done
--- http://127.0.0.1:4123/v1.html titulo: Clube de leitura descricao: (nenhuma) imagem: sem og:image — cartão sai só com texto lang: (nao declarado) h1: 1 na pagina auditoria: titulo 16 chars, descricao 0 chars

http://127.0.0.1:4123/v2.html titulo: Clube de leitura de agosto | Livraria Cardume descricao: Roda de leitura gratuita na Livraria Cardume, toda última quinta do mês, às 19h, com edições comentadas e vagas limitadas. imagem: “capa-clube-400x209.png” é caminho relativo — robô não resolve, cartão sai sem imagem lang: pt-BR h1: 1 na pagina auditoria: titulo 45 chars, descricao 122 chars

http://127.0.0.1:4123/v3.html titulo: Clube de leitura de agosto na Livraria Cardume descricao: Última quinta do mês, 19h, entrada gratuita. Trinta lugares, edições comentadas e café por conta da casa. imagem: 1200x630 (1.90:1), 3633 bytes lang: pt-BR h1: 1 na pagina auditoria: titulo 46 chars, descricao 105 chars

versão o que o cartão mostra tamanho do HTML
v1 — só <title> o título de 16 caracteres e o endereço cru 258 bytes
v2 — title, description e og:image relativa texto certo, nenhuma imagem 552 bytes
v3 — Open Graph completo título próprio, chamada própria, imagem grande 1677 bytes

O <head> completo custou 1,4 KB a mais que o mínimo. É o metadado mais barato do site inteiro.

title: a aba, o favorito e o resultado de busca

O <title> faz três trabalhos ao mesmo tempo: nomeia a aba, nomeia o item salvo nos favoritos e vira o link azul do resultado de busca. Por isso ele começa pelo assunto e termina pela marca, nunca o contrário.

html
<title>Clube de leitura</title>

<title>Clube de leitura de agosto | Livraria Cardume</title>

Todo mundo repete “no máximo 60 caracteres”. O número é um atalho ruim, porque o corte acontece por largura em pixels, não por contagem de letras. Medi quatro títulos num Chromium de verdade, com uma aproximação da tipografia do resultado no desktop — 20px Arial, cortando em 600px de coluna útil:

js
import { chromium } from 'playwright-core';

const titulos = [
  'Clube de leitura de agosto | Livraria Cardume',
  'Clube de leitura mensal e oficina de escrita | Livraria Cardume',
  'MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM',
  'lilili lilili lilili lilili lilili lilili lilili lilili lilili',
];

const navegador = await chromium.launch({ channel: 'chromium' });
const pagina = await navegador.newPage();

const medidas = await pagina.evaluate((lista) => {
  const ctx = document.createElement('canvas').getContext('2d');
  ctx.font = '20px Arial, sans-serif';
  return lista.map((t) => [t.length, Math.round(ctx.measureText(t).width)]);
}, titulos);

for (const [i, [chars, px]] of medidas.entries()) {
  const corta = px > 600 ? 'CORTA' : 'cabe '; // 600px: a largura útil da coluna
  console.log(`${String(chars).padStart(2)} chars  ${String(px).padStart(3)} px  ${corta}  ${titulos[i]}`);
}

await navegador.close();
45 chars 403 px cabe Clube de leitura de agosto | Livraria Cardume 63 chars 552 px cabe Clube de leitura mensal e oficina de escrita | Livraria Cardume 59 chars 883 px CORTA MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM 62 chars 284 px cabe lilili lilili lilili lilili lilili lilili lilili lilili lilili

Um título de 63 caracteres coube com folga; outro de 59 estourou. A conta de letras erra porque a letra não tem largura fixa: na mesma medição, o M deu 16,7 px e o l deu 4,4 px — quase quatro vezes. As duas últimas linhas são títulos artificiais, feitos só para esticar a diferença; o que elas provam é que contar caractere não diz nada sobre o espaço ocupado. Escreva olhando o título renderizado, não a régua.

O acento que vira ç

Se o <head> não declara a codificação, todo acento do título vira lixo. Salvei esta página em UTF-8, sem <meta charset>:

html
<!doctype html>
<html lang="pt-BR">
  <head>
    <title>Edições comentadas | Livraria Cardume</title>
  </head>
  <body><h1>Edições comentadas</h1></body>
</html>

O servidor lá de cima manda esse arquivo com Content-Type: text/html seco, sem charset — a única página em que ele faz isso. Para ler o que sobra na aba, sete linhas de Chromium:

js
import { chromium } from 'playwright-core';

const navegador = await chromium.launch({ channel: 'chromium' });
const pagina = await navegador.newPage();
await pagina.goto(process.argv[2], { waitUntil: 'networkidle' });
console.log('aba mostra:', await pagina.title());
await navegador.close();
bash
curl -sI http://127.0.0.1:4123/v4-sem-charset.html | grep -i content-type
node navegador.mjs http://127.0.0.1:4123/v4-sem-charset.html
Content-Type: text/html aba mostra: Edições comentadas | Livraria Cardume

Sem declaração nenhuma, o Chromium não adivinha UTF-8: ele cai na codificação herdada do idioma (windows-1252) e lê cada byte como um caractere. O ç, que em UTF-8 ocupa dois bytes, aparece como os dois caracteres que aqueles bytes valem naquela tabela: ç.

O robô de preview cai no mesmo buraco: ele também recebe uma pilha de bytes e precisa descobrir em que tabela lê-los, e o og:title sai com o mesmo lixo que apareceu na aba. A correção é uma linha, e ela vem antes de tudo no <head> — o assunto está inteiro em meta tags no HTML e no erro the character encoding of the HTML document was not declared.

meta description não decide ranking, decide o clique

A meta description não é fator de posição há muitos anos. O que ela decide é outra coisa: se a pessoa que já viu o seu link vai clicar nele.

html
<meta
  name="description"
  content="Roda de leitura gratuita na Livraria Cardume, toda última quinta do mês, às 19h, com edições comentadas e vagas limitadas."
/>

Repare no conteúdo: dia, horário, preço e limite de vagas. Uma descrição que serve para qualquer página da livraria (“a melhor livraria da cidade, com atendimento de qualidade”) não ajuda ninguém a decidir.

Entre 110 e 155 caracteres é a faixa que costuma aparecer inteira. Acima disso o Google corta, e às vezes reescreve o trecho inteiro com um pedaço do seu texto que ele achou mais relevante para a busca feita — o que é uma boa notícia disfarçada: se ele reescreve muito, a página responde mais do que a descrição promete.

Um h1 e uma hierarquia que se lê sozinha

O <title> é o nome da página para quem está fora dela. O <h1> é o nome para quem já está dentro. Podem ser diferentes, e normalmente são: o título carrega a marca, o h1 não precisa.

html
<title>Clube de leitura de agosto | Livraria Cardume</title>

<main>
  <h1>Clube de leitura de agosto</h1>
  <h2>Como participar</h2>
  <h3>Reserva pelo WhatsApp</h3>
  <h2>O livro do mês</h2>
</main>

A regra prática: se você ler só os títulos, de cima para baixo, precisa entender do que a página trata. Um h3 logo depois de um h1, sem h2 no meio, é um degrau faltando na escada — quem navega por leitor de tela sente isso na hora. A mecânica de h1 a h6 está em títulos e parágrafos em HTML.

lang, canonical e a página que mora em três endereços

lang="pt-BR" no <html> avisa o idioma. É o que faz o corretor ortográfico, o tradutor automático e o leitor de tela escolherem português — sem ele, um leitor de tela pode ler “clube de leitura” com pronúncia inglesa.

O canonical resolve outro problema. A mesma página do clube pode ser aberta por endereços diferentes: com parâmetro de campanha, com âncora, com e sem www. Para o robô, três endereços são três páginas — a não ser que a página diga qual é a verdadeira.

html
<link rel="canonical" href="https://livrariacardume.com.br/clube-de-leitura" />
<meta property="og:url" content="https://livrariacardume.com.br/clube-de-leitura" />

Pedi a mesma página nos três endereços e li o que ela declara:

js
import { JSDOM } from 'jsdom';

const base = 'http://127.0.0.1:4123/v3.html';

for (const endereco of [base, `${base}?utm_source=whatsapp&utm_campaign=clube`, `${base}#programacao`]) {
  const { document } = new JSDOM(await (await fetch(endereco)).text()).window;
  console.log(`pedido:    ${endereco}`);
  console.log(`canonical: ${document.querySelector('link[rel="canonical"]')?.href}`);
  console.log(`og:url:    ${document.querySelector('meta[property="og:url"]')?.content}\n`);
}
pedido: http://127.0.0.1:4123/v3.html canonical: http://127.0.0.1:4123/v3.html og:url: http://127.0.0.1:4123/v3.html

pedido: http://127.0.0.1:4123/v3.html?utm_source=whatsapp&amp;utm_campaign=clube canonical: http://127.0.0.1:4123/v3.html og:url: http://127.0.0.1:4123/v3.html

pedido: http://127.0.0.1:4123/v3.html#programacao canonical: http://127.0.0.1:4123/v3.html og:url: http://127.0.0.1:4123/v3.html

Os três pedidos chegaram por URLs diferentes e os três apontaram para o mesmo endereço canônico. É assim que os cliques vindos da campanha do WhatsApp somam com os cliques vindos da busca, em vez de virarem três páginas concorrentes.

As cinco tags de Open Graph que montam o cartão

Open Graph usa property, e não name — é a diferença que mais faz gente perder uma tarde. (Repare que o meu leitor lá de cima procura nos dois atributos: ele é tolerante de propósito. O robô do aplicativo não é obrigado a ser, e a especificação pede property.) As cinco tags que montam o cartão:

html
<meta property="og:type" content="article" />
<meta property="og:url" content="https://livrariacardume.com.br/clube-de-leitura" />
<meta property="og:title" content="Clube de leitura de agosto na Livraria Cardume" />
<meta
  property="og:description"
  content="Última quinta do mês, 19h, entrada gratuita. Trinta lugares, edições comentadas e café por conta da casa."
/>
<meta property="og:image" content="https://livrariacardume.com.br/og/clube-agosto.png" />

Mais três que valem o esforço:

html
<meta property="og:site_name" content="Livraria Cardume" />
<meta property="og:locale" content="pt_BR" />
<meta property="og:image:alt" content="Cadeiras em círculo no mezanino da Livraria Cardume" />

O og:title não precisa ser igual ao <title>, e é bom que não seja. No resultado de busca, a pessoa está procurando; num grupo de WhatsApp, ela não estava procurando nada. O texto do cartão pode ser mais direto e mais convite — compare os dois na saída da v3 lá em cima.

og:type sai de uma lista fechada, publicada em ogp.me: website, article, book, profile e as famílias music.* e video.* (music.album, video.movie). product, que aparece em muito tutorial, não está nessa lista — é tipo de catálogo do Facebook, não do protocolo. Na prática o cartão muda pouco: use website na home e article no resto.

og:image: o caminho relativo que apaga a imagem

Este é o erro campeão, e ele é silencioso: a página não quebra, o console não reclama, o cartão simplesmente sai sem imagem.

html
<meta property="og:image" content="capa-clube-400x209.png" />
imagem: "capa-clube-400x209.png" é caminho relativo — robô não resolve, cartão sai sem imagem

Dentro do navegador um caminho relativo funciona: o navegador resolve contra a URL da página. Com o robô de preview a história é outra. A especificação do Open Graph trata og:image como uma URL e usa o endereço completo em todos os exemplos, mas não define o que acontece com caminho relativo — e o que a especificação não define cada aplicativo resolve do seu jeito. O meu leitor recusa, que é justamente o cenário que você não quer descobrir com o link já compartilhado. Escreva o endereço inteiro, com protocolo:

html
<meta property="og:image" content="https://livrariacardume.com.br/og/clube-agosto.png" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />

Com o caminho absoluto, o leitor baixou a imagem e mediu:

imagem: 1200x630 (1.90:1), 3633 bytes

1200×630 dá 1,90:1, como o próprio leitor mediu — é o arredondamento prático da proporção 1,91:1 que a documentação do Facebook recomenda para o cartão grande. A mesma documentação põe 600 pixels de largura como piso: abaixo disso o aplicativo desiste do cartão grande e cai na miniatura quadrada ao lado do texto, o mesmo link ocupando uma fração da área na tela.

Declarar width e height também tem efeito prático, e aqui a documentação do Facebook é explícita: com as duas declaradas, o crawler renderiza a imagem na hora, sem precisar baixar e processar o arquivo antes. Já o og:image:alt descreve a imagem para quem usa leitor de tela, exatamente como o alt de uma imagem comum no HTML.

twitter:card: o que restou dele

O X ainda lê as tags twitter:, mas elas viraram, na prática, um complemento de uma linha. Todo o resto ele pega do Open Graph.

html
<meta name="twitter:card" content="summary_large_image" />

Repare no name, e não property — as tags do X seguem a convenção antiga. summary_large_image pede o cartão grande; summary pede o pequeno. Só escreva twitter:title ou twitter:image se você quiser um texto diferente do Open Graph naquela rede. Duplicar tudo é manutenção dobrada sem ganho nenhum.

Favicon: o pedido que o navegador faz sozinho

Aqui tem uma pegadinha que dá para ver com os próprios olhos. Abri a v1 — a versão sem nenhum <link rel="icon"> — com o mesmo abridor de sete linhas da seção do charset:

bash
node navegador.mjs http://127.0.0.1:4123/v1.html

E fui ler o terminal do servidor, que anotou dois pedidos:

200 /v1.html 258 bytes <- navegador 404 /favicon.ico 0 bytes <- navegador

O navegador pediu /favicon.ico por conta própria, mesmo sem ninguém ter escrito isso no HTML. É comportamento herdado dos anos 1990, e continua vivo.

Declarando os ícones, o pedido muda. Servi a v3, que declara três, e o Chromium buscou o SVG primeiro:

html
<link rel="icon" href="/favicon.ico" sizes="32x32" />
<link rel="icon" href="/icone.svg" type="image/svg+xml" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
200 /v3.html 1677 bytes <- navegador 200 /icone.svg 185 bytes <- navegador

Um pedido só. Quando eu apaguei o icone.svg do disco e repeti o teste, o mesmo Chromium foi atrás do .ico:

200 /v3.html 1677 bytes <- navegador 404 /icone.svg 0 bytes <- navegador 404 /favicon.ico 0 bytes <- navegador

Ou seja: com o SVG declarado, esse Chromium nem tentou o .ico — só voltou a ele, e ao palpite de /favicon.ico, quando o SVG sumiu. O .ico é a rede de segurança para o navegador que ainda não entende SVG. Com esses três arquivos — mais um site.webmanifest se o site vira atalho na tela inicial do celular — você cobre navegador, aba fixada e ícone de iPhone.

O que não existe: meta keywords e as outras lendas

Sobrou muito folclore de 2005 circulando em tutorial. Vale saber o que pode sair do seu <head> sem dó:

tag situação o que fazer
<meta name="keywords"> ignorada pelos buscadores há mais de uma década apagar
<meta name="robots" content="index, follow"> é o padrão; escrever não muda nada apagar
<meta name="revisit-after"> nenhum buscador relevante jamais deu suporte apagar
<meta name="author"> não é sinal de ranking manter só se você usa
<meta name="robots" content="noindex"> funciona mesmo usar em página de teste e obrigado-pelo-pedido

O noindex é o único da lista com efeito real, e é o que mais gente esquece de tirar depois — página de produção nascida de um clone de ambiente de teste costuma vir com ele colado no <head>. Se um site sumiu da busca do nada, procure essa linha antes de procurar qualquer outra coisa.

O head inteiro, para copiar

Juntando tudo, este é o <head> da página do clube de leitura:

html
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />

  <title>Clube de leitura de agosto | Livraria Cardume</title>
  <meta
    name="description"
    content="Roda de leitura gratuita na Livraria Cardume, toda última quinta do mês, às 19h, com edições comentadas e vagas limitadas."
  />
  <link rel="canonical" href="https://livrariacardume.com.br/clube-de-leitura" />

  <meta property="og:type" content="article" />
  <meta property="og:site_name" content="Livraria Cardume" />
  <meta property="og:locale" content="pt_BR" />
  <meta property="og:url" content="https://livrariacardume.com.br/clube-de-leitura" />
  <meta property="og:title" content="Clube de leitura de agosto na Livraria Cardume" />
  <meta
    property="og:description"
    content="Última quinta do mês, 19h, entrada gratuita. Trinta lugares, edições comentadas e café por conta da casa."
  />
  <meta property="og:image" content="https://livrariacardume.com.br/og/clube-agosto.png" />
  <meta property="og:image:width" content="1200" />
  <meta property="og:image:height" content="630" />
  <meta property="og:image:alt" content="Cadeiras em círculo no mezanino da Livraria Cardume" />

  <meta name="twitter:card" content="summary_large_image" />

  <link rel="icon" href="/favicon.ico" sizes="32x32" />
  <link rel="icon" href="/icone.svg" type="image/svg+xml" />
  <link rel="apple-touch-icon" href="/apple-touch-icon.png" />
</head>

O que vem depois

Copie esse <head> para a sua página, suba num endereço público e cole o link numa conversa consigo mesmo no WhatsApp. É o teste mais rápido que existe: ou o cartão aparece com imagem grande, ou você descobre em dez segundos qual das tags está errada.

Depois disso, o próximo assunto da trilha de HTML é guardar dado no próprio elemento com data attributes — o caminho por onde o JavaScript conversa com o seu markup. E o guia completo de HTML mostra onde cada peça entra na ordem de estudo.

  • html
  • seo
  • open graph
  • favicon
  • meta description

Perguntas frequentes

Corrigi as meta tags e o WhatsApp continua mostrando o cartão antigo. Por quê?
Porque o cartão fica em cache no servidor do aplicativo, não no seu celular. O Facebook e o LinkedIn têm ferramentas oficiais que forçam uma nova leitura da URL (Sharing Debugger e Post Inspector). O WhatsApp não tem: o jeito prático é compartilhar a página com um parâmetro novo na URL, como ?v=2, até o cache expirar.
Preciso de Open Graph em toda página do site?
Em toda página que alguém tem motivo para compartilhar: produto, artigo, evento, página de contato. E cada uma com o próprio og:title, og:description e og:image. Repetir o mesmo cartão do home em cem páginas é pior do que não ter cartão nenhum, porque todos os links do site viram o mesmo link.
Dá para gerar a imagem do cartão automaticamente?
Dá, e é o que a maioria dos blogs faz: um script no build desenha um PNG 1200x630 com o título do post e salva em /og/{slug}.png. O cuidado é com o tamanho da letra — a imagem costuma ser exibida com 300px de largura no celular, então texto abaixo de 40px no arquivo original vira borrão.
Open Graph melhora o ranking no Google?
Não. As tags og: existem para os aplicativos de mensagem e redes sociais. O Google pode usar og:title como uma das fontes para escrever o título do resultado quando acha o seu title ruim, mas isso é apresentação, não posição. Quem move ranking é o conteúdo da página.

Dúvidas e comentários

Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.

Todo o código deste artigo foi executado em Node 24.16.0, Chromium 151 e curl 8.7.1 no macOS, e as saídas exibidas são as reais — como produzimos este conteúdo.

Fontes consultadas

  1. The Open Graph protocol — ogp.me
  2. MDN — O elemento meta — developer.mozilla.org
  3. Google Search Central — Links de título nos resultados da Pesquisa — developers.google.com
  4. Google Search Central — Meta tags que o Google entende — developers.google.com
  5. Meta for Developers — Sharing on the web (webmasters) — developers.facebook.com

Continue por aqui