# General Translation Knowledge Base: 多言語サイトのサイトマップはどのように作成すればよいですか？
URL: https://generaltranslation.com/ja/kb/multilingual-sitemap.mdx
Docs index: https://generaltranslation.com/llms.txt
Description: 各ページのすべての言語版を、それぞれ独立した url エントリとして記載します。サイトマップで hreflang を指定する場合は、各エントリに自身を含むすべての言語版の xhtml:link 代替リンクを追加し、必要に応じて x-default も指定します。また、50,000 URL または 50MB を超える場合はファイルを分割します。General Translation の gt-next はロケールごとに個別の URL を割り当てるため、それらをサイトマップに記載できます。
Category: 多言語SEO
Last updated: 2026-10-28
Authors: General Translation Team
Published: 2026-10-28

多言語サイトマップでは、ページの各言語版をそれぞれ別の `<url>` エントリとして記載します。たとえば、`/en/pricing`、`/de/preise`、`/pt-BR/precos` は 3 つの別々のエントリになります。サイトマップで hreflang も宣言する場合は、各エントリに、そのページ自身を含むすべての言語版を `xhtml:link` 代替リンクとして記載します。Google はサイトマップ内の hreflang を HTML 内の hreflang と同等に扱います。そのため、サイトマップに漏れがなく、カノニカル URL とも一致していれば、ページ内のタグの代わりにサイトマップで hreflang を指定できます。

## hreflang はサイトマップと HTML のどちらに記述すべきか？

Google の[ローカライズ版に関するガイド](https://developers.google.com/search/docs/specialty/international/localized-versions)では、言語バージョンを宣言する方法として同等の 3 つの方法が紹介されており、複数の方法を併用しても Google 検索でのメリットはないとされています。どの方法を選んでも、インデックス登録したい言語 URL はすべてサイトマップに含める必要があります。選ぶのは、hreflang 注釈をどこに記述するかだけです。

| 方法                   | 記述場所            | 適したケース                                                                | 注意点                          |
| -------------------- | --------------- | --------------------------------------------------------------------- | ---------------------------- |
| HTML `<link>` タグ     | 各ページの `<head>`  | フレームワークがページごとにメタデータをレンダリングする場合 (Next.js の `alternates.languages` など)  | すべてのテンプレートで全言語分のタグを出力する必要がある |
| HTTP `Link` ヘッダー     | レスポンスヘッダー       | PDF など HTML 以外のファイル                                                   | ファイルタイプごとにサーバー設定が必要          |
| サイトマップの `xhtml:link` | 1 つまたは複数のサイトマップ | CMS やビルドステップがすべてのページとロケールを把握している場合、またはページテンプレートを編集できない場合              | サイトマップに含まれていないページには注釈が付かない   |

フレームワークがすでにページごとにタグをレンダリングしているなら HTML タグを、ビルド内の 1 か所ですべてのページとそのロケールを把握できるならサイトマップを使用してください。両方を手作業で管理するのは避けましょう。2 つのリストは次第にずれていきます。 (タグの構文と HTTP ヘッダーについては、[hreflang とは](/kb/what-is-hreflang)を参照してください。)

## 多言語サイトマップの構成方法

以下のルールは、Google の[ローカライズ版ガイド](https://developers.google.com/search/docs/specialty/international/localized-versions)、[サイトマップガイド](https://developers.google.com/search/docs/crawling-indexing/sitemaps/build-sitemap)、および [sitemaps プロトコル](https://www.sitemaps.org/protocol.html)に基づいています。

* **バージョンごとに 1 つの `<url>` を用意する。** 3 言語で提供されるページなら 3 つのエントリになり、それぞれに固有の `<loc>` を持たせます。
* **各エントリには、自身を含むすべてのバージョンを列挙する。** `xhtml:link` のセットは、そのページのすべてのエントリで同一になります。互いを列挙し合っていないページのペアは、Google に無視されます。
* **`x-default` は任意。** 言語セレクターや、対応していない言語の訪問者向けのデフォルトバージョンがある場合に追加します。
* **名前空間を宣言する。** `<urlset>` には、値が `http://www.w3.org/1999/xhtml` の `xmlns:xhtml` 属性が必要です。
* **最終的なカノニカル URL を使用する。** 完全修飾形式 (`/de/preise` ではなく `https://example.com/de/preise`) で、`200` を返し、`noindex` が指定されていない URL を使います。
* **有効なコードを使う。** ISO 639-1 の言語コードに、必要に応じて ISO 3166-1 alpha-2 のリージョン (`en-GB`) または ISO 15924 の文字体系 (`zh-Hant`) を付加します。
* **エスケープとエンコードを行う。** ファイルは UTF-8 で記述し、`&` は `&amp;` と表記し、非 ASCII のパスはパーセントエンコードします。たとえば `/de/über-uns` は `/de/%C3%BCber-uns` になります。
* **`lastmod` はバージョンごとに設定する。** Google がこの値を利用するのは、その URL に対する最後の重要な変更日として一貫して正確な場合に限られます。英語ページと異なる日に更新されたドイツ語訳には、それ独自の日付を設定します。

1 ファイルに含められるのは、最大 50,000 URL、非圧縮で 50MB までです。`xhtml:link` 子要素は URL 数の上限にはカウントされませんが、ファイルサイズにはカウントされます。URL が 45 文字前後の場合、代替リンク 1 つあたり約 100 バイト、20 言語分のエントリ 1 つで約 2.2KB になります。当社の試算では、このようなエントリが約 23,700 件に達した時点でファイルサイズが 50MB に到達するため、50,000 件よりはるかに手前で上限に達します。

### サイトマップインデックスでロケールごとに分割する

ロケールごとに1ファイルにすれば、ほとんどのサイトではファイルサイズを小さく抑えられますが、上限内に収まるとは限りません。前述の見積もりでは、20言語で約23,700ページを超えるロケールは1ファイルに収まらないため、`sitemap-de-1.xml` や `sitemap-de-2.xml` のようにさらに分割してください。インデックスを送信するだけで、Google はそこに記載されたすべてのファイルを読み取ります。各ファイルを個別に送信するかどうかは任意ですが、送信すると Search Console のサイトマップレポートにファイルごとの行が表示されます。[サイトマップインデックス](https://developers.google.com/search/docs/crawling-indexing/sitemaps/large-sitemaps)には、次のようにファイルを列挙します。

```xml title="sitemap.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>
  </sitemap>
  <sitemap>
    <loc>https://example.com/sitemap-de.xml</loc>
  </sitemap>
  <sitemap>
    <loc>https://example.com/sitemap-pt-br.xml</loc>
  </sitemap>
</sitemapindex>
```

各ロケールのファイルにはそのロケールのURLのみを記載しますが、各エントリにはそのページの代替リンクがすべて含まれます：

```xml title="sitemap-de.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/de/preise</loc>
    <lastmod>2026-10-03</lastmod>
    <xhtml:link rel="alternate" hreflang="en" href="https://example.com/en/pricing"/>
    <xhtml:link rel="alternate" hreflang="de" href="https://example.com/de/preise"/>
    <xhtml:link rel="alternate" hreflang="pt-BR" href="https://example.com/pt-BR/precos"/>
    <xhtml:link rel="alternate" hreflang="x-default" href="https://example.com/en/pricing"/>
  </url>
  <url>
    <loc>https://example.com/de/ueber-uns</loc>
    <lastmod>2026-09-12</lastmod>
    <xhtml:link rel="alternate" hreflang="en" href="https://example.com/en/about"/>
    <xhtml:link rel="alternate" hreflang="de" href="https://example.com/de/ueber-uns"/>
    <xhtml:link rel="alternate" hreflang="pt-BR" href="https://example.com/pt-BR/sobre"/>
    <xhtml:link rel="alternate" hreflang="x-default" href="https://example.com/en/about"/>
  </url>
</urlset>
```

`sitemap-en.xml` と `sitemap-pt-br.xml` は同じ構造で、それぞれ独自の `<loc>` と `lastmod` の値を持ちます。これらのファイルはサイトのルートに配置してください。Google によると、サイトマップの対象となるのは、Search Console で送信しない限り、そのサイトマップが置かれたディレクトリ以下の URL のみです。また、インデックスから参照できるのも、インデックスと同じディレクトリかそれより下の階層にあるサイトマップに限られます。

## Next.js での生成方法

Next.js 14.2 以降では、[`app/sitemap.ts`](https://nextjs.org/docs/app/api-reference/file-conventions/metadata/sitemap#generate-a-localized-sitemap) の各エントリにある `alternates.languages` フィールドをもとに、`xhtml:link` の代替リンクが出力されます。ここで紹介する実装では、ロケールとローカライズ済みスラッグをまとめた 1 つのリストから代替リンクを構築するため、どのエントリにも自身へのリンクと `x-default` が含まれます。これらのリストと、各ロケールで提供するページは `i18n/routing.ts` で管理します。`gt.config.json` を読み込む実装例は、[General Translation のセクション](#how-general-translation-fits)を参照してください。

```ts title="app/sitemap.ts"
import type { MetadataRoute } from 'next';
import {
  BASE_URL,
  DEFAULT_LOCALE,
  LOCALES,
  localeRoutes,
  pathConfig,
} from '@/i18n/routing';

// インデックスさせたいページの共通パス。
const PAGES = ['/', '/about', '/pricing'];

// localeRoutes で除外されていなければ、そのロケールでページを提供する。
const serves = (locale: string, page: string) =>
  !localeRoutes[locale] || localeRoutes[locale].includes(page);

// 特定のロケールにおけるページの公開 URL（ローカライズされたスラッグがあればそれを使用）。
function localeUrl(locale: string, page: string) {
  const path = pathConfig[page]?.[locale] ?? page;
  return `${BASE_URL}/${locale}${path === '/' ? '' : path}`;
}

export default function sitemap(): MetadataRoute.Sitemap {
  return PAGES.flatMap((page) => {
    const locales = LOCALES.filter((locale) => serves(locale, page));

    // <loc> に指定したものを含む、ページの全バージョンと x-default。
    const languages: Record<string, string> = Object.fromEntries(
      locales.map((locale) => [locale, localeUrl(locale, page)]),
    );
    languages['x-default'] = localeUrl(DEFAULT_LOCALE, page);

    // バージョンごとに <url> を 1 つ作成し、それぞれに同じ代替リンクを持たせる。
    return locales.map((locale) => ({
      url: localeUrl(locale, page),
      alternates: { languages },
    }));
  });
}
```

Next.js 16.4.3 と `gt-next` 11.4.16 でビルドしたところ、`/about` ページではこのようなエントリが 4 つ生成され、いずれにも同じ 5 つのリンクが含まれていました：

```xml title="/sitemap.xml"
<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="de" href="https://example.com/de/ueber-uns" />
<xhtml:link rel="alternate" hreflang="fr" href="https://example.com/fr/qui-sommes-nous" />
<xhtml:link rel="alternate" hreflang="pt-BR" href="https://example.com/pt-BR/about" />
<xhtml:link rel="alternate" hreflang="x-default" href="https://example.com/en/about" />
</url>
```

Next.js のドキュメントでは明記されていない点が 3 つあります。

* **自己参照を追加する。** Next.js のドキュメントにあるローカライズの例では、英語の URL の下に `es` と `de` の代替リンクしか記載されておらず、スペイン語とドイツ語の URL には専用のエントリがありません。Google は、各エントリに自分自身も含めて記載し、各バージョンにそれぞれ個別の `<url>` を用意することを想定しています。
* **URL は自分でエスケープする。** 私たちが検証したところ、Next.js 16.4.3 は URL をそのまま XML に書き出しました。クエリ文字列に `&` が含まれていると `xmllint` が受け付けないファイルが生成され、`ü` もエンコードされないままでした。サイトマップの URL にはクエリ文字列を含めず、パスはパーセントエンコードを行う `new URL(path, BASE_URL).href` で組み立ててください。
* **分割してもインデックスは作成されない。** [`generateSitemaps`](https://nextjs.org/docs/app/api-reference/functions/generate-sitemaps) でロケールごとに 1 つの id を返すと、Next.js は各ファイルを `/sitemap/en.xml`、`/sitemap/de.xml` などで配信します。ただし私たちの検証では、この場合 `/sitemap.xml` は 404 を返しました。そのため、各ファイルを Search Console で個別に送信してください。こうすることで、`/sitemap/` 内のファイルに対する Google のディレクトリ制限も解除されます。

## よくあるエラー

* **自己参照または相互リンクの欠落。** `/en/pricing` に `/de/preise` が記載されていても、`/de/preise` に `/en/pricing` が記載されていなければ、Google はそのペアを無視します。これは通常、ロケールを一部のページにだけ追加し、他のページには追加していない場合に起こります。
* **無効なコード。** `UK` は予約済みのコードであるため、`en-UK` は機能しません。`en-GB` を使用してください。`es-419` は BCP 47 としては有効ですが、hreflang ではサポートされていません。`es`、または `es-MX` のように特定の国を指定してください。
* **リダイレクトされる URL。** サーバーが `200` を返す URL を記載してください。上記の例のスラッグでは、`/de/about` は `/de/ueber-uns` にリダイレクトされるため、サイトマップに含めるのはローカライズされたスラッグだけです。
* **`noindex` ページや未翻訳ページ。** サイトマップは、検索結果に表示させたい URL の一覧です。まだ公開していないロケールは含めず、他のエントリの代替リンクからも削除してください。
* **`lastmod` にビルド時の日時を設定している。** サイトマップを再生成しても、コンテンツが変更されたことにはなりません。各バージョンが最後に変更された日付を使用するか、このフィールドを省略してください。

## 送信前の確認方法

Search Console の「サイトマップ」レポートでサイトマップを送信する前、または `robots.txt` に `Sitemap:` 行を追加する前に、次の 3 つのチェックを行ってください。

1. **整形式の XML であること:** `xmllint --noout sitemap-de.xml`
2. **すべてのクラスタが完全で、相互に参照し合っていること。** 次のスクリプトは 1 つ以上のサイトマップファイルを読み込み、自身を列挙していないエントリ、対応するエントリが存在しない代替リンク、セットの不一致、不正な形式のコードを報告します。

   ```python title="check_sitemap.py"
   import re
   import sys
   import xml.etree.ElementTree as ET

   NS = {
       's': 'http://www.sitemaps.org/schemas/sitemap/0.9',
       'x': 'http://www.w3.org/1999/xhtml',
   }
   # 言語 + 任意の文字体系 + 任意のリージョン: es-419 は検出できるが、すべての無効なコードを検出できるわけではない。
   CODE = re.compile(r'^[a-z]{2}(-[a-z]{4})?(-[a-z]{2})?$', re.IGNORECASE)
   RESERVED = ('-uk', '-eu', '-un')

   # すべてのサイトマップファイルからすべての <url> を読み込む: loc -> {hreflang: href}
   entries = {}
   for path in sys.argv[1:]:
       for url in ET.parse(path).getroot().findall('s:url', NS):
           loc = url.findtext('s:loc', namespaces=NS).strip()
           entries[loc] = {
               link.get('hreflang'): link.get('href')
               for link in url.findall('x:link', NS)
               if link.get('rel') == 'alternate'
           }

   problems = []
   for loc, alternates in entries.items():
       if alternates and loc not in alternates.values():
           problems.append(f'{loc} does not list itself')
       for lang, href in alternates.items():
           if lang == 'x-default':
               continue
           if not CODE.match(lang) or lang.lower().endswith(RESERVED):
               problems.append(f'{loc} uses an invalid hreflang code: {lang}')
           if href not in entries:
               problems.append(f'{loc} lists {href}, which has no <url> entry')
           elif entries[href] != alternates:
               problems.append(f'{href} does not list the same alternates as {loc}')

   print('\n'.join(problems) or f'{len(entries)} URLs, no problems found')
   sys.exit(1 if problems else 0)
   ```

   すべてのロケールファイルをまとめて指定して実行します (`python3 check_sitemap.py sitemap-en.xml sitemap-de.xml sitemap-pt-br.xml`) 。上記の例では `6 URLs, no problems found` と出力されます。
3. **すべての URL が `200` を返すこと:**

   ```bash
   curl -s https://example.com/sitemap-de.xml | grep -o '<loc>[^<]*' | sed 's/<loc>//' |
     while read -r url; do echo "$(curl -s -o /dev/null -w '%{http_code}' "$url") $url"; done
   ```

## General Translation の位置づけ

General Translation の `gt-next` は、ロケールごとに専用の URL を割り当てます。多言語サイトマップに列挙するのは、まさにこれらの URL です。[`createNextMiddleware`](/docs/react/nextjs/reference/functions/create-next-middleware) は `/de/...` や `/pt-BR/...` のパスを配信し、`prefixDefaultLocale: true` を指定するとソース言語も `/en/` 配下に配置されます。また、[`pathConfig`](/docs/react/nextjs/reference/functions/create-next-middleware#path-config) はページをローカライズされたスラッグに対応付け、[`localeRoutes`](/docs/react/nextjs/reference/functions/create-next-middleware#locale-routes) は各ロケールで配信するページを制限します。これらの設定は 1 つのモジュールにまとめ、ミドルウェアとサイトマップの両方からインポートしてください。こうすることで、ミドルウェアが応答する URL とサイトマップに列挙される URL が完全に一致します:

```ts title="i18n/routing.ts"
import gtConfig from '../gt.config.json';

export const BASE_URL = 'https://example.com';
export const DEFAULT_LOCALE = gtConfig.defaultLocale;

// gt.config.json から取得した配信対象のすべてのロケール（ソース言語が先頭）。
export const LOCALES = [...new Set([gtConfig.defaultLocale, ...gtConfig.locales])];

// proxy.ts と共有することで、サイトマップにはミドルウェアが配信する URL だけが過不足なく掲載されます。
export const pathConfig: Record<string, Record<string, string>> = {
  '/about': { de: '/ueber-uns', fr: '/qui-sommes-nous' },
};
export const localeRoutes: Record<string, string[]> = {
  'pt-BR': ['/', '/about'], // pricing ページはまだブラジルポルトガル語に翻訳されていません
};
```

```ts title="proxy.ts"
import { createNextMiddleware } from 'gt-next/middleware';
import { localeRoutes, pathConfig } from './i18n/routing';

export default createNextMiddleware({
  prefixDefaultLocale: true,
  pathConfig,
  localeRoutes,
});

export const config = {
  matcher: ['/((?!api|static|.*\\..*|_next).*)'],
};
```

`gt.config.json` に `en`、`de`、`fr`、`pt-BR` を指定した場合、ビルドで生成されたエントリは 11 件でした。3 ページ × 4 ロケールから、ブラジルポルトガル語の料金ページを除いた数です。この URL 自体は訪問者向けには引き続き機能します。ミドルウェアが `/pt-BR/pricing` を `/en/pricing` へ `307` でリダイレクトするためです。ただし、リダイレクトされる URL はサイトマップに含めるべきではありません。掲載したすべての URL が `200` を返し、上記のチェッカーでも問題は検出されませんでした。`serves` チェックはパスを完全一致で比較するため、`localeRoutes` で `/blog/[[...slug]]` のようなパターンを使用している場合は、掲載する個々のページに展開してください。

中国語に `/cn` を使うなど、エイリアスでロケールをルーティングしている場合は、hreflang のキーにはカノニカルなコードを、URL にはエイリアスを使用してください。[ロケールエイリアスの SEO ガイド](/docs/react/nextjs/locale-alias-seo#sitemap)では、[`resolveCanonicalLocale`](/docs/react/nextjs/reference/functions/resolve-canonical-locale) を使ってこれらを変換しています。同じページのタイトルや説明文の翻訳については、[SEO タイトルとメタディスクリプションを翻訳するには?](/kb/translate-seo-metadata)を参照してください。

## 重要なポイント

* 言語版ごとに個別の `<url>` エントリを用意してください。また、hreflang をどの方法で指定する場合でも、インデックスさせたい URL はすべて列挙してください。
* hreflang を含むサイトマップでは、各エントリにそのページ自身を含む全バージョンを列挙し、どのエントリでも同じセットになるようにします。`x-default` は任意です。
* 1 ファイルあたりの上限は 50,000 URL、50MB です。代替リンクは URL 数の上限には含まれませんが、サイズには含まれるため、ロケールが多い場合は先にサイズの上限に達します。
* サイトマップはロケールごとに分割し、大規模なロケールはさらに細かく分割したうえで、サイトのルートに置いたサイトマップインデックスにまとめます。相互参照とステータスコードを確認してから、インデックスを送信してください。
* `gt-next` を使う場合は、ミドルウェアと `app/sitemap.ts` で `pathConfig` と `localeRoutes` を共有し、アプリが実際に配信する URL とサイトマップの内容が正確に一致するようにします。

## Related questions

- [hreflang とは？設定方法を解説](https://generaltranslation.com/ja/kb/what-is-hreflang.mdx): hreflang は、どの URL が同じページの言語版または地域版であるかを Google に伝え、検索結果に適切なページを表示させるための仕組みです。HTML の link タグ、HTTP の Link ヘッダー、またはサイトマップで追加し、すべてのリンクを相互に参照させたうえで、en-UK ではなく en-GB のような有効なコードを使用してください。
- [多言語対応の Next.js アプリで SEO を設定するには？](https://generaltranslation.com/ja/kb/multilingual-nextjs-seo.mdx): 各言語に /en/about や /es/about のようなプレフィックス付きの URL を割り当て、公開している言語ごとに html lang、自己参照のカノニカル、x-default を含む hreflang の代替リンク、翻訳済みのタイトルと説明文、サイトマップのエントリを出力します。未公開のバージョンには noindex を指定します。General Translation の gt-next は各言語を専用の URL にルーティングし、getGT でタイトルを翻訳します。
- [言語リダイレクトと SEO：URL、クッキー、Accept-Language のどれを使うべきか](https://generaltranslation.com/ja/kb/locale-routing-seo.mdx): Googlebot は Accept-Language ヘッダーを送信せず、クロールの大半を米国の IP アドレスから行います。そのため、インデックスさせたいすべてのページの言語は URL で決めましょう。保存済みのクッキーの選択、次いで Accept-Language を使うのは、/ のような中立的なルートのリダイレクト先を決める場合だけにとどめ、その際は一時的な 302 または 307 を使います。
- [多言語SEOにおけるサブディレクトリ・サブドメイン・ccTLDの比較](https://generaltranslation.com/ja/kb/multilingual-url-structure.mdx): 1つのプロダクトでサイトとリリースプロセスを共有するなら、example.com/de/ のようなサブディレクトリを使いましょう。国を示せるのは ccTLD だけで、サブドメインなら個別にホスティングできます。Google は ?lang= パラメータを推奨しておらず、.io や .ai のようなバニティドメインは汎用ドメインとして扱います。
- [SEO用のタイトルとメタディスクリプションはどのように翻訳すればよいですか？](https://generaltranslation.com/ja/kb/translate-seo-metadata.mdx): 各言語版のタイトルとメタディスクリプションは、その言語で、対象市場で実際に検索される語句を使って作成します。こうした語句は直訳とは異なることがよくあります。そのうえで、Open Graph、カノニカル、hreflang、html lang、構造化データなど、関連するタグもローカライズします。General Translation の gt-next では、generateMetadata 内で getGT を使ってタイトルとディスクリプションを翻訳できます。

## Previous and next questions

- Previous question: [YAML のロケールファイルはどのように翻訳すればよいですか？](https://generaltranslation.com/ja/kb/translate-yaml-files.mdx)
- Next question: [翻訳メモリとは？AI時代にもまだ必要なのか](https://generaltranslation.com/ja/kb/what-is-translation-memory.mdx)

## Sitemap

See the [Knowledge Base sitemap](https://generaltranslation.com/ja/kb/sitemap.md) for every article.
