2023年のことですが、ベルリン拠点のクライアント向けに多言語対応のeコマースサイトをリリースしました。12言語、App Router、すべてが整然としていました。3か月後、彼らのドイツ語商品ページはイギリスのSERPでランク付けされ、英語ページはミュンヘンのユーザーに配信されていました。hreflangタグは技術的には存在していました。ただ、間違った形で存在していたため、対処に恥ずかしいほどの午後を費やしてしまいました。この経験から、ドキュメントページ以上にNext.js i18nについて学べました。
これは「公式ドキュメントを要約した記事」ではありません。Seahawk Mediaで多言語サイトを構築してきた経験、繰り返し課題になること、そして2026年に実際にランキングに影響を与える具体的な判断についてお伝えします。
---
App Routerはすべてを変えました(すべてが改善されたわけではありませんが)
Pages Routerはi18nサポートが組み込まれていました。next.config.jsにブロックを追加するだけで、Next.jsはロケール検出、サブパスルーティング、Linkコンポーネントのロケール切り替えをほぼ自動的に処理していました。完璧ではありませんでしたが、意見を持つ設計だったため、落とし穴が少なかったのです。next.config.js and Next.js handled locale detection, sub-path routing, and Link component locale switching almost automatically. It wasn't perfect, but it was opinionated, which meant fewer footguns.
App Router は i18n ルーティングのネイティブサポートを完全に廃止しました。next.config.js に i18n キーはありません。自動ロケール検出もありません。ルーティングロジック全体があなたの責任になり、その責任こそがほとんどのチームが静かに混乱させる場所です。i18n key in next.config.js. No automatic locale detection. You are now responsible for the full routing logic, and that responsibility is where most teams quietly make a mess.
2026年の推奨アプローチは、app/ ディレクトリのルートに [locale] 動的セグメントを置くことです。app/[locale]/page.tsx のような形です。これを next-intl または react-i18next 経由の i18next と組み合わせ、Accept-Language ヘッダーを読んでそれに応じてリダイレクトするミドルウェアファイルを追加すれば、動作する基盤ができます。[locale] dynamic segment at the root of your app/ directory. Something like app/[locale]/page.tsx . Pair that with next-intl or i18next via react-i18next, a middleware file that reads Accept-Language headers and redirects accordingly, and you have a working foundation.
ミドルウェアが誤解される理由
ミドルウェアファイルは、人々が混同する2つの別々の仕事をしています。1つ目はロケール検出:Accept-Language ヘッダー(またはクッキー、URLプリフィックス)を読んで、どのロケールを提供するか判断することです。2つ目は、URLをリダイレクトまたは書き換えて、ユーザーが何らかの隠されたロケール状態ではなく /de/produkte で終わるようにすることです。Accept-Language header (or a cookie, or a URL prefix) and deciding which locale to serve. Second, redirecting or rewriting the URL so the user ends up at /de/produkte rather than /produkte with some hidden locale state.
両方ともページがレンダリングされる前に発生する必要があります。当たり前に聞こえますが、React コンテキストプロバイダーの内部でロケール検出を実行するチームを見てきました。そうすると、Googlebot に配信される初期 HTML にロケールシグナルが全くありません。ボットはページを見て、ルート URL でそれをインデックスし、突然 canonical の混乱が生じます。before the page renders. That sounds obvious. But I've seen teams run locale detection inside a React context provider, which means the initial HTML served to Googlebot has no locale signal at all. The bot sees a page, it indexes it under the root URL, and suddenly you have a canonical mess.
ロケール解決はミドルウェアに保つこと。常に。
---
サブパス vs サブドメイン:どちらかを選んでコミットする
永遠の議論です。/en/about vs en.yoursite.com/about。ほとんどのプロジェクトについて、私はサブパスルーティングを推奨します。理由はこうです:ドメインオーソリティを集約します。.com を指すあらゆるリンクがすべてのロケールに利益をもたらします。サブドメインを使うと、Google の目には実質的に別々のサイトを運営していることになり、各サイトのオーソリティを独立して構築する必要があります。/en/about vs en.yoursite.com/about . For most projects I recommend sub-path routing, and here's why: it concentrates your domain authority. Every link pointing at your .com benefits all locales. With sub-domains you're effectively running separate sites in Google's eyes, which means you need to build authority for each one independently.
1つの例外があります。ローカル TLD が信頼に非常に重要な市場(ドイツの .de、フランスの .fr)にいるなら、国コード TLD はサブパスとサブドメイン両方に勝り、知覚されるローカル信頼性では最強です。これは SEO 主張ではなく、コンバージョン主張です。.de vs yourbrand.com/de でランキングが同じでも信頼メトリクスが意味のある形で動いた A/B テストを見てきました。.de in Germany, .fr in France), a country-code TLD beats both sub-paths and sub-domains for perceived local credibility. That's not an SEO claim, it's a conversion claim. I've seen A/B tests where .de vs yourbrand.com/de moved trust metrics meaningfully even when rankings were identical.
ただし、単一のNext.jsアプリ内でccTLDセットアップを簡単に実行することはできません。そのため、プロジェクトの99%では、サブパス、1つのアプリ、1つのドメインという構成になります。
---
hreflang: あなたを欺くタグ
ここが最も被害を見かける場所です。hreflangは概念的に難しくありません。検索エンジンに「このページには他の言語での同等ページがあります、ここにあります」と伝えます。実装の現場では、私が何度も遭遇する3つの具体的な失敗パターンがあります。
失敗パターン1: タグが相互参照されていない
hreflangセット内のすべてのページは、自身を含む他のすべてのページを参照する必要があります。/en/aboutが/de/aboutを指すhreflangを持っているのに、/de/aboutが/en/aboutへのhreflangを持ち返していない場合、Googleはセット全体を無視します。ペナルティを課すのではなく、無視します。タグが存在しないかのようにです。/en/about has an hreflang pointing to /de/about but /de/about does not have an hreflang pointing back to /en/about, Google ignores the whole set. Not penalises. Ignores. As if the tags don't exist.
Seahawk は昨年、2人の開発者が異なるコンポーネント内で独立して実装し、調整しなかったため、hreflangタグの40%が相互参照されていなかったフィンテックプロジェクトを抱えていました。修正には1時間かかりました。ランキングの回復には2ヶ月かかりました。
失敗パターン2: x-defaultタグがないか、配置が間違っている
x-defaultは、特定の言語または地域をターゲットにしていないページ用です。通常はルート/または言語選択ページです。x-defaultを英語ページに配置するチームもありますが、英語が本当にサポートされていないすべてのロケールのフォールバックであれば問題ありません。ただし、サポートされていない言語のユーザーに別のページを提供する場合、x-defaultはそこを指す必要があります。 is for pages that don't target a specific language or region. Typically your root / or a language-selector page. Some teams put x-default on their English page, which is fine if English is genuinely your fallback for all unmatched locales. But if you serve a different page to users whose language you don't support, x-default should point there.
x-defaultが完全に省略されているケースを見てきました。Googleはエラーを発生させません。単にマッチしないクエリに対してどのバージョンを表示するかを独自に判断し、その判断はしばしば間違っています。x-default omitted entirely. Google doesn't throw an error. It just makes its own decision about which version to show for unmatched queries, and that decision is often wrong.
失敗パターン3:末尾スラッシュと正規URL(Canonical)の競合
hreflangtタグが https://yoursite.com/de/ueber-uns を指しているのに、同じページの正規URLタグが https://yoursite.com/de/ueber-uns/ (末尾のスラッシュに注目) を指している場合、Googleはこれらを異なるURLとして扱います。正規URLとhreflangtが異なるものを指すようになり、Search Consoleに目立ったエラーは表示されないまま、ランキングが静かに抑制されるシグナル競合を生み出してしまいます。https://yoursite.com/de/ueber-uns . Your canonical tag on the same page says https://yoursite.com/de/ueber-uns/ (note the trailing slash). Google treats those as different URLs. Your canonical and hreflang are now pointing at different things, and you've created a signal conflict that quietly suppresses rankings without any visible error in Search Console.
next.config.js で trailingSlash を設定して、どこでも一貫性を保ってください。チェックしてください。実際に自分のURLをcurlして、出力結果を読んで確認してください。trailingSlash in next.config.js and be consistent everywhere. Check it. Actually check it by curling your own URLs and reading the output.
Google Search Central のhreflangtに関するドキュメントは、全文を読む価値があります。具体的で実用的な、あの種の稀な公式ドキュメントの1つです。Google Search Central documentation on hreflang is genuinely worth reading in full. It's one of those rare official docs that's specific enough to be useful.
---
Next.js での大規模な hreflang 生成
5言語のサイトであればhreflangtタグを手作業で書くこともできます。ただし12ロケールを3,000商品ページに渡って対応させるなら、体系的なアプローチが必要です。
僕が使っているのはこの方法です:
- ロケールのリストを1回だけ定義して、単一の設定ファイルに置いてください。locales.config.ts みたいなファイルで、['en', 'de', 'fr', 'es', 'nl'...] という配列をエクスポートする形です。
locales.config.tsexporting an array:['en', 'de', 'fr', 'es', 'nl'...]. - getHreflangAlternates(pathname: string) というユーティリティ関数を構築します。ロケール非依存のパス(/about)を受け取り、全ロケール向けの { hreflang, href } オブジェクトの配列を返す関数です。
getHreflangAlternates(pathname: string)that takes a locale-agnostic path (/about) and returns an array of{ hreflang, href }objects for every locale. - このメソッドをルートレイアウトの root layout.tsx で呼び出し、Next.js の metadata API の alternates.languages フィールドを使って結果をインジェクトします。これは Next.js 13.3 で追加され、正しい <link rel="alternate" hreflang="..."> タグが <head> に自動生成されます。
layout.tsxand inject the results via Next.js'smetadataAPI using thealternates.languagesfield. This was added in Next.js 13.3 and it generates the correct<link rel="alternate" hreflang="...">tags in<head>automatically. - ブログ投稿や商品ページなど動的コンテンツを持つページの場合、すべてのロケールに翻訳があるわけではないので、supportedLocales 配列をユーティリティに渡して、実際にコンテンツが存在するロケールに限定したタグを生成します。404を返すページを指すhreflangタグは、タグを設定しないよりも悪いです。
supportedLocalesarray to your utility and only generate tags for locales where the content actually exists. A hreflang tag pointing to a page that returns 404 is worse than no hreflang tag.
metadata API の alternates.languages アプローチは、これを処理する最もクリーンな方法です。カスタム <Head> のやり取りもなく、タグ自体に関する外部パッケージも不要です。alternates.languages approach in the metadata API is the cleanest way I've found to handle this. No custom <Head> juggling, no third-party packages for the tags themselves.
---
面倒にならないロケール検出
ここに UX の落とし穴があり、それは SEO にも影響を与えます。ブラウザのロケールに基づく積極的な自動リダイレクトは両方とも悪いです。
ドイツのユーザーが yoursite.com/en/article にアクセスし、ミドルウェアがユーザーに尋ねずに /de/artikel に強制リダイレクトした場合、ユーザーの意図したナビゲーションを壊してしまいます。また、リンク共有も潜在的に壊します。誰かが英語の URL を共有し、フランスのユーザーが受信者である場合、フランス語にまだ翻訳されていないコンテンツにバウンスされる可能性があります。yoursite.com/en/article and your middleware hard-redirects them to /de/artikel without asking, you've just broken their intended navigation. You've also potentially broken link sharing: someone shares the English URL, the recipient in France gets bounced to French, and the content they were meant to see might not exist in French yet.
私のルール:検出して、提案する、強制しない。Accept-Language ヘッダーを使って初回アクセス時に合理的なデフォルトを設定し(Cookie に保存)、ユーザーが手動でオーバーライドするのを許可し、その後のすべてのアクセスでそのオーバーライドを優先します。Cookie がヘッダーより優先されます。detect, suggest, don't force. Use the Accept-Language header to set a sensible default on first visit (stored in a cookie), but let users override it manually and honour that override on every subsequent visit. The cookie takes priority over the header.
SEO の観点では、Googlebot は意味のある Accept-Language ヘッダーを送信しません。URL をそのままクロールします。すべてのロケール URL がリダイレクトなしで直接アクセス可能であり、サイトマップにすべてのロケールバリアントが明示的に含まれていることを確認してください。Accept-Language headers. It will crawl your URLs as-is. So make sure every locale URL is directly accessible without redirection, and that your sitemap includes all locale variants explicitly.
next-intl ミドルウェアドキュメントはこの検出戦略をよくカバーしており、新しいプロジェクトを始める誰かに送る場所です。next-intl middleware documentation covers this detection strategy well, and it's where I'd send anyone starting a new project.
多言語サイト向けサイトマップ
多言語サイト向けサイトマップ
小規模サイトであれば、すべてのロケールバリアントを含む単一のsitemap.xmlで十分です。大規模サイト(複数のロケール全体で数万のURL)の場合は分割します。/sitemap.xmlにサイトマップインデックスを1つ配置し、その後/sitemaps/en.xml、/sitemaps/de.xmlなど、ロケールごとに別のサイトマップファイルを配置します。sitemap.xml that includes every locale variant is fine for smaller sites. For large sites (tens of thousands of URLs across multiple locales) I split it: one sitemap index at /sitemap.xml , then separate sitemap files per locale at /sitemaps/en.xml , /sitemaps/de.xml, and so on.
各URLエントリには、hreflangの設定をミラーリングした<xhtml:link rel="alternate">タグを含めるべきです。ページ内タグと冗長になりますが、Googlebotがまだクロールしていないページでも関連性を認識するのに役立ちます。<xhtml:link rel="alternate"> tags mirroring your hreflang setup. This is redundant with the in-page tags but it helps Googlebot connect the dots on pages it hasn't crawled yet.
Next.js 14以降では、app/sitemap.tsを経由してサイトマップを動的に生成できます。戻り値の型はメタデータAPIと同じ形状のalternates.languagesをサポートしています。以前作成した同じgetHreflangAlternatesユーティリティに接続すれば、一貫性が自動的に確保されます。app/sitemap.ts . The return type supports alternates.languages in the same shape as the metadata API. Wire it up to the same getHreflangAlternates utility you built earlier and you get consistency for free.
具体的な数字を挙げると、8つのロケールと4,500の商品を持つプロジェクトでは、サイトマップを分割することで、GSCのクロール統計に基づいてロケールあたりの平均クロール時間が約30%削減されました。劇的ではありませんが、意味のある改善です。
---
翻訳ライブラリ:実際に使っているもの
知る価値のあるものが3つあります。next-intlはApp Routerプロジェクトのデフォルトです。サーバーコンポーネントへのファーストクラスサポートがあり、複数形を適切に処理し、APIがクリーンです。2023年半ば以降、約40プロジェクトで使用しています。next-intl is my default for App Router projects. It has first-class support for server components, handles pluralisation properly, and the API is clean. I've used it on about 40 projects since mid-2023.
react-i18nextは実績十分で巨大なエコシステムを持っていますが、その思考モデルはクライアントコンポーネント向けに構築されており、i18nextコアを経由してサーバーコンポーネントサポートを追加するのはnext-intlのネイティブなアプローチと比べると付け足されたような感じです。i18next core feels bolted-on compared to next-intl's native approach.
Linguiは、翻訳ワークフローがCMSではなくプロの翻訳者を中心としている場合に検討する価値があります。抽出ツールが優れています。ただし、ContentfulやSanityからコンテンツが来るほとんどのエージェンシープロジェクトでは、next-intlのほうがシンプルです。
避けるべきこと:独自に実装することです。2020年に、クライアントが既存のPHP翻訳ファイルを直接再利用することを主張したプロジェクトで一度やりました。何とか動きましたが、2年間にわたってメンテナンスすることになり、今ではnext-intlが単一のuseTranslations()呼び出しで処理するものです。useTranslations() call.
---
FAQ
Next.js App Routerは古い`next.config.js`のi18nブロックをサポートしていますか?
いいえ。i18n設定キーはPages Router特有のものです。App Routerへ移行する場合、[locale]動的セグメントとミドルウェアファイルを使用してルーティングを実装する必要があります。古い設定ブロックはApp Routerプロジェクトでは無視されます。i18n configuration key was specific to the Pages Router. If you're migrating to App Router, you need to implement routing via a [locale] dynamic segment and a middleware file. The old config block is silently ignored in App Router projects.
URLパスに`lang`を使用するべきか、サブドメインを使用するべきか?
ほとんどのプロジェクトでは、URLパスに言語を含める(/de/のようなサブパスルーティング)が正しい選択です。サブドメインはドメインオーソリティを分散させ、個別のDNS設定が必要になります。ccTLDは、ローカルの信頼シグナルが特定の市場で実際にコンバージョン率を動かしている場合だけ、複雑さに見合う価値があります。lang in the URL path (sub-path routing like /de/) is the right call. Subdomains fragment your domain authority and require separate DNS configuration. ccTLDs are only worth the complexity if local trust signals are genuinely moving conversion rates in your specific markets.
Googlebotは、JavaScriptでレンダリングされたロケールコンテンツをどのように処理しますか?
時によっては不適切に処理します。ロケール検出がReactコンテキスト内で純粋にクライアント側で実行され、初期HTMLにロケールマーカーがない場合、Googlebotはhydration前の状態をインデックスする可能性があります。常にミドルウェアでロケールを解決し、サーバーレンダリングされたHTML内の<html>要素に正しいlang属性があることを確認してください。ブラウザのdevtoolsではなく、ソースを表示することで確認してください(devtoolsはhydration後の状態を表示します)。lang attribute is on the <html> element in the server-rendered HTML. Check this by viewing source, not by using browser devtools (which show post-hydration state).
サイトに言語セレクターページがない場合、正しい `x-default` hreflang値は何ですか?
プライマリロケールのホームページ(通常は /en/)を指すようにしてください。これにより、サポートしていないユーザーの言語のフォールバックとして英語であることをGoogleに通知します。これを行うことはペナルティではなく、Googleが尊重するかどうかは分からない柔らかい優先度信号です。/en/. It signals to Google that English is your fallback for users whose language you don't support. It's not a penalty to do this, it's just a soft preference signal Google may or may not honour.
有料SEOツールを使わずにhreflang実装を検証できますか?
はい。Google Search ConsoleのURL検査ツールで、インデックスされているURLに対して検出されたhreflangタグが表示されます。相互参照を自動的に検証することはできませんが、代表的なページをいくつか手動で確認できます。一括検証の場合、Screaming Frog(500URLまで無料)はhreflangタブで相互参照チェックを実行します。Google Search Console's URL Inspection tool will show you detected hreflang tags for any indexed URL. It won't validate reciprocity automatically, but you can manually check a few representative pages. For bulk validation, Screaming Frog (free up to 500 URLs) handles reciprocity checks in its hreflang tab.
---
Next.jsの多言語対応を正しく実装することは、本当に地味な作業です。設定ファイル、URLの一貫性、そして2人の開発者が異なるコンポーネントで同じものを実装していないかを確認することになります。ただし、きちんと時間をかけて実装したサイトは、複数の地域でランキングが安定しており、手を抜いたサイトは、ここに書き留めた教訓を教えてくれました。
ちなみに、ベルリンのクライアントは最終的にドイツのドイツ語ページのランキングを獲得しました。完全なクロール再実行サイクルと大量のコーヒーが必要でした。
