Next.js の App Router では、サイトマップ(sitemap.xml)と robots.txt を、専用の規約ファイル app/sitemap.ts と app/robots.ts を置くだけで自動生成できます。XML を手書きしたり、別途プラグインを入れたりする必要はありません。この記事では、静的なサイトマップの基本形から、データベースや API と連携した動的なサイトマップ、robots.txt のルール設定、生成結果の確認方法、そして大量の URL を扱うときの分割方法まで、実際に動くコードとあわせて解説します。対象は Next.js の App Router を使っている初〜中級の方です。
目次
なぜ sitemap.xml と robots.txt を用意するのか
検索エンジンは、サイト内のリンクをたどりながらページを見つけ(クロール)、その内容を検索結果に登録します(インデックス)。このとき役立つのが sitemap.xml と robots.txt です。
sitemap.xml は「このサイトにはこんな URL があります」という一覧を検索エンジンに伝えるファイルです。リンクをたどるだけでは見つかりにくいページや、追加したばかりのページも、サイトマップに載せておけばクロールしてもらいやすくなります。一方 robots.txt は「どのページをクロールしてよいか/してほしくないか」を伝えるファイルで、管理画面やプレビュー用のページなど、検索結果に出したくない領域を除外するのに使います。
App Router では、これらを app ディレクトリ直下の規約ファイルとして書けます。ファイルを置くと Next.js が自動でルート(/sitemap.xml と /robots.txt)を用意し、内容を生成してくれるのが特徴です。
app/sitemap.ts でサイトマップを生成する
まずは静的なサイトマップです。app/sitemap.ts を作り、MetadataRoute.Sitemap 型の配列を返す default 関数を書きます。型は next から import type で読み込みます。
import type { MetadataRoute } from 'next';
export default function sitemap(): MetadataRoute.Sitemap {
return [
{
url: 'https://example.com',
lastModified: new Date(),
changeFrequency: 'yearly',
priority: 1,
},
{
url: 'https://example.com/about',
lastModified: new Date(),
changeFrequency: 'monthly',
priority: 0.8,
},
{
url: 'https://example.com/blog',
lastModified: new Date(),
changeFrequency: 'weekly',
priority: 0.5,
},
];
}
配列の各要素が1つの URL を表します。それぞれのフィールドの意味は次のとおりです。url 以外は省略でき、検索エンジンへのヒントとして働きます。
| フィールド | 意味 |
|---|---|
url | ページの URL。絶対 URLで書く(必須)。 |
lastModified | 最終更新日時。Date か ISO 形式の文字列。 |
changeFrequency | 更新頻度の目安。always / hourly / daily / weekly / monthly / yearly / never のいずれか。 |
priority | サイト内での相対的な重要度。0.0〜1.0 で指定。 |
url は必ず https:// から始まる絶対 URL で書く点に注意してください。相対パス(/about など)ではサイトマップとして正しく機能しません。ドメインは環境変数などにまとめておくと、本番と開発で切り替えやすくなります。
データベースや API から動的にサイトマップを作る
ブログや商品ページのように件数が増えていくサイトでは、URL を手書きするのは現実的ではありません。そんなときは default 関数を async にして、データベースや API から一覧を取得し、その結果から URL を組み立てます。返り値の型は Promise<MetadataRoute.Sitemap> になります。
import type { MetadataRoute } from 'next';
const BASE_URL = 'https://example.com';
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
// API やデータベースから記事一覧を取得する
const posts: { slug: string; updatedAt: string }[] = await fetch(
`${BASE_URL}/api/posts`
).then((res) => res.json());
// 記事ごとの URL エントリを作る
const postEntries: MetadataRoute.Sitemap = posts.map((post) => ({
url: `${BASE_URL}/blog/${post.slug}`,
lastModified: new Date(post.updatedAt),
changeFrequency: 'weekly',
priority: 0.7,
}));
// 固定ページと動的ページをまとめて返す
return [
{
url: BASE_URL,
lastModified: new Date(),
changeFrequency: 'yearly',
priority: 1,
},
...postEntries,
];
}
固定ページのエントリと、map で作った記事ページのエントリをスプレッド構文でまとめて返しています。記事が増えても、この関数がそのつど最新の一覧を取り込むので、サイトマップに反映されます。データ取得を含むため、後述するように動的に生成される点も押さえておきましょう。
app/robots.ts で robots.txt を生成する
次は robots.txt です。app/robots.ts を作り、MetadataRoute.Robots 型のオブジェクトを返す default 関数を書きます。rules にクロールの許可・不許可を、sitemap にサイトマップの場所を指定します。
import type { MetadataRoute } from 'next';
export default function robots(): MetadataRoute.Robots {
return {
rules: {
userAgent: '*',
allow: '/',
disallow: '/admin/',
},
sitemap: 'https://example.com/sitemap.xml',
};
}
userAgent はルールを適用するクローラーの名前で、'*' はすべてのクローラーを表します。allow でクロールを許可するパス、disallow で禁止するパスを指定します。この例では、サイト全体を許可しつつ /admin/ だけを除外しています。sitemap フィールドに絶対 URL を書いておくと、robots.txt からサイトマップの場所も伝えられます。
クローラーごとに違うルールを設けたい場合は、rules を配列にします。userAgent や allow / disallow には配列も渡せます。
import type { MetadataRoute } from 'next';
export default function robots(): MetadataRoute.Robots {
return {
rules: [
{
userAgent: 'Googlebot',
allow: ['/'],
disallow: ['/private/'],
},
{
userAgent: ['Applebot', 'Bingbot'],
disallow: ['/'],
},
],
sitemap: 'https://example.com/sitemap.xml',
};
}
この例では、Googlebot にはサイト全体を許可しつつ /private/ を禁止し、Applebot と Bingbot にはサイト全体を禁止しています。なお robots.txt はあくまでクローラーへの「お願い」であり、非公開にすべき情報を隠す仕組みではありません。見せたくないページはアクセス制御など別の手段で守る必要があります。
生成された sitemap.xml と robots.txt を確認する
ファイルを置いたら、開発サーバーを起動して http://localhost:3000/sitemap.xml と http://localhost:3000/robots.txt にアクセスしてみましょう。sitemap.ts / robots.ts という拡張子でも、公開されるパスは .xml / .txt になります。先ほどの静的なサイトマップなら、次のような XML が生成されます。
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://example.com</loc>
<lastmod>2026-07-27T00:00:00.000Z</lastmod>
<changefreq>yearly</changefreq>
<priority>1</priority>
</url>
</urlset>
関数の中でデータ取得などの動的な処理をしていない場合、Next.js はビルド時にこれらのファイルを生成し、静的なファイルとして配信します。一方、先ほどの動的サイトマップのように fetch でデータを取得したり、cookies などのリクエスト情報を参照したりすると、リクエストごとに動的に生成されるようになります。頻繁に更新されるサイトマップでも、アクセス時点の最新データが反映されるということです。ビルド時に固定したいのか、常に最新にしたいのかで、関数の中身(データ取得の有無やキャッシュ設定)を選ぶとよいでしょう。
大量の URL は generateSitemaps で分割する
サイトマップの仕様では、1ファイルに含められる URL は最大 5 万件までと決められています。商品数の多い EC サイトなど、この上限を超えそうな場合は、generateSitemaps という関数を使ってサイトマップを複数ファイルに分割します。
import type { MetadataRoute } from 'next';
const BASE_URL = 'https://example.com';
// 生成するサイトマップの一覧(id を割り振る)
export async function generateSitemaps() {
return [{ id: 0 }, { id: 1 }, { id: 2 }, { id: 3 }];
}
// id ごとに担当範囲の URL を返す
export default async function sitemap({
id,
}: {
id: number;
}): Promise<MetadataRoute.Sitemap> {
const start = id * 50000;
const end = start + 50000;
const products = await getProducts(start, end);
return products.map((product) => ({
url: `${BASE_URL}/product/${product.id}`,
lastModified: product.updatedAt,
}));
}
generateSitemaps が返した各 id に対して default 関数が呼ばれ、それぞれが /product/sitemap/0.xml、/product/sitemap/1.xml … のような URL で配信されます。default 関数は id を引数で受け取れるので、そこから担当するデータの範囲を計算して返します。分割したサイトマップは、通常サイトマップインデックス(複数のサイトマップをまとめたファイル)から参照されます。
まとめ
Next.js の App Router では、app/sitemap.ts と app/robots.ts を置くだけで sitemap.xml と robots.txt を自動生成できます。サイトマップは MetadataRoute.Sitemap を返す関数で、url / lastModified / changeFrequency / priority を指定します。url は必ず絶対 URL にすること、記事や商品のように件数が増えるものは async 関数でデータを取得して動的に組み立てることがポイントです。robots.txt は MetadataRoute.Robots を返す関数で、rules の userAgent / allow / disallow と、sitemap フィールドを設定します。URL が 5 万件を超えるときは generateSitemaps で分割しましょう。データ取得の有無によってビルド時生成とリクエスト時生成が切り替わることも覚えておくと、更新頻度に合わせた設計ができます。