Skip to content

Crescimento/SEO: produção anuncia canonical, sitemap e og:image em coderacer.app (503) — todo link compartilhado sai sem preview (site.ts:8) #117

Description

@caioross

Contexto

Toda a identidade pública do site é derivada de SITE.url (src/lib/site.ts:8):

const RAW_URL = process.env.NEXT_PUBLIC_SITE_URL || "https://coderacer.app";

NEXT_PUBLIC_SITE_URL não está setada no build de produção, então o fallback literal vale — e ele
aponta para um domínio que não é o que serve o site. Conferido na produção real agora (2026-08-01):

Onde O que a produção publica hoje
GET /robots.txt Host: https://coderacer.app · Sitemap: https://coderacer.app/sitemap.xml
GET /sitemap.xml <loc>https://coderacer.app</loc> e <loc>https://coderacer.app/leaderboard</loc>
GET / (HTML) <link rel="canonical" href="https://coderacer.app"/> · og:url · og:image idem
GET /room/ABC123 herda og:image = https://coderacer.app/opengraph-image?...

E o destino não responde:

curl -sIL https://coderacer.app/opengraph-image   ->  503   (redireciona para www.coderacer.app)
curl -sI  https://code-racer-three.vercel.app/opengraph-image  ->  200  image/png  153 KB

Ou seja: o asset de OG existe e funciona — só a URL anunciada está errada.

Por que isso é P1 de crescimento (não cosmético)

  1. Todo link compartilhado sai sem imagem. O og:image aponta para um host 503, então WhatsApp,
    Discord, Slack, X e Telegram renderizam o card sem preview. Isso atinge exatamente os dois motores
    de crescimento que a frota já construiu: o botão "Compartilhar" do Resultado (Crescimento: tela de Resultado não tem "compartilhar" — transformar cada corrida terminada em convite (loop viral) #22) e o convite de
    sala (copyLink em src/components/Lobby.tsx:60-65, e o chip "copiar link" em
    src/components/RoomView.tsx:265-269). O convite é o loop de aquisição nº1 de um jogo sem login —
    hoje ele chega no chat do amigo como um link cru.
  2. O canonical entrega o SEO para outro domínio. <link rel="canonical" href="https://coderacer.app">
    instrui o buscador a atribuir o conteúdo a uma origem que não serve o site. Somado ao sitemap.xml
    com <loc> do domínio errado e ao Host: do robots.txt, o resultado prático é que a produção
    real não acumula indexação — todo o trabalho de SEO já feito (metadata, OG/Twitter cards, JSON-LD,
    next/font self-hosted) não converte em nada.
  3. O JSON-LD tem o mesmo defeito (src/app/layout.tsx:82, :103-104).

Escopo

  • src/lib/site.ts:8 — o fallback não pode ser um domínio arbitrário. Derive a origem real do deploy
    quando NEXT_PUBLIC_SITE_URL não estiver setada, e deixe o literal só como último recurso de dev.
  • .env.example:26 e README.md (linhas ~109, ~295, ~407) — hoje ensinam https://coderacer.app
    como valor de produção; devem ensinar "a URL real do seu deploy".
  • Cobrir com teste o contrato de SITE.url (normalização e precedência do fallback).

Não decida o domínio nesta issue. Se o dono pretende usar coderacer.app no futuro, isso continua
funcionando: basta setar a env ou apontar o domínio na Vercel. O que esta issue conserta é o site
anunciar como canônica uma origem que ele não serve. Se o Conselho entender que escolher o domínio
público é chamada do dono (§7.1), aplique decisao-dono — mas o fallback quebrado é bug em qualquer
cenário.

Acceptance criteria (verificáveis)

  • Sem NEXT_PUBLIC_SITE_URL no ambiente, SITE.url resolve para a origem real do deploy — e não
    para um domínio hardcoded que o deploy não serve.
  • Com NEXT_PUBLIC_SITE_URL setada, ela continua tendo precedência (comportamento atual preservado),
    com a normalização de barra final mantida (src/lib/site.ts:14).
  • Teste novo (vitest, no padrão de src/lib/share.test.ts) cobrindo: env setada, env ausente com
    origem do deploy disponível, env ausente sem nada disponível (fallback de dev), e trailing slash.
  • Após o deploy: GET /robots.txt, GET /sitemap.xml, o <link rel="canonical"> e o og:image da
    home e de /room/<code> apresentam a origem que efetivamente serve o site. Cole os 4 curl no PR.
  • og:image da produção retorna 200 image/png quando buscada exatamente como publicada no HTML.
  • .env.example e o README não sugerem mais um domínio concreto de terceiro como valor de produção.
  • Gate: pnpm typecheck · pnpm build · pnpm test verdes.

Dica de abordagem

Na Vercel, o build expõe a origem do deploy em variáveis próprias (a de produção do projeto e a do
deploy corrente). Uma cadeia NEXT_PUBLIC_SITE_URL → origem de produção do provedor → origem do deploy
→ literal de dev resolve o caso sem exigir nenhuma ação no painel. Cuidado: src/lib/site.ts é
importado por src/lib/share.ts, que roda no cliente — variável sem prefixo NEXT_PUBLIC_ vira
undefined no bundle do browser e pode divergir do valor do servidor. Como share.ts só usa SITE.url
como default quando não há window (src/lib/share.ts:18), o risco prático é baixo, mas confirme que
nada renderizado no cliente passa a divergir do HTML do servidor (hydration). Se preferir cortar o
problema pela raiz, mantenha a resolução restrita aos arquivos de servidor (layout, robots,
sitemap, manifest, opengraph-image) e deixe share.ts explicitamente com o default de dev.

Relacionada (não é duplicata): #84 acrescenta /practice ao sitemap.ts — este aqui conserta a
origem de todas as entradas, inclusive as que a #84 vai adicionar.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1Alto valor — próximo da filaarea:docsREADME, docs, handbookarea:infraCI/CD, build, tooling, deploy, deps

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions