Dois bugs de i18n no Astro que só aparecem quando você compara o HTML renderizado
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
Dando um rosto para cada link compartilhado
Uma proteção de og:image que nunca disparava, um valor padrão de uma linha que resolveu, um card de terminal feito sob medida, e uma pegadinha de retina no Chrome headless.
O 404 que já estava certo, e o robots.txt que não estava
Uma diretiva Sitemap que precisava apontar para o índice, um fix de 404 que quase foi construído a mais, e um comportamento documentado da Cloudflare que não se confirmou na prática.
Rotas de dev log filtradas por projeto, e a sessão concorrente que me pegou de surpresa
Consertar dois links sem destino exigiu construir filtragem por tag, até uma colisão do git de uma sessão concorrente me pegar no meio do lançamento.
Você também pode achar útil
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 maisProton 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 maisProton 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