Devlog · Fase 0 · 7/7
O SEO multilíngue de aubia.dev: URLs, hreflang e JSON-LD
Os seis idiomas de aubia.dev têm URLs e links hreflang próprios. O HTML servido também liga os artigos ao autor, sem garantir indexação nem citações por IA.
- seo
- i18n
- geo
- inertia

Uma visita a /fr/blog/multilingual-seo-geo deve retornar este artigo em francês, mesmo que o navegador prefira inglês. O cookie de idioma também não deve alterar o conteúdo dessa URL. Para compartilhar uma tradução ou permitir sua indexação, preciso que o endereço sempre identifique o mesmo idioma.
As páginas públicas de aubia.dev usam seis prefixos: /fr, /en, /es, /de, /it e /pt. Os slugs permanecem em inglês em todas as versões. O prefixo basta para diferenciá-las.
A renderização no servidor do Inertia fornece o conteúdo e os metadados na resposta HTML inicial. Um robô de indexação pode lê-los sem executar o JavaScript da página. Isso depende do funcionamento do SSR: em caso de falha, a configuração do site permite recorrer à renderização no cliente. O conteúdo passa então a depender do JavaScript, como explica em detalhe o artigo sobre essa stack.
O idioma da URL e o do navegador
O middleware SetLocale escolhe o idioma antes da renderização. Ele consulta o segmento da URL, depois o cookie de preferência, o cabeçalho Accept-Language do navegador e, por fim, a configuração da aplicação. Usa o primeiro valor reconhecido.
Em uma página sob /fr, o segmento da URL sempre tem precedência. Em /, não há segmento de idioma: o cookie, as preferências do navegador e o idioma padrão, inglês, determinam o destino do redirecionamento.
Para ler Accept-Language, o servidor percorre os idiomas em ordem de preferência e compara suas duas primeiras letras com os seis idiomas aceitos. Sem um cookie que tenha precedência, um navegador que solicita pt-BR pode, portanto, ser redirecionado para /pt.
A raiz responde com um redirecionamento 302. Um 301 indicaria uma mudança permanente, enquanto o destino depende do visitante e de sua preferência naquele momento.
Uma resposta que varia conforme a requisição
Um cache compartilhado não deve reutilizar um redirecionamento para francês para um visitante que solicita italiano. O cabeçalho Vary indica quais cabeçalhos da requisição diferenciam as respostas.
O Inertia define Vary: X-Inertia para separar suas respostas HTML e JSON. Seu middleware substitui esse cabeçalho, então o SetLocale acrescenta os valores relacionados ao idioma após o retorno desse middleware, apenas na rota de redirecionamento:
if ($request->routeIs('home.redirect')) {
$response->headers->set('Vary', ['Cookie', 'Accept-Language'], replace: false);
}
A resposta passa a incluir X-Inertia, Cookie e Accept-Language. As páginas localizadas mantêm o Vary do Inertia, sem acréscimos para a negociação de idioma: a URL já determina o idioma.
Esse mecanismo depende do cache que recebe a resposta. O Cloudflare não considera todos os valores de Vary por padrão. Enviar o cabeçalho, por si só, não prova que a CDN diferencia essas variantes; suas regras de cache precisam ser compatíveis com as respostas servidas.
Hreflang liga as traduções
Uma URL distinta para cada idioma torna cada versão acessível separadamente. A anotação hreflang indica aos mecanismos de busca quais páginas são traduções umas das outras.
Cada versão lista sua própria URL e as dos outros idiomas disponíveis. O Google exige esses links recíprocos: se duas páginas não fizerem referência uma à outra, as anotações desse par podem ser ignoradas. Os pares que têm referências recíprocas ainda podem ser processados.
Uso apenas códigos de idioma: fr, en, es, de, it e pt. O site oferece uma versão em português destinada aos falantes do idioma, não conteúdos separados para Portugal e Brasil. pt descreve essa escolha; pt-PT indicaria conteúdo em português destinado a Portugal.
A anotação informa ao mecanismo de busca qual é o público pretendido. O redirecionamento HTTP da raiz escolhe o destino de uma visita com base na requisição recebida.
Uma versão alternativa
A anotação x-default identifica uma versão alternativa para idiomas ou regiões não contemplados. Em aubia.dev, ela aponta para o inglês. Para um artigo cuja tradução em inglês não está publicada, ela é omitida: uma URL que retorna 404 não seria um destino alternativo útil.
As anotações do blog se limitam às traduções publicadas do mesmo slug. Um artigo disponível em três idiomas lista essas três versões, mesmo que o restante do site aceite seis.
O componente SeoHead escreve esses links no <head>. O sitemap também os gera a partir dos idiomas publicados. Os dois métodos são equivalentes para o Google; usar ambos não traz benefício adicional de posicionamento nos resultados de busca. Manter os dois exige, sobretudo, que seus destinos continuem consistentes.
Uma URL canônica para cada idioma
O link canonical responde a outra pergunta: qual URL deve ser preferida entre páginas idênticas ou muito semelhantes? Uma tradução completa não é uma duplicata do original só porque aborda o mesmo assunto em outro idioma.
Cada versão de um artigo declara, portanto, sua própria URL canônica. A página em francês não designa a página em inglês como canônica. O Google recomenda um destino no mesmo idioma, quando existe, e mantém a decisão final sobre qual URL selecionar.
As páginas de confirmação e de cancelamento da inscrição usam noindex,nofollow. O site não adiciona links canonical nem hreflang a elas e as exclui do sitemap. Assim, as anotações de versões por idioma ficam restritas às páginas destinadas à indexação.
O Open Graph usa outro formato: og:locale é, por exemplo, fr_FR. A especificação Open Graph define language_TERRITORY para essa propriedade opcional. Esse formato baseado em território não se aplica à definição de público pelo hreflang.
Links de idioma disponíveis antes do JavaScript
O seletor de idioma contém links reais para as outras versões da página. O Google recomenda esses links junto com as anotações hreflang para que os visitantes possam escolher por conta própria.
O componente usa os elementos HTML nativos details e summary. As âncoras são renderizadas mesmo quando o painel está fechado. Com SSR, elas já estão no HTML recebido, sem depender de um clique para serem adicionadas ao documento.
O navegador gerencia a abertura com o mouse ou o teclado. Um efeito React adiciona o fechamento com Esc e com um clique fora do painel. Um menu que só monta seu conteúdo ao ser aberto não forneceria esses links na renderização inicial; essa limitação depende do comportamento do componente, não apenas da presença de um portal React.
Os destinos vêm do servidor. Em um artigo, eles se restringem às traduções publicadas, assim como o hreflang. O seletor não oferece um idioma em que o artigo não existe.
Um autor identificado no grafo JSON-LD
O componente SeoHead descreve três entidades comuns em JSON-LD: a organização Aubia, minha pessoa e o site. O JSON-LD usa aqui o vocabulário do schema.org para nomear tipos e relações.
Cada entidade tem um @id estável. As referências a esses identificadores declaram uma relação sem repetir o objeto inteiro. O grafo inclui as ligações a seguir, apresentadas em um trecho limitado a tipos, identificadores e relações:
{
"@context": "https://schema.org",
"@graph": [
{
"@type": "Organization",
"@id": "https://aubia.dev/#organization",
"founder": { "@id": "https://aubia.dev/#founder" }
},
{
"@type": "Person",
"@id": "https://aubia.dev/#founder",
"worksFor": { "@id": "https://aubia.dev/#organization" }
},
{
"@type": "WebSite",
"@id": "https://aubia.dev/#website",
"publisher": { "@id": "https://aubia.dev/#organization" }
}
]
}
As páginas adicionam seus dados por meio de additionalJsonLd. A página inicial fornece um FAQPage com as perguntas e respostas visíveis. Um artigo adiciona um BlogPosting e um BreadcrumbList, sua trilha de navegação.
No BlogPosting, author faz referência a #founder e publisher a #organization. O autor do artigo é, portanto, declarado como a mesma pessoa que fundou o site. Adicionar um objeto a @graph não cria, por si só, uma relação com todos os outros objetos: são as propriedades e suas referências que descrevem essas relações.
A assinatura visível de cada artigo aponta para o mesmo perfil público do nó Person. Os metadados Open Graph declaram o tipo article e suas datas de publicação e alteração.
O grafo não declara SoftwareApplication nem uma oferta de pré-venda: o site oferece uma lista de espera, não um download do software. Não são inventadas resenhas ou avaliações para obter um resultado enriquecido. As perguntas frequentes visíveis mantêm a marcação, identificada pela URL da página localizada, mas o Google não exibe resultados enriquecidos de FAQ desde 7 de maio de 2026. Essas declarações não garantem seu uso por um mecanismo de busca nem a indexação.
O GEO, sem garantia de citação
O termo GEO, de Generative Engine Optimization, designa práticas destinadas a melhorar a visibilidade do conteúdo nas respostas geradas por IA. Não é um protocolo comum que assegure que uma página será lida, selecionada ou citada.
Para aparecer como link de fonte no AI Overviews ou no AI Mode do Google Search, uma página precisa estar indexada pelo Google e apta a ser exibida com um snippet. Nenhuma marcação schema.org especial é necessária. Mesmo uma página que atende a essas condições não tem garantia de aparecer como fonte.
Procuro escrever seções compreensíveis sem que seja necessário reler o artigo inteiro. Isso ajuda os leitores a acompanhar uma explicação e citar um trecho, sem garantir que uma IA o reproduza fielmente.
Um arquivo llms.txt para manter
O site também serve um arquivo llms.txt. Ele contém uma apresentação do produto em Markdown e links para páginas públicas, sem uma interface para percorrer. Baseia-se na proposta llms.txt, que é distinta das regras de rastreamento do robots.txt.
Esse arquivo é escrito separadamente do site e dos artigos. Suas afirmações e seus links precisam, portanto, ser verificados quando mudam em outros lugares. Sua presença não prova que um mecanismo o lê nem que melhora o posicionamento ou as citações do site.
A publicação das traduções
Um artigo do blog corresponde a um arquivo Markdown por idioma. Sua data de publicação é lida do front matter, em UTC. O build compila o Markdown e o realce Phiki em um catálogo local obrigatório em produção, incluindo os artigos futuros. As datas são verificadas a cada leitura: antes da data, a URL retorna 404; quando ela chega, o artigo fica acessível sem um novo deploy. Um catálogo ausente, inválido ou desatualizado produz uma resposta 503 nas rotas que dependem dele, sem executar Phiki durante a requisição.
O índice o lista, e o feed RSS publica seu resumo com a data de publicação. O sitemap usa a data de atualização, ou a data de publicação se a primeira não for informada, para lastmod. Ele adiciona as anotações hreflang das traduções disponíveis. A própria página fornece o BlogPosting e a trilha de navegação.
O feed RSS e o sitemap são servidos sem sessões nem cookies e indicam um prazo de cache HTTP de uma hora. Sua atualização em um cache intermediário ou leitor de feeds pode ocorrer depois que o artigo fica disponível na aplicação. O lastmod de cada índice do blog corresponde à data de alteração ou publicação mais recente de seus artigos publicados. As páginas estáticas não declaram uma data desconhecida. O <head> das páginas do blog também inclui um link para descobrir o feed RSS localizado.
Os textos da interface seguem outro processo
Os textos da interface são armazenados nos arquivos lang/<locale>.json. O francês é a fonte, com o inglês como pivô para espanhol, alemão, italiano e português.
Quando uma alteração no arquivo francês é commitada, o hook envia ao agente de tradução as chaves adicionadas ou modificadas que não estão excluídas. Ele mescla os valores recebidos no arquivo inglês e preserva os demais. As exclusões de chaves são aplicadas separadamente. A propagação do inglês para os outros quatro idiomas é iniciada por um comando manual.
Três prefixos são excluídos das adições e alterações automáticas: usp.*, features.tagline.* e letter.body.*. Os dois primeiros não têm chaves nos dicionários atuais. O terceiro protege o corpo da Carta do fundador. O claim e o título do banner da página inicial não fazem parte dessas exclusões, por isso podem ser traduzidos automaticamente.
O servidor carrega as traduções do idioma solicitado e preenche as chaves ausentes com inglês. O hook React useT() lê então os valores compartilhados nas props Inertia. Os artigos não passam por esse script: seus arquivos Markdown são traduzidos separadamente.
O corpo da Carta existe apenas nos dicionários francês e inglês. Em espanhol, alemão, italiano e português, seus parágrafos aparecem, portanto, em inglês, embora os títulos e rótulos ao redor estejam traduzidos. Seu contêiner declara lang="en": essa anotação identifica o idioma sem traduzir os parágrafos.
Esta construção é contada aqui, artigo após artigo. O que vem a seguir depende do que você disser sobre ela.
Entrar na lista de espera