Pular para o conteúdo
Development

Dois bugs de i18n no Astro que só aparecem quando você compara o HTML renderizado

Por Victor Da Luz•
astroi18nseodev-logsite

O imperfectsystems.com está ganhando um idioma espanhol, deliberadamente como piloto: três posts aqui contra cento e sessenta e seis no vdaluz.com, então qualquer erro de i18n é descoberto primeiro no site menor. Adicionar i18n: { defaultLocale: 'en', locales: ['en', 'es'], routing: { fallbackType: 'rewrite' } } à configuração do Astro e rebuildar foi a parte fácil. Isso gerou automaticamente versões /es/ funcionais da homepage, da página 404, de toda página estática, sem nenhuma linha extra de código de rota. Fiquei desconfiado de como isso foi fácil. E tinha razão para estar.

O fallback não sabe o que é uma tradução

Os posts do blog vivem numa content collection, src/content/blog/en/hello-dev-log.md e por aí vai, com as traduções em espanhol chegando depois em es/ através de um pipeline separado. Minha rota em inglês filtra a collection só para entradas en/ e chama getStaticPaths(). Presumi que o fallback do Astro faria o mesmo truque que fez para a homepage: pedir /es/blog/hello-dev-log, receber o conteúdo em inglês de volta, pronto.

Isso acontece, mas o mecanismo me surpreendeu. O rewrite automático de fallback funciona no nível da URL: ele pega a requisição, remove o prefixo de idioma, e resolve de novo qualquer rota que bata com o caminho puro. Para uma página estática isso é exatamente certo. Para uma rota com getStaticPaths() lendo uma collection dividida por idioma, isso significa que /es/blog/<slug> vai sempre renderizar o que a rota em inglês produziu, para sempre, mesmo depois que uma tradução real em espanhol existir em es/<slug>.md. O fallback não tem a menor ideia de que uma tradução é uma coisa que existe. Ele só sabe “nenhuma rota bateu, tenta a rota do idioma padrão”.

Então construí um /es/blog/[...slug].astro explícito que faz sua própria busca: prefere a entrada em es/, cai para en/ se ela ainda não existir. O mesmo para o índice paginado. São talvez quarenta linhas extras, mas é o único jeito de uma tradução futura realmente aparecer, em vez de ficar silenciosamente encoberta por um fallback de rota que já “resolveu” a URL.

const byLocale = (loc) =>
  new Map(all.filter((p) => p.id.startsWith(`${loc}/`)).map((p) => [stripPrefix(p.id), p]));

const localized = byLocale(locale);
const fallback = byLocale('en');
const slugs = new Set([...fallback.keys(), ...localized.keys()]);
return [...slugs].map((slug) => localized.get(slug) ?? fallback.get(slug));

Nota de rodapé pequena que me custou um ciclo de debug: os ids da content collection também recebem o prefixo de idioma (en/hello-dev-log), e um componente de card compartilhado estava construindo as URLs dos posts direto a partir de post.id. O primeiro build produziu /blog/en/hello-dev-log. Removo o prefixo antes de passar as entradas para qualquer coisa que construa uma URL, mas mantenho a entrada original, com prefixo, para a chamada render() do Astro, já que essa busca é indexada pelo id real e um clone sem prefixo não resolve.

A tag canonical que mente sobre onde a página está

Esse aqui não apareceu no navegador de jeito nenhum. O site parecia completamente normal nos dois idiomas. astro check passou. O build teve sucesso. Só peguei o bug porque tinha anotado “páginas es/ se autocanonicalizam” como um requisito explícito e decidi de fato verificar isso em vez de confiar na página bonita:

grep -o '<link rel="canonical"[^>]*>' dist/client/es/index.html dist/client/index.html

Os dois arquivos apontavam para a mesma URL. A em inglês.

O bug: numa página servida pelo rewrite automático de fallback, a homepage sob /es/, por exemplo, Astro.url.pathname reflete a rota que de fato executou, que é a rota da homepage em inglês, não a URL que o navegador pediu. Astro.currentLocale acerta isso; ele diz corretamente 'es' na mesma renderização exata. Só o Astro.url está mentindo, e só nas páginas renderizadas por fallback, as rotas de blog que eu tinha construído explicitamente não tinham esse problema, porque ali o caminho pedido e a rota que bateu são a mesma coisa.

Construir a tag canonical a partir de new URL(Astro.url.pathname, Astro.site), que era o que o código já fazia antes de eu mexer nele, significava que toda página em espanhol renderizada por fallback se autocanonicalizava de volta para o inglês. O Google teria ficado com duas URLs, as duas insistindo que a em inglês era a real. Não quebrado, só errado silenciosamente, e errado exatamente no único lugar que ninguém olha, porque a página renderizada não dá nenhum sinal visual de que algo está fora do lugar.

A correção contorna o Astro.url por completo. getAbsoluteLocaleUrl(locale, barePath), de astro:i18n, reconstrói a URL a partir do idioma que o Astro de fato resolveu mais um caminho puro sem prefixo, e não importa se a página atual chegou aqui por uma rota explícita ou por um rewrite de fallback:

const bareLocalePath = Astro.url.pathname.replace(/^\/es(\/|$)/, '/') || '/';
const canonicalURL = getAbsoluteLocaleUrl(locale, bareLocalePath);

O que eu diria para o eu do passado

“Simplesmente funcionou” no primeiro build era o sinal de alerta, não o alívio. O fallback de i18n do Astro é bem projetado para o caso para o qual foi construído, páginas estáticas, sem variação de conteúdo por URL, e para de ser a ferramenta certa silenciosamente no momento em que uma rota passa a ter sua própria ideia do que pertence a uma URL. O bug do canonical é a lição mais afiada: uma verificação que só olha se a página renderiza nunca vai pegar um campo de metadado apontando para a URL errada. Só encontrei isso tratando “páginas es/ se autocanonicalizam” como algo para dar grep, não como algo para checar de olho.

Leitura relacionada

Você também pode achar útil

AdGuard

AdGuard para iOS

Bloqueio de anúncios e rastreadores em todo o sistema no iOS, sem necessidade de um servidor DNS separado.

Como afiliado da AdGuard, ganho com compras qualificadas.

Saiba mais
Proton

Proton VPN

VPN comercial com filtragem NetShield e interruptor de desligamento automático.

Como parceiro da Proton, ganho com compras qualificadas dos serviços de privacidade e segurança da Proton (Pass, Mail, VPN, Drive).

Saiba mais
Proton

Proton Mail

E-mail criptografado de ponta a ponta, com arquitetura de acesso zero.

Como parceiro da Proton, ganho com compras qualificadas dos serviços de privacidade e segurança da Proton (Pass, Mail, VPN, Drive).

Saiba mais