Executando verificação de segurança...
3

Como gerei imagens OG dinâmicas no Next.js

Há alguns dias eu publiquei aqui um pitch do https://bloodlinkbr.vercel.app — uma plataforma que conecta doadores de sangue a campanhas abertas em hospitais. Uma das funcionalidades que mais gostei de implementar foi relativamente simples, mas com resultado visual imediato: cada campanha gera automaticamente uma imagem de compartilhamento (OG image) com os dados reais daquela campanha.

Quando alguém copia o link de uma campanha e cola no WhatsApp ou no Twitter, aparece uma imagem com o nome da campanha, o hospital, os tipos sanguíneos aceitos e uma barra de progresso mostrando quantos doadores já se inscreveram. Não é uma imagem estática — é gerada na hora, com os dados daquela campanha específica.


Como funciona

O Next.js tem uma API chamada ImageResponse, disponível via next/og. Ela recebe JSX e devolve uma imagem PNG. Por baixo dos panos, ela usa o https://github.com/vercel/satori, que converte um subconjunto de HTML/CSS em SVG e depois renderiza o PNG.

O setup é uma Route Handler simples:

// app/api/og/route.tsx
import { ImageResponse } from 'next/og';

export const runtime = 'edge';

export async function GET(req: Request) {
const { searchParams } = new URL(req.url);
const title = searchParams.get('title') ?? 'Campanha de doação';
// ...

return new ImageResponse(
  <div style={{ /* JSX inline styles */ }}>
    {title}
  </div>,
  { width: 1200, height: 630 },
);

}

O runtime = 'edge' é importante — a ImageResponse roda na Edge Runtime do Vercel, o que significa latência menor e sem cold start perceptível.


Montando a URL dinamicamente

No generateMetadata de cada página de campanha, monto a URL com os parâmetros da campanha:

const ogUrl = new URL('/api/og', 'https://bloodlinkbr.vercel.app');
ogUrl.searchParams.set('title', campaign.title);
ogUrl.searchParams.set('types', campaign.acceptedBloodTypes.join(','));
ogUrl.searchParams.set('urgency', campaign.urgency);
ogUrl.searchParams.set('hospital', campaign.hospitalName);
ogUrl.searchParams.set('city', campaign.city);
ogUrl.searchParams.set('pct', String(progressPercent));
// ...

Essa URL vai direto no openGraph.images e no twitter.images dos metadados da página.


Dois formatos: landscape e quadrado

O Twitter/WhatsApp esperam 1200×630, mas o Instagram funciona melhor com imagem quadrada (1080×1080). Adicionei um parâmetro ?square=1 que muda o layout: o texto fica
maior, os elementos se reorganizam verticalmente e há mais respiro.

No botão de compartilhamento da campanha, quando o usuário clica em "baixar para Instagram", a requisição já vai com square=1.


Um detalhe chato do Satori

O Satori não suporta todo CSS — flex funciona bem, mas propriedades como gap precisam de atenção, e display: grid não existe. Todo elemento precisa ter display: flex explícito, mesmo que seja só um . Descobri isso na força bruta: se um texto não aparece na imagem, provavelmente é porque o elemento pai não tem display: flex.


Resultado

O arquivo inteiro tem menos de 370 linhas incluindo os dois formatos de card. Não tem dependência extra além do que já vem com o Next.js. E o resultado é bom o suficiente para que valha a pena — uma campanha compartilhada no WhatsApp com imagem chama muito mais atenção do que um link sem preview.

Se quiser ver funcionando, qualquer campanha em https://bloodlinkbr.vercel.app gera sua própria imagem — dá pra ver diretamente acessando
/api/og?title=Teste&types=A%2B,O-&urgency=critical&hospital=Hospital+X&city=São+Paulo&state=SP&pct=60&target=10&applied=6.

Carregando publicação patrocinada...