Toolz は Next.js App Router 上で 9 つの言語で動作し、hreflang のセットアップは正しく実行される前に 3 つの間違ったバージョンを経ました。最初のものはモジュール スコープを置きます alternates レイアウト内のオブジェクトはロケールによって変更できないため、翻訳されたすべてのページが正規の英語 URL として発表されました。それが翻訳を削除するバグです。それは Google に伝えます /es/pricing の複製です /pricing また、インデックスが付けられてはいけません。インデックスを付けると、すべてのページが正しくレンダリングされている間、静かに翻訳作業が元に戻されます。
2 番目のバージョンでは、正規のものを修正し、翻訳がまだ生成されていないロケールを含む、構成されたすべてのロケールを代替として宣伝しました。そのため、このサイトは、スペイン語の URL の下に英語のテキストであるページに名前を付けるフレフラング クラスターを公開しました。現在実行されている 3 番目のバージョンでは、ページごとに実際の翻訳範囲から代替セットが導出されます。
このガイドでは、作業セットアップ (ロケール構成) について説明します generateMetadata 実装、サイトマップのバリアント、および上記の3 つの間違いなので、スキップできます。 の下に座っています 完全なフレフラングガイド の 沿い WordPressバージョン。
tl;dr: App Router では、hreflang は から来ます
alternates.languagesによって返却されましたgenerateMetadataと定義され、カノニカル はモジュールスコープで一度宣言するのではなく、アクティブなロケールからビルドする必要があります。 build both from one locale map, emit the full reciprocal set on every page in the group, and includex-default。 content が本物に存在するロケールのみを宣伝するか、未翻訳のページを指すクラスターを公開します。 the フレフラング発生器 レンダリングされた出力を検証された参照セットと照合するのに役立ちます。
Next.jsは箱から出して何をくれますか?
メタデータ API は hreflang を直接サポートします。 returning alternates.languages から generateMetadata を生成します <link rel="alternate" hreflang> 頭の中のタグ:
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 はそれらをヘッドにレンダリングし、を処理します x-default 特別なケーシングのないキー メタデータ API リファレンス ドキュメント.それがしないことは,セット内のどのロケールが属するかを決めるか,カノニカルなものを同期させるか,あるいはオブジェクト全体をモジュールスコープで変化できないところに置くのを止めるか,です.これらは正しくしなければならない部分であり,それらは壊れる部分です.
それにも注意してください metadataBase ここで相対URLに影響します。 hreflangは絶対URLを必要とするので、どちらかを設定します metadataBase そしてパスを使うか、絶対 URL を自分でビルドします。 production URL を発するプレビューデプロイはそれ自身の問題のカテゴリーであるため、私は構成されたオリジンから明示的にビルドすることを好みます。
ステップ 1: 1 つのロケール マップ、および 1 つだけ
下流にあるものはすべてここから読み取られます。 ロケールには 3 つの事実が必要です。ルート セグメント、フレフラングの BCP 47 タグ、および <html lang>、そして現在出荷中かどうか。
// 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}`
}
ザ・ code そして、 hreflang 値は意図的に別々のフィールドです。 route セグメントは de 誰も望んでいないからです /de-DE/pricing URL では、hreflang 値は次のとおりです de-DE それが発表したいことだからです 混同するということは醜いURLか むき出しのURLのどちらかです de 注釈で、そしてあなたが追加する瞬間 pt-BR 並んで pt-PT 融合はまったく機能しなくなります。
ステップ 2: セットを構築するヘルパー
1 つの関数、すべてのページで使用されているため、形状はルート間をドリフトできません:
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
}
そこにある 2 つの決定は盗む価値があります。
2 つ未満のロケールでの早期復帰。 英語のみで存在するページは、フレフランをまったく発しないはずです。単一の自己参照注釈は正確には間違っていませんが、ノイズであり、それを発行したこのコードのバージョンにより、英語のみのページはすべて、クロール レポートで壊れたクラスタのように見えます。
x-default は呼び出し元ではなくヘルパーによって付加されます。 私が見たすべての実装は、呼び出し元にフォールバックを残すため、最終的にそれを忘れた1 つのテンプレートになります。 setを構築するものにそれを折り、それを忘れることはできません。
ステップ 3: generateMetadata にワイヤーで接続します
// 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])) }
: {}),
},
}
}
カノニカルとは、凝視する線である。 の関数でなければならない localeつまり、モジュールスコープ定数には住めず、ルートレイアウトにも住めず、ロケールセグメント全体で共有することもできません。 グループ内のすべてのページが同一になります languages マップとそれ自体固有の正準、まさにその形状です フレフランと正典 要求.
マップはグループ全体で同一であるため、相互主義は分野別ではなく構造的に満たされます。 ドイツ語のページには、ドイツ語、英語、フランス語、日本語、および x-default がリストされています。英語のページにも、エントリを省略できるページごとのロジックはありません。
ステップ 4: 存在するもののみを宣伝します
これは、ほとんどのガイドがスキップするステップであり、セットアップの最悪バージョンを生み出したステップです。
翻訳が非同期で生成された場合、またはロケールが半分に満たされている場合、設定されたロケールリストと翻訳されたコンテンツは異なるセットになります 設定したリストを宣伝するということは、ローカライズされたURLの下で英語のテキストであるページのフレフラングを公表することを意味します Googleは注釈に従い、ドイツ語が約束されていた英語を見つけ、一度に9 つのロケールにまたがる重複コンテンツを製造しました。
修正は、コンフィグではなく実際のカバレッジを渡すことです:
// 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)
それとペアリングしてください robots: { index: false, follow: true } ロケールに存在するが、まだ翻訳されたコピーがないページ。自分のナビゲーションから到着した人は誰でもアクセスでき、コピーが着地するまでインデックスから離れます。カバレッジが埋まると、両方とも自動的に削除され、フォローアップのデプロイはありません。
ステップ 5: サイトマップのバリアント
もしあなたがむしろページの頭からhreflangを保ちたいならば、同じヘルパーがサイトマップのルートをフィードします。 1つ <loc> 1 人ではなく、子供の頃の交代がいるコンテンツごとに <loc> ロケールごと:
// 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>`
ザ・ xmlns:xhtml 宣言オン <urlset> は必須で忘れやすい; それなしでは代替エントリは無視されます。 structure: listing one に注意してください <loc> ロケールごとにファイルにロケール数を掛け、すべての翻訳を独自の標準との競合に置きます。同じ発見範囲、エントリの一部。
1 つの方法を選択します。同じ URL に対してヘッド タグとサイトマップ エントリの両方を送信すると、ドリフトするソースが 2 つあります。私たちのものは、ページのヘッド タグとツール カタログのサイトマップの代替であり、これらは互いに素なセットです。
デフォルトのロケールには URL プレフィックスが付きますか?
これは、hreflang セット内のすべての URL を変更するルーティングの決定であるため、ヘルパーを作成した後ではなく、作成する前に実行してください。
2 つの戦略が一般的です. 必要に応じて接頭辞を付けます で接頭辞のないデフォルトのロケールを提供します /pricing そして他のすべて /de/pricing。 常に接頭辞を付けます を含む、セグメント内のすべてのロケールにサービスを提供します /en/pricing、 と /pricing リダイレクトする。 next.jsミドルウェアは両方をサポートしており、next-intlはそれをaとして公開している localePrefix 設定.
必要に応じて()/pricing、 /de/pricing) |
常に ()/en/pricing、 /de/pricing) |
|
|---|---|---|
| 既存の英語url | 保存されています | 1つ1 つがリダイレクトになる |
| デフォルトのロケールの Hreflang | 接頭辞のない URL のポイント | ポイント アット /en/... |
| x-default target | 接頭辞のない語根。これもです en url |
接頭辞付きロケールを 1 つ選択する必要があります |
| コードにおける対称性 | localize() デフォルトのロケールのブランチが必要です |
枝はありません;すべてのロケールは均一です |
シンメトリーを得るためにインデックス付き英語 URL をすべて書き換えるのは、小さな整理整頓の勝利には多額の費用がかかり、トラフィックの多いページでのリダイレクトは無料ではないため、既存のサイトで必要に応じてプレフィックスを使用します。 greenfield ビルドでは、常にプレフィックスがクリーンになります localize ヘルパーは特別なケースを失い、どうかについては決して疑問の余地はありません /pricing あんど /en/pricing は 同一ページ.
どちらの方法でも hreflang にとって重要なのは一貫性です。 アノテーションの URL、カノニカル内の URL、およびリダイレクトせずに 200 を提供する URL は同じ文字列でなければなりません。確実に壊れる 1 つの組み合わせは、常に接頭辞を付け、hreflang が接頭辞のない URL を指し続けることです。デフォルトのロケールのすべてのエントリはリダイレクトに名前を付け、リターン タグは名前を付けた URL ではなく宛先に住んでいます。
どのライブラリを使うべきですか?
ほとんどのApp Router i18nセットアップは最終的にオンになります next-intl や next-i18next、そしてどちらもあなたのためにhreflangを生成しません。 they solve routing and message loading; the annotations are still yours to emit.それは問題ありません、なぜなら上記のヘルパーは20 行で、とにかく自分の制御下にしたいからです。
| 懸念 | 図書館 扱う | あなたの |
|---|---|---|
| ロケールルーティングとミドルウェア | はいって | |
| メッセージカタログとフォールバック | はいって | |
<html lang> |
通常 | それが hreflang の値と一致することを確認します |
alternates.canonical ロケールごと |
いやー | アクティブなロケールからビルドします |
alternates.languages |
いやー | ロケールマップからビルドします |
x-default |
いやー | ヘルパーに追加します |
ライブラリ固有の確認すべき点は 1 つあります <html lang>。 文書言語と矛盾する構造化データとフレフラングは、何もないよりも悪く、ハードコーディングするレイアウトである lang="en" ドイツ語での奉仕は驚くほど一般的な残り物です。
どうやって検証するの?
テンプレートは証拠ではないため、実際の出力を構築して読みます:
next build && next start
curl -s http://localhost:3000/de/preise | grep -E 'rel="(canonical|alternate)"'
その出力に対する3 つのチェック 正準はフェッチしたURLです ロケールごとに正確に1 つのエントリに1 つを加えたものがあります x-default。 エブリィ href は絶対値であり、末尾のスラッシュを含むページが提供される URL と一致します。
次に、兄弟を連れてきて、その 2 人を区別します languages ブロック. byte-identical; only the canonical differences.同一でない場合、ページ内の何かがグループからではなく現在のロケールからセットを構築しています。これは、最も一般的な偽装における相互主義の失敗です。
最後に、セットを に貼り付けます フレフラングタグジェネレータ コード自体を検証するため ISO 形状に対して各値をチェックし、認識されないサブタグにフラグを立て、重複をキャッチし、セットにフォールバックがない場合に警告します クライアント側で実行されるため、ステージング URL 構造は非公開のままです ツールガイド ワークフローを歩き、そして 12 の一般的なフレフラング エラー マークアップがライブになったら何を探すべきかをカバーします。
よくある質問
Next.js App Routerでhreflangタグを追加するにはどうすればよいですか?
返す an alternates.languages からのobject generateMetadata、各 BCP 47 タグをその絶対 URL にマッピングし、設定します alternates.canonical アクティブなロケールから。 next.js は両方をヘッドにレンダリングします。
なぜ generateMetadata 内に canonical をビルドしなければならないのですか?
ロケールごとに変更する必要があるためです。 module-scope 正準では変更できないため、翻訳されたすべてのページがデフォルトのロケール URL を正規として宣言し、インデックスから翻訳が削除されます。
next-intl は hreflang タグを自動的に生成しますか?
No. next-intl はルーティングとメッセージの読み込みを処理します。 hreflang アノテーションとロケールごとのカノニカル は、まだあなたが発するべきものです generateMetadata。
Next.jsでx-defaultを追加するにはどうすればよいですか?
追加する an 'x-default' の 鍵 alternates.languages フォールバック URL を指すオブジェクト。 next.js は特別な処理を行わずにキーを通過します。テンプレートがキーを忘れることができないように、セットを構築するヘルパー内にキーを追加します。
Hreflangは頭の中に入るべきですか、それともNext.jsのサイトマップですか?
どちらも機能しますが、同じURLでは両方ではありません。 head tags via generateMetadata デバッグが簡単です。 サイトマップのルートは、大規模なカタログに適しており、ページの頭がリーンに保たれます; を覚えておいてください xmlns:xhtml 宣言オン <urlset>。
ロケールが部分的にしか翻訳されていない場合はどうなりますか?
そのページにコンテンツが真に存在するロケールのみを宣伝し、未翻訳のページにマークを付けます noindex, follow。設定されたロケール リストを宣伝すると、未翻訳のページを指すクラスターが公開されます。
Next.jsのhreflang URLは絶対的である必要がありますか?
はい。 Hreflangは完全修飾URLが必要です。 setはどちらでも metadataBase または、プレビュー デプロイが実稼働 URL を送信しないように、設定されたオリジンから URL をビルドします。
レンダリングされたフレフラング出力を確認するにはどうすればよいですか?
プロダクションビルドを実行し、兄弟ページ2 つをフェッチして比較します。 the languages ブロックはグループ全体でバイト同一である必要がありますが、各標準は独自の URL を指します。



