Next.js 多言語 SEO 最適化完全ガイド:検索エンジンに各言語を正しくインデックスさせる
せっかく多言語サイトを構築しても、検索エンジンが意図しない言語バージョンを表示してしまうことがあります。日本語で検索したユーザーが英語のページに飛ばされたり、異なる言語バージョン同士が検索結果で競合して全体の順位が下がったりする——そんな現象です。
これらはすべて、多言語サイトにおける SEO 設定の不備が原因です。Google の統計によると、多言語サイトの 60% 以上で hreflang の設定に誤りがあり、国際化の効果やユーザー体験を著しく損ねています。
本記事では、Next.js で多言語 SEO を正しく実装するための具体的なアプローチを解説します。
- hreflang タグの正しい設定方法 — 言語バージョンの混同を防止
- 多言語 Sitemap の生成戦略 — 検索エンジンのインデックス登録を高速化
- URL 設計のベストプラクティス — 最適な国際化構造の選択
- よくあるエラーの調査と修正 — トラブルシューティングの迅速化
Pages Router と App Router のどちらを採用している場合でも、すぐに実践できる解決策をまとめています。
1. 多言語 SEO の核心概念を理解する
1.1 hreflang とは何か
hreflang は、検索エンジンにそのページの「言語」と「地域」を伝えるための HTML 属性です。主な役割は次の 3 つです。
- 重複コンテンツペナルティの回避 — 異なる言語版が「翻訳された同一コンテンツ」であることを伝え、コピーコンテンツ扱いされるのを防ぎます
- ユーザーターゲティング — 検索ユーザーの言語設定や地域に基づいて、最適なページを表示します
- ユーザー体験の向上 — 誤った言語版を見せず、直帰率を下げコンバージョン率を上げます
1.2 Google は多言語コンテンツをどう処理するか
Google のクローラーが多言語サイトを訪れるとき、以下の手順で処理します。
- ページの言語を検出(HTML の
lang属性、hreflangタグ、コンテンツ分析など) hreflangタグを探し、他の言語バージョンとの関係を理解する- 検索ユーザーの言語設定に合わせて、最適なバージョンを検索結果に表示する
- 各言語バージョンの SEO 評価を統合する(競合させるのではなく)
1.3 よくある SEO 失敗事例
失敗 1:hreflang タグの欠落
<!-- ❌ 間違い:hreflang がない -->
<head>
<title>My Website</title>
<link rel="canonical" href="https://example.com/en/about" />
</head>
結果:検索エンジンはページ間の関係を理解できず、意図しない言語が表示される可能性があります。
失敗 2:hreflang の非対称設定
<!-- 英語ページ -->
<link rel="alternate" hreflang="en" href="https://example.com/en/about" />
<link rel="alternate" hreflang="ja" href="https://example.com/ja/about" />
<!-- ❌ 日本語ページ - 間違い:hreflang タグが不足 -->
<!-- すべての言語ページで、すべての言語バージョンへのリンクが必要です -->
結果:Google は hreflang が対称的であることを要求します。一方向の設定は無視されます。
失敗 3:誤った言語コード
<!-- ❌ 間違い:非標準の言語コード -->
<link rel="alternate" hreflang="cn" href="..." /> <!-- 正しくは zh -->
<link rel="alternate" hreflang="en-us" href="..." /> <!-- 正しくは en-US。大文字小文字に注意 -->
結果:検索エンジンが言語コードを認識できず、hreflang 設定が無効になります。
2. URL 戦略の選択
実装に入る前に、適切な URL 構造を選ぶ必要があります。これは SEO、ユーザー体験、技術的実装のすべてに影響します。
2.1 主な 3 つの戦略比較
| 戦略 | 例 | SEO 効果 | 実装難易度 | 推奨度 |
|---|---|---|---|---|
| サブディレクトリ | example.com/en/ example.com/ja/ | ⭐⭐⭐⭐⭐ 最高 | ⭐⭐⭐ 普通 | ⭐⭐⭐⭐⭐ |
| サブドメイン | en.example.com ja.example.com | ⭐⭐⭐ 普通 | ⭐⭐⭐⭐ 複雑 | ⭐⭐⭐ |
| URL パラメータ | example.com?lang=en | ⭐⭐ 低い | ⭐⭐⭐⭐⭐ 簡単 | ⭐⭐ |
2.2 各戦略の詳細分析
戦略 1:サブディレクトリ(推奨)
メリット:
- SEO 評価がメインドメインに集中し、サイト全体の順位向上に有利
- 設定がシンプル。追加ドメインや SSL 証明書が不要
- 保守・拡張が容易。コードを一元デプロイできる
- Next.js がネイティブサポートしており、実装が容易
デメリット:
- すべての言語が同一ドメインを共有。特定市場向けの DNS 最適化が難しい
Next.js 実装:
// next.config.js
module.exports = {
i18n: {
locales: ['en', 'zh', 'ja', 'de'],
defaultLocale: 'en',
localeDetection: true // ユーザーの言語を自動検出
}
}
戦略 2:サブドメイン
メリット:
- 市場ごとにサーバーを分けやすい(例:中国向けサーバーを分離)
- サイトごとに技術スタックを独立させられる
- CDN や地理的最適化がしやすい
デメリット:
- SEO 評価が分散。各サブドメインで独自に評価を積み上げる必要がある
- 追加のドメイン管理と SSL 証明書が必要
- 実装・保守コストが高い
戦略 3:URL パラメータ(非推奨)
メリット:
- 実装が最も簡単
デメリット:
- SEO 効果が最悪。検索エンジンがパラメータを無視することがある
- ユーザー体験が悪く、URL が分かりにくい
- CDN キャッシュの最適化が難しい
- 検索結果で言語バージョンを区別できない
結論:
ほとんどのプロジェクトにおいて、サブディレクトリ戦略を強く推奨します。SEO 効果、実装難易度、保守コストのバランスが最も優れています。
3. hreflang 設定 完全ガイド
3.1 hreflang タグの役割
hreflang は以下の 3 点を検索エンジンに伝えます。
- このページには他言語版があること
- 各バージョンの完全な URL
- 各バージョンに対応する言語・地域コード
3.2 Next.js App Router での設定
方法 1:Metadata API を使用(推奨)
Next.js 13 以降の App Router では、Metadata API で簡潔に設定できます。
// app/[lang]/about/page.tsx
import { Metadata } from 'next'
type Props = {
params: { lang: string }
}
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { lang } = params
// サポートする言語リスト
const languages = ['en', 'zh', 'ja', 'de']
// 全言語の alternate リンクを生成
const alternates = {
canonical: `https://example.com/\${lang}/about`,
languages: languages.reduce((acc, locale) => {
acc[locale] = `https://example.com/\${locale}/about`
return acc
}, {} as Record<string, string>)
}
return {
title: 'About Us',
alternates,
// x-default はデフォルト言語(マッチしない場合用)
other: {
'x-default': 'https://example.com/en/about'
}
}
}
export default function AboutPage({ params }: Props) {
return <div>About page in {params.lang}</div>
}
方法 2:カスタム Head コンポーネント
より細かい制御が必要な場合に適しています。
// components/I18nHead.tsx
import Head from 'next/head'
interface I18nHeadProps {
currentLang: string
pathname: string
languages?: string[]
}
export default function I18nHead({
currentLang,
pathname,
languages = ['en', 'zh', 'ja', 'de']
}: I18nHeadProps) {
const baseUrl = 'https://example.com'
return (
<Head>
{/* 現在ページの canonical URL */}
<link rel="canonical" href={`\${baseUrl}/\${currentLang}\${pathname}`} />
{/* 全言語バージョンの hreflang */}
{languages.map(lang => (
<link
key={lang}
rel="alternate"
hrefLang={lang}
href={`\${baseUrl}/\${lang}\${pathname}`}
/>
))}
{/* x-default はデフォルト言語 */}
<link
rel="alternate"
hrefLang="x-default"
href={`\${baseUrl}/en\${pathname}`}
/>
</Head>
)
}
使用例:
// app/[lang]/about/page.tsx
import I18nHead from '@/components/I18nHead'
export default function AboutPage({ params }: { params: { lang: string } }) {
return (
<>
<I18nHead
currentLang={params.lang}
pathname="/about"
/>
<div>About page content</div>
</>
)
}
3.3 Next.js Pages Router での設定
Pages Router では異なる API を使います。
// pages/about.tsx
import { GetStaticProps } from 'next'
import Head from 'next/head'
import { useRouter } from 'next/router'
export default function AboutPage() {
const router = useRouter()
const { locale, locales, asPath } = router
const baseUrl = 'https://example.com'
return (
<>
<Head>
{/* 現在ページの canonical URL */}
<link rel="canonical" href={`\${baseUrl}/\${locale}\${asPath}`} />
{/* 全言語バージョンの hreflang */}
{locales?.map(loc => (
<link
key={loc}
rel="alternate"
hrefLang={loc}
href={`\${baseUrl}/\${loc}\${asPath}`}
/>
))}
{/* x-default デフォルト言語 */}
<link
rel="alternate"
hrefLang="x-default"
href={`\${baseUrl}/en\${asPath}`}
/>
</Head>
<div>About page content</div>
</>
)
}
export const getStaticProps: GetStaticProps = async ({ locale }) => {
return {
props: {
messages: (await import(`../locales/\${locale}.json`)).default
}
}
}
3.4 地域コードを含む高度な設定
特定の国・地域向けにコンテンツを出し分ける場合は、language-REGION 形式を使います。
// 地域別の英語ユーザー向け
const hreflangConfig = {
'en-US': 'https://example.com/en-us/about', // アメリカ英語
'en-GB': 'https://example.com/en-gb/about', // イギリス英語
'en-AU': 'https://example.com/en-au/about', // オーストラリア英語
'zh-CN': 'https://example.com/zh-cn/about', // 中国本土簡体字
'zh-TW': 'https://example.com/zh-tw/about', // 台湾繁体字
'zh-HK': 'https://example.com/zh-hk/about', // 香港繁体字
}
Next.js で地域レベルのルーティングを実装:
// next.config.js
module.exports = {
i18n: {
locales: ['en-US', 'en-GB', 'en-AU', 'zh-CN', 'zh-TW', 'zh-HK'],
defaultLocale: 'en-US',
}
}
3.5 よくある設定ミスと修正方法
ミス 1:自己参照(Self-referencing)がない
<!-- ❌ 間違い:現在ページが自分自身を参照していない -->
<link rel="alternate" hreflang="ja" href="https://example.com/ja/about" />
<!-- ✅ 正しい:現在ページを含む全言語版を参照 -->
<link rel="alternate" hreflang="en" href="https://example.com/en/about" />
<link rel="alternate" hreflang="ja" href="https://example.com/ja/about" />
なぜ自己参照が必須か?
Google は hreflang が対称的であることを要求します。各言語版は他のすべてのバージョン(自分自身を含む)を参照する必要があります。
ミス 2:x-default がない
<!-- ✅ 推奨:x-default をデフォルト言語として設定 -->
<link rel="alternate" hreflang="x-default" href="https://example.com/en/about" />
x-default は、どの hreflang にもマッチしないユーザー向けのフォールバックページを指定します。例:
- アラビア語ブラウザのユーザーだが、サイトがアラビア語非対応
- 検索エンジンは
x-defaultで指定されたページを返す
ミス 3:hreflang と canonical の矛盾
<!-- ❌ 間違い:canonical が他言語版を指している -->
<link rel="canonical" href="https://example.com/en/about" />
<link rel="alternate" hreflang="ja" href="https://example.com/ja/about" />
<!-- ✅ 正しい:canonical は現在の言語版を指す -->
<link rel="canonical" href="https://example.com/ja/about" />
<link rel="alternate" hreflang="en" href="https://example.com/en/about" />
<link rel="alternate" hreflang="ja" href="https://example.com/ja/about" />
原則:canonical タグは現在ページ自身の URLを指す必要があり、他言語版を指してはいけません。
4. 多言語 Sitemap の実装
Sitemap は、検索エンジンがページを発見・インデックスするための重要な地図です。多言語サイトでは、正しい Sitemap 設定が不可欠です。
4.1 なぜ多言語 Sitemap が必要か
多言語 Sitemap の 3 つのメリット:
- インデックス速度の向上 — クローラーがリンクを辿るのを待たず、全言語版を能動的に通知できる
- 網羅性の確保 — リンク階層が深いページも含め、言語版の取りこぼしを防ぐ
- hreflang 情報の補完 — Sitemap 内にも hreflang を記述でき、言語関係を強化できる
4.2 Sitemap 戦略の選択
サイト規模に応じて適切な方式を選びます。
方式 1:単一 Sitemap(小規模サイト向け)
5000 ページ以内のサイトなら、すべての言語 URL を 1 つの sitemap.xml にまとめます。
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
xmlns:xhtml="http://www.w3.org/1999/xhtml">
<!-- 英語版 -->
<url>
<loc>https://example.com/en/about</loc>
<xhtml:link rel="alternate" hreflang="en" href="https://example.com/en/about"/>
<xhtml:link rel="alternate" hreflang="zh" href="https://example.com/zh/about"/>
<xhtml:link rel="alternate" hreflang="ja" href="https://example.com/ja/about"/>
<xhtml:link rel="alternate" hreflang="x-default" href="https://example.com/en/about"/>
</url>
<!-- 日本語版 -->
<url>
<loc>https://example.com/ja/about</loc>
<xhtml:link rel="alternate" hreflang="en" href="https://example.com/en/about"/>
<xhtml:link rel="alternate" hreflang="zh" href="https://example.com/zh/about"/>
<xhtml:link rel="alternate" hreflang="ja" href="https://example.com/ja/about"/>
<xhtml:link rel="alternate" hreflang="x-default" href="https://example.com/en/about"/>
</url>
</urlset>
方式 2:言語別 Sitemap(大規模サイト向け)
言語ごとに Sitemap を分割し、sitemap index で集約。5000 ページ超や言語版が多いサイトに適しています。
<!-- sitemap-index.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<sitemap>
<loc>https://example.com/sitemap-en.xml</loc>
<lastmod>2025-01-01</lastmod>
</sitemap>
<sitemap>
<loc>https://example.com/sitemap-zh.xml</loc>
<lastmod>2025-01-01</lastmod>
</sitemap>
<sitemap>
<loc>https://example.com/sitemap-ja.xml</loc>
<lastmod>2025-01-01</lastmod>
</sitemap>
</sitemapindex>
4.3 Next.js App Router で Sitemap を生成
Next.js 13 以降は、組み込みの Sitemap 生成機能が使えます。
// app/sitemap.ts
import { MetadataRoute } from 'next'
// サポートする言語リスト
const languages = ['en', 'zh', 'ja', 'de']
// サイトの全ルート(言語プレフィックスなし)
const routes = ['', '/about', '/blog', '/contact']
export default function sitemap(): MetadataRoute.Sitemap {
const baseUrl = 'https://example.com'
const sitemap: MetadataRoute.Sitemap = []
// 各ルートの全言語版を生成
routes.forEach(route => {
languages.forEach(lang => {
const url = `${baseUrl}/${lang}${route}`
sitemap.push({
url,
lastModified: new Date(),
changeFrequency: 'weekly',
priority: route === '' ? 1 : 0.8,
// Next.js が alternateRefs を自動処理
alternates: {
languages: languages.reduce((acc, l) => {
acc[l] = `${baseUrl}/${l}${route}`
return acc
}, {} as Record<string, string>)
}
})
})
})
return sitemap
}
4.4 動的コンテンツの Sitemap 生成
ブログ記事や商品ページなど動的コンテンツがある場合、DB や CMS からデータを取得します。
// app/sitemap.ts
import { MetadataRoute } from 'next'
const languages = ['en', 'zh', 'ja']
const baseUrl = 'https://example.com'
// DB または CMS から記事一覧を取得
async function getArticles() {
// 実プロジェクトでは DB や CMS API から取得
// 例:const articles = await prisma.article.findMany()
return [
{ slug: 'getting-started', lastModified: '2025-01-01' },
{ slug: 'advanced-guide', lastModified: '2025-01-15' },
]
}
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const sitemap: MetadataRoute.Sitemap = []
// 1. 静的ページを追加
const staticPages = ['', '/about', '/contact']
staticPages.forEach(page => {
languages.forEach(lang => {
sitemap.push({
url: `${baseUrl}/${lang}${page}`,
lastModified: new Date(),
changeFrequency: 'monthly',
priority: page === '' ? 1 : 0.8,
alternates: {
languages: languages.reduce((acc, l) => {
acc[l] = `${baseUrl}/${l}${page}`
return acc
}, {} as Record<string, string>)
}
})
})
})
// 2. 動的コンテンツ(ブログ記事)を追加
const articles = await getArticles()
articles.forEach(article => {
languages.forEach(lang => {
sitemap.push({
url: `${baseUrl}/${lang}/blog/${article.slug}`,
lastModified: new Date(article.lastModified),
changeFrequency: 'weekly',
priority: 0.6,
alternates: {
languages: languages.reduce((acc, l) => {
acc[l] = `${baseUrl}/${l}/blog/${article.slug}`
return acc
}, {} as Record<string, string>)
}
})
})
})
return sitemap
}
4.5 Pages Router で Sitemap を生成
Pages Router では API ルートを手動で作成します。
// pages/api/sitemap.xml.ts
import { NextApiRequest, NextApiResponse } from 'next'
const baseUrl = 'https://example.com'
const languages = ['en', 'zh', 'ja']
function generateSiteMap(pages: string[]) {
return `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
xmlns:xhtml="http://www.w3.org/1999/xhtml">
${pages.map(page => {
return languages.map(lang => {
const url = `${baseUrl}/${lang}${page}`
const alternates = languages.map(l =>
` <xhtml:link rel="alternate" hreflang="${l}" href="${baseUrl}/${l}${page}"/>`
).join('\n')
return ` <url>
<loc>${url}</loc>
<lastmod>${new Date().toISOString()}</lastmod>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
${alternates}
<xhtml:link rel="alternate" hreflang="x-default" href="${baseUrl}/en${page}"/>
</url>`
}).join('\n')
}).join('\n')}
</urlset>`
}
export default function handler(req: NextApiRequest, res: NextApiResponse) {
// 全ページルートを定義
const pages = ['', '/about', '/blog', '/contact']
const sitemap = generateSiteMap(pages)
res.setHeader('Content-Type', 'text/xml')
res.write(sitemap)
res.end()
}
4.6 Sitemap を検索エンジンに送信
Sitemap 生成後は、検索エンジンへ能動的に送信し、インデックスを加速します。
方法 1:robots.txt で宣言
最もシンプル。検索エンジンが自動で読み取ります。
# public/robots.txt
User-agent: *
Allow: /
Sitemap: https://example.com/sitemap.xml
方法 2:Google Search Console に送信
手動送信で即座にインデックスをトリガーできます。
- Google Search Console にアクセス
- サイトプロパティを選択
- 左メニューから「サイトマップ」を選択
- sitemap.xml の URL を入力
- 「送信」をクリック
方法 3:Bing Webmaster Tools に送信
Bing も一定のシェアがあります。
- Bing Webmaster Tools にアクセス
- サイトを追加
- 「サイトマップ」セクションで sitemap.xml を送信
4.7 Sitemap の検証
以下のツールで Sitemap 形式が正しいか確認します。
- XML Sitemap Validator: https://www.xml-sitemaps.com/validate-xml-sitemap.html
- Google Search Console: 送信後にインデックス状態とエラーを確認
- オンライン XML バリデータ: XML が標準に完全準拠しているか確認
5. ベストプラクティスと注意点
5.1 コンテンツ翻訳品質の重要性
検索エンジン(特に Google)は低品質な翻訳を検出でき、順位に直接影響します。
やってはいけないこと:
- ❌ Google 翻訳などの自動翻訳結果をそのまま公開する
- ❌ ナビゲーションとタイトルだけ翻訳し、本文は元の言語のままにする
- ❌ 言語版ごとにコンテンツ構造や情報量に大きな差をつける
やるべきこと:
- ✅ プロの翻訳者やネイティブスピーカーに翻訳を依頼する
- ✅ 単なる翻訳ではなくローカライズ(文化差・表現習慣を考慮)を行う
- ✅ 各言語版のコンテンツ一貫性と品質基準を維持する
5.2 自動翻訳の SEO リスク
クライアントサイドの自動翻訳は SEO に全く効果がありません。検索エンジンのクローラーは元のコンテンツしか見ないからです。
// ❌ 非推奨:クライアントサイド自動翻訳(検索エンジンはインデックスできない)
import GoogleTranslate from 'google-translate-api'
export default function Page() {
const [content, setContent] = useState('')
useEffect(() => {
// この方式は SEO に無効
GoogleTranslate(originalText, { to: 'ja' })
.then(res => setContent(res.text))
}, [])
return <div>{content}</div>
}
// ✅ 推奨:サーバーサイドで実際の翻訳コンテンツをレンダリング
export default function Page({ params }: { params: { lang: string } }) {
// DB またはファイルシステムから実際の翻訳コンテンツを取得
const content = await getTranslatedContent(params.lang)
return <div>{content}</div>
}
5.3 パフォーマンス最適化の提案
多言語サイトはグローバルユーザー向けのため、パフォーマンス最適化が特に重要です。
1. CDN で多地域アクセスを加速
// next.config.js
module.exports = {
images: {
domains: ['cdn.example.com'],
},
// 自動圧縮を有効化
compress: true,
}
2. 言語パックを必要に応じて読み込む
すべての言語の翻訳ファイルを一度に読み込まないようにします。
// 現在の言語の翻訳ファイルを動的インポート
const messages = await import(`@/locales/${lang}.json`)
3. キャッシュ戦略
ページキャッシュ時間を適切に設定します。
// app/[lang]/layout.tsx
export const revalidate = 3600 // 1 時間ごとに再検証
5.4 監視とメンテナンス
多言語 SEO は一度設定して終わりではありません。継続的な監視と最適化が必要です。
1. hreflang エラーを定期的に確認
Google Search Console の「国際化」レポートを活用します。
- hreflang タグのエラーと警告を確認
- 各言語版のインデックス状態を確認
- 言語別の検索パフォーマンスとクリック率を監視
2. おすすめの監視ツール
- Google Search Console — 公式ツール。必須。無料
- Ahrefs Site Audit — プロ向け SEO ツール。hreflang を一括チェック可能
- Screaming Frog — クローラーツール。サイト全体をローカルで検査
- hreflang Tags Testing Tool — オンライン検証ツール:https://www.aleydasolis.com/english/international-seo-tools/hreflang-tags-generator/
3. 監視スクリプトの作成
hreflang 設定を自動チェックします。
// scripts/check-hreflang.ts
import { JSDOM } from 'jsdom'
async function checkHreflang(url: string) {
const response = await fetch(url)
const html = await response.text()
const dom = new JSDOM(html)
const document = dom.window.document
const hreflangLinks = document.querySelectorAll('link[rel="alternate"][hreflang]')
console.log(`Found ${hreflangLinks.length} hreflang links on ${url}`)
hreflangLinks.forEach(link => {
const hreflang = link.getAttribute('hreflang')
const href = link.getAttribute('href')
console.log(` ${hreflang}: ${href}`)
})
// 自己参照があるか確認
const currentUrl = new URL(url).href
const hasSelfReference = Array.from(hreflangLinks).some(
link => link.getAttribute('href') === currentUrl
)
if (!hasSelfReference) {
console.warn('⚠️ Warning: Missing self-reference hreflang tag')
}
// x-default があるか確認
const hasXDefault = Array.from(hreflangLinks).some(
link => link.getAttribute('hreflang') === 'x-default'
)
if (!hasXDefault) {
console.warn('⚠️ Warning: Missing x-default hreflang tag')
}
}
// 使用例
checkHreflang('https://example.com/en/about')
checkHreflang('https://example.com/ja/about')
6. 実践例:完全なプロジェクトサンプル
Next.js App Router プロジェクトで多言語 SEO を実装する完全な例を見ていきます。
6.1 プロジェクト構造
my-i18n-site/
├── app/
│ ├── [lang]/ # 動的言語ルート
│ │ ├── layout.tsx # 言語レベルのレイアウト
│ │ ├── page.tsx # ホームページ
│ │ ├── about/
│ │ │ └── page.tsx # About ページ
│ │ └── blog/
│ │ ├── page.tsx # ブログ一覧
│ │ └── [slug]/
│ │ └── page.tsx # ブログ記事詳細
│ ├── sitemap.ts # Sitemap ジェネレータ
│ └── robots.ts # Robots.txt ジェネレータ
├── components/
│ └── I18nMetadata.tsx # 多言語メタデータコンポーネント
├── lib/
│ ├── i18n.ts # 国際化設定
│ └── articles.ts # 記事データ取得
├── locales/ # 翻訳ファイル
│ ├── en.json
│ ├── zh.json
│ └── ja.json
└── next.config.js # Next.js 設定
6.2 設定ファイル
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
// 注意:App Router では i18n 設定を使わない
// 言語ルーティングは手動で実装
}
module.exports = nextConfig
// lib/i18n.ts
export const languages = ['en', 'zh', 'ja'] as const
export type Language = (typeof languages)[number]
export const defaultLanguage: Language = 'en'
export const languageNames: Record<Language, string> = {
en: 'English',
zh: '中文',
ja: '日本語',
}
export function isValidLanguage(lang: string): lang is Language {
return languages.includes(lang as Language)
}
6.3 Layout コンポーネント
// app/[lang]/layout.tsx
import { languages, isValidLanguage } from '@/lib/i18n'
import { notFound } from 'next/navigation'
export async function generateStaticParams() {
return languages.map(lang => ({ lang }))
}
export default function LangLayout({
children,
params,
}: {
children: React.ReactNode
params: { lang: string }
}) {
// 言語コードが有効か検証
if (!isValidLanguage(params.lang)) {
notFound()
}
return (
<html lang={params.lang}>
<body>{children}</body>
</html>
)
}
6.4 Metadata 付きページコンポーネント
// app/[lang]/about/page.tsx
import { Metadata } from 'next'
import { languages, Language } from '@/lib/i18n'
type Props = {
params: { lang: Language }
}
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { lang } = params
const baseUrl = 'https://example.com'
const pathname = '/about'
// alternates 設定を生成
const alternates = {
canonical: `${baseUrl}/${lang}${pathname}`,
languages: languages.reduce((acc, locale) => {
acc[locale] = `${baseUrl}/${locale}${pathname}`
return acc
}, {} as Record<string, string>)
}
// 言語別のタイトルと説明
const titles: Record<Language, string> = {
en: 'About Us - Learn More About Our Company',
zh: '关于我们 - 了解更多关于我们公司的信息',
ja: '私たちについて - 当社についてもっと知る',
}
const descriptions: Record<Language, string> = {
en: 'Learn about our mission, values, and the team behind our success.',
zh: '了解我们的使命、价值观以及我们成功背后的团队。',
ja: '私たちの使命、価値観、そして成功を支えるチームについて学びます。',
}
return {
title: titles[lang],
description: descriptions[lang],
alternates,
openGraph: {
title: titles[lang],
description: descriptions[lang],
url: `${baseUrl}/${lang}${pathname}`,
siteName: 'Example Site',
locale: lang,
type: 'website',
},
}
}
export default function AboutPage({ params }: Props) {
const content = {
en: 'About us content in English...',
zh: '关于我们的中文内容...',
ja: '私たちについての日本語コンテンツ...',
}
return (
<div>
<h1>About Us</h1>
<p>{content[params.lang]}</p>
</div>
)
}
6.5 hreflang 付き動的ルート
// app/[lang]/blog/[slug]/page.tsx
import { Metadata } from 'next'
import { languages, Language } from '@/lib/i18n'
import { getArticle, getAllArticles } from '@/lib/articles'
import { notFound } from 'next/navigation'
type Props = {
params: { lang: Language; slug: string }
}
// 全記事ページを静的生成
export async function generateStaticParams() {
const articles = await getAllArticles()
return languages.flatMap(lang =>
articles.map(article => ({
lang,
slug: article.slug,
}))
)
}
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { lang, slug } = params
const article = await getArticle(slug, lang)
if (!article) {
return {}
}
const baseUrl = 'https://example.com'
const pathname = `/blog/${slug}`
const alternates = {
canonical: `${baseUrl}/${lang}${pathname}`,
languages: languages.reduce((acc, locale) => {
acc[locale] = `${baseUrl}/${locale}${pathname}`
return acc
}, {} as Record<string, string>)
}
return {
title: article.title,
description: article.excerpt,
alternates,
openGraph: {
title: article.title,
description: article.excerpt,
url: `${baseUrl}/${lang}${pathname}`,
type: 'article',
publishedTime: article.publishedAt,
authors: [article.author],
},
}
}
export default async function BlogArticle({ params }: Props) {
const { lang, slug } = params
const article = await getArticle(slug, lang)
if (!article) {
notFound()
}
return (
<article>
<h1>{article.title}</h1>
<p>{article.content}</p>
</article>
)
}
6.6 Sitemap 生成
// app/sitemap.ts
import { MetadataRoute } from 'next'
import { languages } from '@/lib/i18n'
import { getAllArticles } from '@/lib/articles'
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const baseUrl = 'https://example.com'
const sitemap: MetadataRoute.Sitemap = []
// 1. 静的ページを追加
const staticPages = ['', '/about', '/contact']
staticPages.forEach(page => {
languages.forEach(lang => {
sitemap.push({
url: `${baseUrl}/${lang}${page}`,
lastModified: new Date(),
changeFrequency: 'monthly',
priority: page === '' ? 1 : 0.8,
alternates: {
languages: languages.reduce((acc, l) => {
acc[l] = `${baseUrl}/${l}${page}`
return acc
}, {} as Record<string, string>)
}
})
})
})
// 2. 動的コンテンツ(ブログ記事)を追加
const articles = await getAllArticles()
articles.forEach(article => {
languages.forEach(lang => {
sitemap.push({
url: `${baseUrl}/${lang}/blog/${article.slug}`,
lastModified: new Date(article.updatedAt),
changeFrequency: 'weekly',
priority: 0.6,
alternates: {
languages: languages.reduce((acc, l) => {
acc[l] = `${baseUrl}/${l}/blog/${article.slug}`
return acc
}, {} as Record<string, string>)
}
})
})
})
return sitemap
}
6.7 Robots.txt
// app/robots.ts
import { MetadataRoute } from 'next'
export default function robots(): MetadataRoute.Robots {
return {
rules: {
userAgent: '*',
allow: '/',
},
sitemap: 'https://example.com/sitemap.xml',
}
}
7. 検証とテスト
7.1 ローカルテストチェックリスト
本番環境へデプロイする前に、以下を確認してください。
- すべてのページの
<html>タグに正しいlang属性がある - 各ページに完全な hreflang タグ(全言語版を含む)がある
- hreflang タグに自己参照(現在ページが自分自身を参照)がある
-
x-defaultタグがデフォルト言語を指している - canonical タグが正しい URL(現在の言語版)を指している
- Sitemap に全言語版の URL が含まれている
- robots.txt が sitemap.xml を正しく指している
- 言語版間でコンテンツ品質が一貫し、翻訳が正確である
7.2 Google Rich Results Test の使用
Google Rich Results Test でページをテストします。
- ページ URL を入力
- Google のクロールと分析を待つ
- エラーや警告がないか確認
- hreflang タグが正しく認識されているか確認
7.3 hreflang チェックツール
専用の hreflang 検証ツール:
- Aleyda Solis hreflang Generator: https://www.aleydasolis.com/english/international-seo-tools/hreflang-tags-generator/
- Merkle hreflang Checker: https://technicalseo.com/tools/hreflang/
これらのツールでできること:
- 複数ページの hreflang 設定を一括チェック
- 対称性の問題(一方向参照)を発見
- 言語コードの誤りを検出
7.4 Google Search Console での検証
本番デプロイ後:
- Google Search Console に Sitemap を送信
- Google の初期インデックス完了まで 1〜2 週間待つ
- 「国際化」>「言語」レポートを確認
- hreflang エラーと警告を確認
- 各言語版の検索パフォーマンスを監視
8. よくある質問
Q1: hreflang と canonical の違いは?
- canonical — ページの正規 URL を検索エンジンに伝え、重複コンテンツ問題に対処
- hreflang — ページの言語バージョンを伝え、言語ターゲティングに使用
両方を同時に使えます。競合しません。各言語版の canonical は自分自身を指し、hreflang は全言語版を指します。
Q2: すべてのページで hreflang を設定する必要がある?
はい。hreflang タグは各言語版すべてに存在し、対称的(相互参照)である必要があります。英語ページだけに設定し日本語ページにない場合、Google は設定を無視します。
Q3: x-default はどの言語を指すべき?
通常、デフォルト言語または最も汎用的なバージョンを指します。推奨戦略:
- 主なユーザーが英語圏なら英語版
- グローバルサイトなら国際英語版(en-US)
- 特定地域向けサイトならその地域の主要言語
Q4: サブディレクトリ vs サブドメイン、どちらが良い?
サブディレクトリ(推奨):
- SEO 評価がメインドメインに集中
- 実装・保守がシンプル
- ほとんどのプロジェクトに適している
サブドメイン:
- 市場ごとに独立サーバーへデプロイ可能
- 各市場を独立運用する大規模国際サイト向け
- 追加のドメイン管理とコストが必要
結論:特別な要件がなければサブディレクトリ戦略を選びましょう。
Q5: 機械翻訳コンテンツはどう扱う?
SEO 目的での機械翻訳は非推奨です。
- 検索エンジンは低品質翻訳を識別し、順位に影響
- 低価値コンテンツとみなされる可能性
- ユーザー体験が悪く、直帰率が高い
予算が限られる場合の対策:
- コアページを優先翻訳(ホーム、主要商品ページ、高トラフィックページ)
- 機械翻訳後は必ず人間が校正・推敲
- 翻訳品質を段階的に改善し、コンテンツを定期更新
Q6: 多言語サイトのインデックスにはどのくらいかかる?
一般的なタイムライン:
- Sitemap 送信後 1〜2 週間でインデックス開始
- 完全インデックスには 1〜2 か月
- SEO 評価の蓄積には 3〜6 か月
インデックスを加速する方法:
- Sitemap 形式を正しく保ち、適時に送信
- コンテンツ品質と更新頻度を高める
- 高品質な外部リンクを獲得
- Google Search Console で重要ページのインデックスをリクエスト
9. まとめ
多言語 SEO 最適化は、国際化サイト成功の鍵です。核心ポイントを振り返りましょう。
9.1 核心ポイント
-
URL 戦略
- サブディレクトリ戦略を推奨(example.com/en/、example.com/ja/)
- URL 構造を明確・一貫・理解しやすく保つ
-
hreflang 設定
- 各ページに完全な hreflang タグ(全言語版を含む)を設定
- 自己参照(現在ページが自分自身を参照)を必須とする
- x-default でデフォルト言語を指定
- 正しい言語コードを使用(ISO 639-1 標準に準拠)
-
Sitemap
- 全言語版の URL を含める
- Sitemap 内にも hreflang 情報を追加(任意だが推奨)
- 定期的に更新し、検索エンジンに送信
-
コンテンツ品質
- 機械翻訳をそのまま公開しない
- 各言語版の一貫性と専門性を維持
- 翻訳だけでなくローカライズ(文化・表現習慣を考慮)
-
監視とメンテナンス
- Google Search Console で継続監視
- hreflang エラーと警告を定期確認
- 言語別の検索パフォーマンスとコンバージョン率を追跡
9.2 アクションリスト
以下を完了し、多言語 SEO 設定が正しいことを確認しましょう。
- URL 戦略を選択・実装(サブディレクトリ推奨)
- 全ページに完全な hreflang タグを追加
- 正しい canonical タグを設定
- 全言語版を含む Sitemap を生成
- robots.txt を設定し sitemap.xml を指す
- Sitemap を Google Search Console と Bing Webmaster Tools に送信
- 検証ツールで hreflang 設定を確認
- コンテンツ翻訳品質を確認・向上
- 監視フローと定期チェック体制を整備
9.3 関連資料
- Google 多言語・多地域サイトガイド — Google 公式ドキュメント
- hreflang 完全ガイド — Google 公式 hreflang ドキュメント
- Next.js 国際化ルーティング — Next.js 公式ドキュメント
- Schema.org 多言語マークアップ — 構造化データの多言語サポート
多言語 SEO を正しく実装するには時間と労力がかかりますが、リターンは大きい——より良い検索順位、より正確なユーザーマッチング、より高いコンバージョン率です。本記事のベストプラクティスに従えば、多言語サイトは検索エンジンでより良いパフォーマンスを発揮できます。
実装中に問題があれば、ぜひコメント欄で議論してください!
Next.js 多言語 SEO 完全設定フロー
hreflang タグの設定から多言語 Sitemap 生成、URL 戦略選択までの完全ステップ
⏱️ 目安時間: 2 時間
- 1
ステップ1: hreflang タグを設定する
metadata で設定:
```tsx
// app/[locale]/about/page.tsx
export async function generateMetadata({ params }): Promise<Metadata> {
const { locale } = params
return {
title: 'About Us',
alternates: {
languages: {
'zh': '/zh/about',
'en': '/en/about',
'x-default': '/en/about', // デフォルト言語
},
},
}
}
```
ポイント:
• 全言語バージョンを含める
• x-default でデフォルト言語を指定
• 各ページで設定する
HTML 出力:
```html
<link rel="alternate" hreflang="zh" href="https://example.com/zh/about" />
<link rel="alternate" hreflang="en" href="https://example.com/en/about" />
<link rel="alternate" hreflang="x-default" href="https://example.com/en/about" />
```
効果:
• 検索エンジンにページの対象言語を伝える
• 重複コンテンツペナルティを防止
• ユーザーに最適な言語版を表示 - 2
ステップ2: 多言語 Sitemap を生成する
方法 1:言語ごとに独立 Sitemap
```tsx
// app/[locale]/sitemap.ts
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const baseUrl = 'https://example.com'
const locale = params.locale
return [
{
url: `${baseUrl}/${locale}`,
lastModified: new Date(),
changeFrequency: 'daily',
priority: 1,
},
// ...
]
}
```
方法 2:Sitemap インデックスを使用
```tsx
// app/sitemap.ts
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const locales = ['zh', 'en']
const baseUrl = 'https://example.com'
return locales.flatMap(locale => [
{
url: `${baseUrl}/${locale}`,
lastModified: new Date(),
changeFrequency: 'daily',
priority: 1,
},
// ...
])
}
```
ポイント:
• 全言語バージョンを含める
• 正しい URL 形式を使用
• Google Search Console に送信 - 3
ステップ3: URL 戦略を選択する
方式 1:サブパス(推奨)
• URL 形式:/ja/about、/en/about
• 設定がシンプル
• SEO に有利
• ほとんどのプロジェクトに適している
方式 2:ドメイン
• URL 形式:ja.example.com、en.example.com
• 複数ドメインの設定が必要
• より専門的
• 大規模プロジェクト向け
方式 3:Cookie
• Cookie で言語切り替え
• URL に言語プレフィックスなし
• SEO に不利
• 非推奨
選択の提案:
• ほとんどのプロジェクト → サブパス
• 大規模プロジェクト → ドメイン
• 避けるべき → Cookie
ポイント:サブパス方式が SEO 的に最も有利。推奨。 - 4
ステップ4: 検証とテスト
検証ツール:
1. Google Search Console:
• 多言語 Sitemap を送信
• hreflang タグを確認
• インデックス状態を確認
2. hreflang テストツール:
• https://www.aleydasolis.com/en/english-tools/international-seo-tools/hreflang-tags-validator/
• hreflang 設定が正しいか確認
3. 多言語 Sitemap 検証:
• Sitemap 形式を確認
• 全言語バージョンが含まれているか確認
• URL の正確性を検証
よくあるエラーの確認:
• hreflang タグの欠落
• x-default の設定ミス
• Sitemap に全言語版が含まれていない
• URL 形式の不一致
提案:設定後すぐに検証し、問題が起きてから対処しない。
FAQ
hreflang タグとは?なぜ必要?
主な役割:
1. 重複コンテンツペナルティの防止 — 異なる言語版が同一コンテンツの翻訳であることを伝える
2. ユーザーターゲティング — ユーザーの言語・地域設定に基づき最適なページ版を表示
3. ユーザー体験の向上 — 誤った言語版を見せない
設定方法:
```tsx
export async function generateMetadata({ params }): Promise<Metadata> {
return {
alternates: {
languages: {
'zh': '/zh/about',
'en': '/en/about',
'x-default': '/en/about',
},
},
}
}
```
ポイント:
• 全言語バージョンを含める
• x-default でデフォルト言語を指定
• 各ページで設定する
Google の統計によると、多言語サイトの 60% 以上で hreflang 設定に誤りがあります。
hreflang タグはどう設定する?
```tsx
// app/[locale]/about/page.tsx
export async function generateMetadata({ params }): Promise<Metadata> {
const { locale } = params
return {
title: 'About Us',
alternates: {
languages: {
'zh': '/zh/about',
'en': '/en/about',
'x-default': '/en/about', // デフォルト言語
},
},
}
}
```
HTML 出力:
```html
<link rel="alternate" hreflang="zh" href="https://example.com/zh/about" />
<link rel="alternate" hreflang="en" href="https://example.com/en/about" />
<link rel="alternate" hreflang="x-default" href="https://example.com/en/about" />
```
ポイント:
• 全言語バージョンを含める
• x-default でデフォルト言語を指定
• 各ページで設定する
• URL は絶対パスであること
注意:hreflang タグには現在ページを含む全言語バージョンが必要です。
多言語 Sitemap はどう生成する?
```tsx
// app/[locale]/sitemap.ts
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const baseUrl = 'https://example.com'
const locale = params.locale
return [
{
url: `${baseUrl}/${locale}`,
lastModified: new Date(),
changeFrequency: 'daily',
priority: 1,
},
]
}
```
方法 2:Sitemap インデックスを使用
```tsx
// app/sitemap.ts
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const locales = ['zh', 'en']
const baseUrl = 'https://example.com'
return locales.flatMap(locale => [
{
url: `${baseUrl}/${locale}`,
lastModified: new Date(),
changeFrequency: 'daily',
priority: 1,
},
])
}
```
ポイント:
• 全言語バージョンを含める
• 正しい URL 形式を使用
• Google Search Console に送信
提案:Sitemap インデックスの方が柔軟。
多言語サイトの URL 戦略はどう選ぶ?
方式 1:サブパス(推奨)
• URL 形式:/ja/about、/en/about
• 設定がシンプル
• SEO に有利
• ほとんどのプロジェクトに適している
方式 2:ドメイン
• URL 形式:ja.example.com、en.example.com
• 複数ドメインの設定が必要
• より専門的
• 大規模プロジェクト向け
方式 3:Cookie
• Cookie で言語切り替え
• URL に言語プレフィックスなし
• SEO に不利
• 非推奨
選択の提案:
• ほとんどのプロジェクト → サブパス
• 大規模プロジェクト → ドメイン
• 避けるべき → Cookie
ポイント:サブパス方式が SEO 的に最も有利。推奨。
注意:URL 戦略選択後、hreflang タグの URL 形式と一致させること。
多言語 SEO 設定はどう検証する?
1. Google Search Console:
• 多言語 Sitemap を送信
• hreflang タグを確認
• インデックス状態を確認
2. hreflang テストツール:
• https://www.aleydasolis.com/en/english-tools/international-seo-tools/hreflang-tags-validator/
• hreflang 設定が正しいか確認
3. 多言語 Sitemap 検証:
• Sitemap 形式を確認
• 全言語バージョンが含まれているか確認
• URL の正確性を検証
よくあるエラーの確認:
• hreflang タグの欠落
• x-default の設定ミス
• Sitemap に全言語版が含まれていない
• URL 形式の不一致
提案:
• 設定後すぐに検証
• インデックス状態を定期確認
• 問題を早期に修正
覚えておくこと:検証は SEO 最適化の重要なステップ。見落とさない。
多言語 SEO のよくあるエラーは?
1. hreflang タグの欠落
• 検索エンジンがページの言語を認識できない
• 誤った言語版が検索結果に表示される
2. x-default の設定ミス
• x-default が未設定
• または x-default が誤った言語を指している
3. Sitemap に全言語版が含まれていない
• 一部の言語版しか送信していない
• 検索エンジンが全ページを発見できない
4. URL 形式の不一致
• hreflang タグの URL 形式が統一されていない
• 設定エラーの原因になる
5. 重複コンテンツ問題
• hreflang が正しく設定されていない
• 検索エンジンが言語版を重複コンテンツと判断
解決方法:
• hreflang タグを設定
• 全言語バージョンを含める
• 正しい URL 形式を使用
• 完全な Sitemap を送信
提案:本記事のベストプラクティスに従い、これらのよくあるエラーを避ける。
8分で読めます · 公開日: 2025年12月25日 · 更新日: 2026年6月8日
Next.js 完全ガイド
検索からこのページに来た場合は、前後の記事もあわせて読むと同じテーマの理解がかなり早く深まります。
前の記事
Next.js CI/CD 実践ガイド:GitHub Actions で実現する自動テストとデプロイ
GitHub Actions を使用して Next.js プロジェクトの自動テストとデプロイを実現。完全な設定コード、トラブルシューティング、ベストプラクティスを含みます。手動デプロイに別れを告げ、push するだけで自動的に公開しましょう。
第 37 / 47 記事
次の記事
Next.js 国際化と静的生成:SSG 多言語サイト構築の実践ガイド
ビルドエラーからパフォーマンス最適化まで、App Router でハマりどころのない多言語静的生成を実装する手順を解説。完全なコード例、generateStaticParams の設定詳細、ビルド時間短縮のテクニックを含みます。
第 39 / 47 記事
関連記事
Next.js App Router 入門ガイド:コア概念と基本操作を解説
Next.js App Router 入門ガイド:コア概念と基本操作を解説
Next.js 15 実践:週末で本番級ブログシステムを構築した方法
Next.js 15 実践:週末で本番級ブログシステムを構築した方法
Next.js Middleware 実践ガイド:パスマッチ、Edge Runtime 制限とよくある落とし穴
コメント
GitHubアカウントでログインしてコメントできます