Toolz é executado no Next.js App Router em nove idiomas, e a configuração hreflang passou por três versões erradas antes de dar certo O primeiro colocou um módulo-escopo alternates objeto no layout, que não pode variar de acordo com a localidade, então cada página traduzida anunciou o URL em inglês como canônico. Esse é o bug que exclui traduções: informa ao Google /es/pricing é uma duplicata de /pricing e não deve ser indexado, o que desfaz silenciosamente o trabalho de tradução enquanto cada página ainda é renderizada corretamente.
A segunda versão corrigiu o canônico e anunciou cada local configurado como uma alternativa, incluindo locais cujas traduções ainda não haviam sido geradas. Portanto, o site publicou um cluster hreflang nomeando páginas que eram texto em inglês sob uma URL em espanhol. A terceira versão, a que está em execução agora, deriva o conjunto alternativo da cobertura real da tradução, por página.
Este guia percorre a configuração de trabalho: a configuração de localidade, o generateMetadata implementação, a variante do mapa do site e os três erros acima para que você possa ignorá-los Ele fica abaixo do guia completo de hreflang ao lado do Versão WordPress.
tl;dr: No App Router, o hreflang vem de
alternates.languagesvoltou porgenerateMetadata, e o canônico deve ser construído a partir do local ativo, em vez de declarado uma vez no escopo do módulo Construa ambos a partir de um mapa de localidade, emita o conjunto recíproco completo em cada página do grupo e incluax-default. Anuncie apenas locais cujo conteúdo realmente existe ou publique um cluster apontando para páginas não traduzidas. O gerador hreflang é útil para verificar a saída renderizada em relação a um conjunto de referência validado.
O que Next.js lhe dá da caixa?
A API de metadados suporta diretamente o hreflang. Retornando alternates.languages desde generateMetadata produz o <link rel="alternate" hreflang> tags na cabeça:
export async function generateMetadata({ params }) {
const { locale } = await params
return {
alternates: {
canonical: 'https://example.com/de/preise',
languages: {
'en-US': 'https://example.com/pricing',
'de-DE': 'https://example.com/de/preise',
'x-default': 'https://example.com/pricing',
},
},
}
}
Next.js os coloca na cabeça e lida com o x-default chave sem invólucro especial, como sua Referência da API de metadados documentos. O que ele não faz é decidir quais locais pertencem ao conjunto, manter o canônico em sincronia ou impedir que você coloque todo o objeto no escopo do módulo onde ele não pode variar Essas são as partes que você tem que acertar, e são as partes que quebram.
Observe também isso metadataBase afeta URLs relativos aqui Hreflang requer URLs absolutos, então qualquer um dos dois conjuntos metadataBase e use caminhos, ou construa URLs absolutos você mesmo Eu prefiro construí-los explicitamente a partir de uma origem configurada, porque uma implantação de visualização que emite URLs de produção é sua própria categoria de problema.
Passo 1: um mapa de localidade e apenas um
Tudo o que está a jusante lê a partir disso Um local precisa de três fatos: o segmento de rota, a tag BCP 47 para hreflang e <html lang>, e se está enviando atualmente.
// i18n/locales.ts
export const DEFAULT_LOCALE = 'en'
export const LOCALES = [
{ code: 'en', hreflang: 'en-US', label: 'English' },
{ code: 'de', hreflang: 'de-DE', label: 'Deutsch' },
{ code: 'fr', hreflang: 'fr-FR', label: 'Français' },
{ code: 'ja', hreflang: 'ja-JP', label: '日本語' },
]
/** '/pricing' -> '/de/pricing', and '/pricing' for the default locale. */
export function localize(path: string, locale: string): string {
return locale === DEFAULT_LOCALE ? path : `/${locale}${path}`
}
o code e o hreflang valor são campos deliberadamente separados O segmento de rota é de porque ninguém quer /de-DE/pricing em seus URLs, o valor hreflang é de-DE porque é isso que você quer anunciar Conflacioná-los significa ou URLs feios ou um bare de na anotação e no momento em que você adiciona pt-BR ao lado pt-PT a fusão para de funcionar.
Passo 2: um ajudante que constrói o conjunto
Uma função, usada por cada página, para que a forma não possa variar entre as rotas:
export function hreflangAlternates(path: string, locales: string[], base = SITE_URL) {
const eligible = LOCALES.filter((l) => locales.includes(l.code))
// A single-entry cluster is not a cluster: emit nothing rather than
// advertise a one-sided relationship.
if (eligible.length < 2) return []
const alts = eligible.map((l) => ({
hreflang: l.hreflang,
href: base + localize(path, l.code),
}))
alts.push({ hreflang: 'x-default', href: base + localize(path, DEFAULT_LOCALE) })
return alts
}
Duas decisões aí valem a pena roubar.
O retorno antecipado em menos de dois locais. Uma página que existe apenas em inglês não deve emitir nenhum hreflang Uma única anotação auto-referencial não está errada exatamente, mas é ruído, e a versão deste código que a emitiu fez com que cada página somente em inglês parecesse um cluster quebrado em relatórios de rastreamento.
x-default é anexado pelo ajudante, não pelo chamador. Cada implementação que EU vi que deixa o fallback para o chamador acaba com um modelo que esqueceu isso Dobre-o na coisa que constrói o conjunto e ele não pode ser esquecido.
Etapa 3: conecte-o ao gerateMetadata
// app/[locale]/pricing/page.tsx
import { localize, hreflangAlternates, SHIPPING_LOCALES } from '@/i18n/locales'
import { SITE_URL } from '@/lib/site'
export async function generateMetadata({ params }) {
const { locale } = await params
const path = '/pricing'
const alternates = hreflangAlternates(path, SHIPPING_LOCALES)
return {
alternates: {
// Built from the ACTIVE locale. This is the line that matters.
canonical: `${SITE_URL}${localize(path, locale)}`,
...(alternates.length
? { languages: Object.fromEntries(alternates.map((a) => [a.hreflang, a.href])) }
: {}),
},
}
}
O canônico é a linha para olhar. Deve ser uma função de locale0, o que significa que não pode viver numa constante de módulo-escopo, não pode viver na disposição raiz, e não pode ser partilhado através do segmento de localidade Cada página do grupo acaba com uma idêntica languages mapa e um canônico único para si mesmo, que é exatamente a forma hreflang e canônico exigir.
Como o mapa é idêntico em todo o grupo, a reciprocidade é satisfeita estruturalmente e não por disciplina A página alemã lista alemão, inglês, francês, japonês e x-padrão; o mesmo acontece com a página em inglês Não há lógica por página que possa omitir uma entrada.
Passo 4: anuncie apenas o que existe
Esta é a etapa que a maioria dos guias pula e foi a que produziu a pior versão da nossa configuração.
Se suas traduções são geradas de forma assíncrona, ou uma localidade é meio preenchida, a lista de localidades configurada e o conteúdo traduzido são conjuntos diferentes, anunciar a lista configurada significa publicar hreflang para páginas que são texto em inglês sob uma URL localizada O Google segue a anotação, encontra o inglês onde o alemão foi prometido e você fabricou conteúdo duplicado em nove localidades de uma só vez.
A correção é passar a cobertura real em vez da configuração:
// Coverage is per-page, not global: this page might be translated
// into three languages while the one next to it has none.
const locales = await localesForPage(path)
const alternates = hreflangAlternates(path, locales)
Emparelhe isso com robots: { index: false, follow: true } em páginas que existem em um local, mas ainda não têm cópia traduzida Eles permanecem acessíveis para qualquer um que chega de sua própria navegação, e eles ficam fora do índice até que a cópia pousa Ambos cair fora automaticamente uma vez que a cobertura preenche, sem acompanhamento implantar.
Passo 5: a variante do mapa do site
Se você preferir manter o hreflang fora dos cabeçalhos das páginas, o mesmo ajudante alimenta uma rota do mapa do site. Um <loc> por conteúdo com suplentes quando crianças, em vez de um <loc> por localidade:
// app/sitemap-pages.xml/route.ts
const entries = PAGES.map((path) => {
const alts = hreflangAlternates(path, SHIPPING_LOCALES)
const links = alts
.map((a) => `\n <xhtml:link rel="alternate" hreflang="${a.hreflang}" href="${a.href}"/>`)
.join('')
return ` <url>\n <loc>${SITE_URL}${path}</loc>${links}\n </url>`
})
const xml =
`<?xml version="1.0" encoding="UTF-8"?>\n` +
`<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" ` +
`xmlns:xhtml="http://www.w3.org/1999/xhtml">\n${entries.join('\n')}\n</urlset>`
o xmlns:xhtml declaração sobre <urlset> é obrigatório e fácil de esquecer; sem ele as entradas alternativas são ignoradas. Observe a estrutura: listando um <loc> per localidade multiplica o arquivo pela contagem de localidade e coloca cada tradução em competição com seu próprio canônico Mesma cobertura de descoberta, uma fração das entradas.
Escolha um método Se você emitir ambas as tags de cabeça e entradas de mapa do site para os mesmos URLs, você tem duas fontes que irão derivar Nossas são tags de cabeça para páginas e alternativas de mapa do site para o catálogo de ferramentas, que são conjuntos disjuntos.
A localidade padrão obtém um prefixo de URL?
Esta é uma decisão de roteamento que altera cada URL em seu conjunto hreflang, portanto, faça-o antes de escrever o auxiliar e não depois.
Duas estratégias são comuns. Prefixação conforme necessário atende a localidade padrão sem prefixo em /pricing e tudo o resto em /de/pricing. Sempre prefixando atende todos os locais em um segmento, inclusive /en/pricing, com /pricing redirecionando. O middleware Next.js suporta ambos e o next-intl o expõe como um localePrefix configuração.
Conforme necessário (/pricing, /de/pricing) |
Sempre (/en/pricing, /de/pricing) |
|
|---|---|---|
| URLs em inglês existentes | Preservado | Cada um se torna um redirecionamento |
| hreflang para a localidade padrão | Pontos no URL não prefixado | Pontos em /en/... |
| alvo x-default | A raiz não prefixada, que também é a en URL |
Deve escolher um local prefixado |
| Simetria em código | localize() precisa de uma ramificação de localidade padrão |
Sem filial; cada local é uniforme |
Eu uso o prefixo conforme necessário em um site existente, porque reescrever cada URL em inglês indexado para ganhar simetria é um grande custo para uma pequena vitória de arrumação, e redirecionamentos em suas páginas de maior tráfego não são gratuitos Em uma construção greenfield, sempre-prefixing é mais limpo: o localize helper perde seu caso especial e nunca há dúvidas sobre se /pricing e /en/pricing são a mesma página.
O que importa para o hreflang de qualquer maneira é a consistência A URL na anotação, a URL no canônico e a URL que serve um 200 sem redirecionar devem ser a mesma string A única combinação que quebra de forma confiável é sempre prefixando com o hreflang ainda apontando para as URLs não prefixadas: cada entrada para a localidade padrão nomeia um redirecionamento e a tag de retorno vive no destino e não na URL que você nomeou.
Qual biblioteca você deve usar?
A maioria das configurações do App Router i18 n acaba ligada next-intl ou next-i18next& n? gera hreflang para você Eles resolvem roteamento e carregamento de mensagens; as anotações ainda são suas para emitir Isso é bom, porque o ajudante acima é vinte linhas e você quer sob seu próprio controle de qualquer maneira.
| Preocupação | Manipulado pela biblioteca | Seu |
|---|---|---|
| Roteamento local e middleware | sim | |
| Catálogos de mensagens e fallback | sim | |
<html lang> |
geralmente | Verifique se corresponde ao valor hreflang |
alternates.canonical por localidade |
não | Construir a partir do local ativo |
alternates.languages |
não | Construir a partir do mapa de localidade |
x-default |
não | Anexe no ajudante |
A única coisa específica da biblioteca a ser verificada é <html lang>. Dados estruturados e hreflang que contradizem a linguagem do documento são piores que nenhum e um layout que codifica lang="en" enquanto serve alemão é uma sobra surpreendentemente comum.
Como você verifica isso?
Construa e leia a saída real, porque o modelo não é a evidência:
next build && next start
curl -s http://localhost:3000/de/preise | grep -E 'rel="(canonical|alternate)"'
Três verificações nessa saída O canônico é o URL que você buscou Há exatamente uma entrada por localidade mais uma x-default. Cada href é absoluto e corresponde ao URL em que a página é servida, incluindo a barra final.
Em seguida, busque um irmão e diff os dois languages blocos. Eles devem ser byte-idênticos; apenas o canônico difere Se eles não são idênticos, algo em sua página está construindo o conjunto a partir do local atual, em vez de do grupo, que é a falha de reciprocidade em seu disfarce mais comum.
Finalmente, cole o conjunto no gerador de etiquetas Hreflang para validar os códigos em si Ele verifica cada valor contra a forma ISO, sinaliza subtags não reconhecidas, captura duplicatas e avisa quando um conjunto não tem fallback Ele executa client-side, de modo que uma estrutura de URL de preparação permanece privada guia ferramenta caminha pelo fluxo de trabalho e 12 erros comuns de hreflang cobre o que procurar quando a marcação estiver ativa.
Perguntas frequentes
Como adiciono tags hreflang no roteador de aplicativos Next.js?
Devolver um alternates.languages objeto de generateMetadata, mapeando cada tag BCP 47 para seu URL absoluto e definir alternates.canonical do local ativo. Next.js renderiza ambos na cabeça.
Por que o canônico deve ser construído dentro do gerageMetadata?
Porque tem que variar por localidade Um canônico módulo-escopo não pode, então cada página traduzida iria declarar o URL padrão-local como canônico, o que remove a tradução do índice.
Next-intl gera tags hreflang automaticamente?
No. next-intl lida com roteamento e carregamento de mensagens As anotações hreflang e o canônico por local ainda são seus para emitir generateMetadata.
Como adiciono x-default em Next.js?
Adicionar um 'x-default' chave para o alternates.languages objeto apontando para o seu URL de fallback Next.js passa a chave através sem tratamento especial Anexe-o dentro do ajudante que constrói o conjunto para que nenhum modelo possa esquecê-lo.
O hreflang deve ir na cabeça ou no mapa do site em Next.js?
Qualquer um funciona, mas não ambos para os mesmos URLs Tags de cabeça via generateMetadata são mais fáceis de depurar Uma rota de mapa do site se adapta a catálogos grandes e mantém os cabeçalhos das páginas enxutos; lembre-se do xmlns:xhtml declaração sobre <urlset>.
E se um local for traduzido apenas parcialmente?
Anuncie apenas os locais cujo conteúdo existe genuinamente para essa página e marque as páginas não traduzidas noindex, follow. Anunciar a lista de localidades configurada publica um cluster apontando para páginas não traduzidas.
Os URLs hreflang em Next.js precisam ser absolutos?
Sim. Hreflang requer URLs totalmente qualificados. Qualquer um dos conjuntos metadataBase ou crie os URLs a partir de uma origem configurada para que as implantações de visualização não emitam URLs de produção.
Como verifico a saída hreflang renderizada?
Execute uma compilação de produção, busque duas páginas irmãs e compare. O languages os blocos devem ser idênticos a bytes em todo o grupo, enquanto cada canônico aponta para sua própria URL.



