言語を切り替える
テーマを切り替える

Next.js 多言語 SEO 最適化完全ガイド:検索エンジンに各言語を正しくインデックスさせる

せっかく多言語サイトを構築しても、検索エンジンが意図しない言語バージョンを表示してしまうことがあります。日本語で検索したユーザーが英語のページに飛ばされたり、異なる言語バージョン同士が検索結果で競合して全体の順位が下がったりする——そんな現象です。

これらはすべて、多言語サイトにおける SEO 設定の不備が原因です。Google の統計によると、多言語サイトの 60% 以上で hreflang の設定に誤りがあり、国際化の効果やユーザー体験を著しく損ねています。

60%+
多言語サイトの設定エラー率
多言語サイトの60%以上でhreflangの設定に誤りがあります

本記事では、Next.js で多言語 SEO を正しく実装するための具体的なアプローチを解説します。

  • hreflang タグの正しい設定方法 — 言語バージョンの混同を防止
  • 多言語 Sitemap の生成戦略 — 検索エンジンのインデックス登録を高速化
  • URL 設計のベストプラクティス — 最適な国際化構造の選択
  • よくあるエラーの調査と修正 — トラブルシューティングの迅速化

Pages Router と App Router のどちらを採用している場合でも、すぐに実践できる解決策をまとめています。

1. 多言語 SEO の核心概念を理解する

1.1 hreflang とは何か

hreflang は、検索エンジンにそのページの「言語」と「地域」を伝えるための HTML 属性です。主な役割は次の 3 つです。

  1. 重複コンテンツペナルティの回避 — 異なる言語版が「翻訳された同一コンテンツ」であることを伝え、コピーコンテンツ扱いされるのを防ぎます
  2. ユーザーターゲティング — 検索ユーザーの言語設定や地域に基づいて、最適なページを表示します
  3. ユーザー体験の向上 — 誤った言語版を見せず、直帰率を下げコンバージョン率を上げます

1.2 Google は多言語コンテンツをどう処理するか

Google のクローラーが多言語サイトを訪れるとき、以下の手順で処理します。

  1. ページの言語を検出(HTML の lang 属性、hreflang タグ、コンテンツ分析など)
  2. hreflang タグを探し、他の言語バージョンとの関係を理解する
  3. 検索ユーザーの言語設定に合わせて、最適なバージョンを検索結果に表示する
  4. 各言語バージョンの 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 点を検索エンジンに伝えます。

  1. このページには他言語版があること
  2. 各バージョンの完全な URL
  3. 各バージョンに対応する言語・地域コード

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 つのメリット:

  1. インデックス速度の向上 — クローラーがリンクを辿るのを待たず、全言語版を能動的に通知できる
  2. 網羅性の確保 — リンク階層が深いページも含め、言語版の取りこぼしを防ぐ
  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 に送信

手動送信で即座にインデックスをトリガーできます。

  1. Google Search Console にアクセス
  2. サイトプロパティを選択
  3. 左メニューから「サイトマップ」を選択
  4. sitemap.xml の URL を入力
  5. 「送信」をクリック

方法 3:Bing Webmaster Tools に送信

Bing も一定のシェアがあります。

  1. Bing Webmaster Tools にアクセス
  2. サイトを追加
  3. 「サイトマップ」セクションで sitemap.xml を送信

4.7 Sitemap の検証

以下のツールで Sitemap 形式が正しいか確認します。

  1. XML Sitemap Validator: https://www.xml-sitemaps.com/validate-xml-sitemap.html
  2. Google Search Console: 送信後にインデックス状態とエラーを確認
  3. オンライン 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. おすすめの監視ツール

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 でページをテストします。

  1. ページ URL を入力
  2. Google のクロールと分析を待つ
  3. エラーや警告がないか確認
  4. hreflang タグが正しく認識されているか確認

7.3 hreflang チェックツール

専用の hreflang 検証ツール:

これらのツールでできること:

  • 複数ページの hreflang 設定を一括チェック
  • 対称性の問題(一方向参照)を発見
  • 言語コードの誤りを検出

7.4 Google Search Console での検証

本番デプロイ後:

  1. Google Search Console に Sitemap を送信
  2. Google の初期インデックス完了まで 1〜2 週間待つ
  3. 「国際化」>「言語」レポートを確認
  4. hreflang エラーと警告を確認
  5. 各言語版の検索パフォーマンスを監視

8. よくある質問

Q1: hreflang と canonical の違いは?

  • canonical — ページの正規 URL を検索エンジンに伝え、重複コンテンツ問題に対処
  • hreflang — ページの言語バージョンを伝え、言語ターゲティングに使用

両方を同時に使えます。競合しません。各言語版の canonical は自分自身を指し、hreflang は全言語版を指します。

Q2: すべてのページで hreflang を設定する必要がある?

はい。hreflang タグは各言語版すべてに存在し、対称的(相互参照)である必要があります。英語ページだけに設定し日本語ページにない場合、Google は設定を無視します。

Q3: x-default はどの言語を指すべき?

通常、デフォルト言語または最も汎用的なバージョンを指します。推奨戦略:

  • 主なユーザーが英語圏なら英語版
  • グローバルサイトなら国際英語版(en-US)
  • 特定地域向けサイトならその地域の主要言語

Q4: サブディレクトリ vs サブドメイン、どちらが良い?

サブディレクトリ(推奨):

  • SEO 評価がメインドメインに集中
  • 実装・保守がシンプル
  • ほとんどのプロジェクトに適している

サブドメイン:

  • 市場ごとに独立サーバーへデプロイ可能
  • 各市場を独立運用する大規模国際サイト向け
  • 追加のドメイン管理とコストが必要

結論:特別な要件がなければサブディレクトリ戦略を選びましょう。

Q5: 機械翻訳コンテンツはどう扱う?

SEO 目的での機械翻訳は非推奨です。

  • 検索エンジンは低品質翻訳を識別し、順位に影響
  • 低価値コンテンツとみなされる可能性
  • ユーザー体験が悪く、直帰率が高い

予算が限られる場合の対策:

  1. コアページを優先翻訳(ホーム、主要商品ページ、高トラフィックページ)
  2. 機械翻訳後は必ず人間が校正・推敲
  3. 翻訳品質を段階的に改善し、コンテンツを定期更新

Q6: 多言語サイトのインデックスにはどのくらいかかる?

一般的なタイムライン:

  • Sitemap 送信後 1〜2 週間でインデックス開始
  • 完全インデックスには 1〜2 か月
  • SEO 評価の蓄積には 3〜6 か月

インデックスを加速する方法:

  • Sitemap 形式を正しく保ち、適時に送信
  • コンテンツ品質と更新頻度を高める
  • 高品質な外部リンクを獲得
  • Google Search Console で重要ページのインデックスをリクエスト

9. まとめ

多言語 SEO 最適化は、国際化サイト成功の鍵です。核心ポイントを振り返りましょう。

9.1 核心ポイント

  1. URL 戦略

    • サブディレクトリ戦略を推奨(example.com/en/、example.com/ja/)
    • URL 構造を明確・一貫・理解しやすく保つ
  2. hreflang 設定

    • 各ページに完全な hreflang タグ(全言語版を含む)を設定
    • 自己参照(現在ページが自分自身を参照)を必須とする
    • x-default でデフォルト言語を指定
    • 正しい言語コードを使用(ISO 639-1 標準に準拠)
  3. Sitemap

    • 全言語版の URL を含める
    • Sitemap 内にも hreflang 情報を追加(任意だが推奨)
    • 定期的に更新し、検索エンジンに送信
  4. コンテンツ品質

    • 機械翻訳をそのまま公開しない
    • 各言語版の一貫性と専門性を維持
    • 翻訳だけでなくローカライズ(文化・表現習慣を考慮)
  5. 監視とメンテナンス

    • 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 関連資料

多言語 SEO を正しく実装するには時間と労力がかかりますが、リターンは大きい——より良い検索順位、より正確なユーザーマッチング、より高いコンバージョン率です。本記事のベストプラクティスに従えば、多言語サイトは検索エンジンでより良いパフォーマンスを発揮できます。

実装中に問題があれば、ぜひコメント欄で議論してください!

Next.js 多言語 SEO 完全設定フロー

hreflang タグの設定から多言語 Sitemap 生成、URL 戦略選択までの完全ステップ

⏱️ 目安時間: 2 時間

  1. 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

    ステップ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

    ステップ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

    ステップ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 タグとは?なぜ必要?
hreflang は、検索エンジンにページの対象言語と地域を伝える HTML 属性です。

主な役割:
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 タグはどう設定する?
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', // デフォルト言語
},
},
}
}
```

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 はどう生成する?
方法 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 に送信

提案:Sitemap インデックスの方が柔軟。
多言語サイトの URL 戦略はどう選ぶ?
3 つの 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日

関連記事

コメント

GitHubアカウントでログインしてコメントできます