動的サイトマップの実装
Next.js App Router では src/app/sitemap.ts を作成するだけで、自動的に /sitemap.xml が生成されます。
Webサイトを公開しても、検索エンジンに正しくページを認識してもらえなければ、検索結果には表示されない。特に動的にページが生成されるサイトでは、Google Search Consoleから手動で1ページずつインデックス登録をリクエストするのは現実的ではない。
この問題を解決するのが robots.txt と sitemap.xml だ。Next.jsのApp Routerには、この2つのファイルをTypeScriptで簡単に生成できる仕組みが用意されている。サードパーティのパッケージは不要で、規約通りのファイルをappディレクトリに置くだけで動作する。
この記事では、robots.txtとsitemap.xmlがそもそも何なのかという基礎から、Next.js App Routerでの具体的な実装方法までを解説する。
robots.txtとは何か
robots.txtは、検索エンジンのクローラー(Webページを自動巡回するプログラム)に対して「このサイトのどこをクロールしていいか、どこをクロールしないでほしいか」を伝えるためのテキストファイルだ。サイトのルート(https://example.com/robots.txt)に配置する。
クローラーはサイトにアクセスするとき、最初にこのファイルを確認する。たとえば管理画面やAPIエンドポイント、ログインページなど、検索結果に載せる意味のないページはrobots.txtでクロール対象外にできる。
ただし注意点がある。robots.txtはあくまで「お願い」であり、強制力はない。Googlebotのような主要なクローラーは従うが、悪意のあるボットは無視する可能性がある。アクセス制御が目的なら、認証などの別の手段を使う必要がある。
sitemap.xmlとは何か
sitemap.xmlは、サイトに存在するページの一覧をXML形式でまとめたファイルだ。各ページのURL、最終更新日などの情報を含めることができ、クローラーに「このサイトにはこれだけのページがあります」と明示的に伝える役割を持つ。
クローラーは通常、ページ内のリンクを辿ってサイト内のページを発見する。しかし、リンク構造が複雑だったり、DBのデータから動的に生成されるページが多い場合、すべてのページをリンク経由で発見できるとは限らない。サイトマップを用意しておけば、クローラーがページを見落とすリスクを減らせる。
Google Search Consoleでサイトマップを登録しておくと、Googleが定期的にサイトマップを確認しに来る。新しいページを追加すれば、次にサイトマップが再生成されたタイミングで自動的にURLが追加され、手動でのインデックス登録リクエストが不要になる。
Next.js App Routerでの robots.ts の書き方
App Routerでは、app/robots.tsファイルを作成し、MetadataRoute.Robots型のオブジェクトを返す関数をデフォルトエクスポートするだけでいい。Next.jsが自動的に/robots.txtとしてルーティングしてくれる。
// app/robots.ts
import type { MetadataRoute } from "next";
const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL || "https://example.com";
export default function robots(): MetadataRoute.Robots {
return {
rules: {
userAgent: "*",
allow: "/",
disallow: ["/api/", "/admin/", "/login/"],
},
sitemap: `${SITE_URL}/sitemap.xml`,
};
}
これで /robots.txt にアクセスすると、以下のような内容が返される。
User-agent: *
Allow: /
Disallow: /api/
Disallow: /admin/
Disallow: /login/
Sitemap: https://example.com/sitemap.xml
各フィールドの意味は以下の通りだ。
userAgent: "*" はすべてのクローラーを対象にするという意味。allow: "/" でサイト全体のクロールを許可し、disallow で除外したいパスを配列で指定する。最後のsitemapは、クローラーにサイトマップの場所を知らせるためのもの。
特定のクローラーだけ別のルールを適用したい場合は、rulesを配列にして複数のルールを定義できる。
rules: [
{
userAgent: "Googlebot",
allow: "/",
disallow: "/admin/",
},
{
userAgent: "GPTBot",
disallow: "/",
},
],
この例では、Googlebotには/admin/以外をクロール許可し、OpenAIのGPTBotにはサイト全体のクロールを拒否している。
Next.js App Routerでの sitemap.ts の書き方
app/sitemap.tsを作成し、MetadataRoute.Sitemap型の配列を返す関数をデフォルトエクスポートする。/sitemap.xmlとして自動的にルーティングされる。
静的なサイトマップ
ページ数が少なく固定のサイトなら、URLを直接書くだけで済む。
// app/sitemap.ts
import type { MetadataRoute } from "next";
const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL || "https://example.com";
export default function sitemap(): MetadataRoute.Sitemap {
return [
{
url: SITE_URL,
lastModified: new Date(),
},
{
url: `${SITE_URL}/about`,
lastModified: new Date(),
},
{
url: `${SITE_URL}/blog`,
lastModified: new Date(),
},
];
}
動的なサイトマップ
ブログやECサイトのように、DBのデータからページが動的に生成されるサイトでは、サイトマップもデータに応じて動的に生成する必要がある。関数をasyncにして、API呼び出しやDB問い合わせの結果を元にURL一覧を構築する。
// app/sitemap.ts
import type { MetadataRoute } from "next";
const API_BASE_URL = process.env.API_BASE_URL || "http://localhost:3000";
const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL || "https://example.com";
async function getPosts() {
try {
const res = await fetch(`${API_BASE_URL}/api/posts`, {
next: { revalidate: 3600 },
});
if (!res.ok) return [];
return await res.json();
} catch {
return [];
}
}
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const posts = await getPosts();
const staticPages: MetadataRoute.Sitemap = [
{
url: SITE_URL,
lastModified: new Date(),
},
{
url: `${SITE_URL}/about`,
lastModified: new Date(),
},
];
const postPages: MetadataRoute.Sitemap = posts.map((post: any) => ({
url: `${SITE_URL}/blog/${post.slug}`,
lastModified: new Date(post.updatedAt),
}));
return [...staticPages, ...postPages];
}
next: { revalidate: 3600 } はNext.jsのfetchキャッシュ機能で、APIレスポンスを1時間キャッシュする指定だ。サイトマップへのリクエストのたびにAPIやDBへアクセスするのを避けられる。
lastModified、changeFrequency、priorityについて
サイトマップの各URLには、lastModifiedの他にchangeFrequencyとpriorityというフィールドを設定できる。
{
url: `${SITE_URL}/blog`,
lastModified: new Date(),
changeFrequency: "weekly",
priority: 0.8,
}
ただし、GoogleはchangeFrequencyとpriorityを無視すると公式に明言している。Google公式ドキュメントにはっきり「Google ignores <priority> and <changefreq> values」と書かれており、GoogleのGary Illyes氏はこれらの値を「bag of noise(ノイズの塊)」と呼んでいる。
Googleが実際に使うのはlastModified(<lastmod>)だけだ。ただし、lastModifiedもコンテンツの実際の変更日と一致していなければ信頼されない。ページを更新していないのにnew Date()で常に現在日時を入れると、Googleに「このlastmodは信頼できない」と判断される可能性がある。理想的には、DBレコードのupdatedAtのような実際の更新日時を使うべきだ。
Bing等の他の検索エンジンはこれらの値を参照する可能性はあるので、設定しておくこと自体は無駄ではない。ただ、SEO的な効果をGoogleに期待するならlastModifiedを正確に設定することに集中した方がいい。
ページ数が多い場合の対応
Googleのサイトマップ仕様では、1つのサイトマップファイルに含められるURLは最大50,000件、ファイルサイズは50MBまでという制限がある。ページ数が多いサイトでは、サイトマップを分割する必要が出てくる。
Next.jsではgenerateSitemaps関数を使ってサイトマップを分割できる。
// app/sitemap.ts
import type { MetadataRoute } from "next";
export async function generateSitemaps() {
return [{ id: 0 }, { id: 1 }, { id: 2 }];
}
export default async function sitemap(
props: { id: Promise<string> }
): Promise<MetadataRoute.Sitemap> {
const id = await props.id;
const start = Number(id) * 50000;
const end = start + 50000;
const products = await getProducts(start, end);
return products.map((product) => ({
url: `https://example.com/products/${product.slug}`,
lastModified: new Date(product.updatedAt),
}));
}
この場合、/sitemap/0.xml、/sitemap/1.xml、/sitemap/2.xmlのようにURLが自動的に生成される。
Google Search Consoleへの登録
robots.txtとsitemap.xmlを作成したら、Google Search Consoleでサイトマップを登録しておく。「サイトマップ」セクションでhttps://example.com/sitemap.xmlを送信するだけでいい。
一度登録すれば、Googleが定期的にサイトマップを確認しに来るようになる。新しいページが追加されるたびにサイトマップに自動反映されるので、手動でインデックス登録をリクエストする必要はなくなる。
ただし、サイトマップを登録したからといってすべてのページが必ずインデックスされるわけではない。Googleはサイトマップを「ヒント」として扱い、最終的にどのページをインデックスするかはGoogle自身のアルゴリズムが判断する。
まとめ
robots.txtとsitemap.xmlは、検索エンジンにサイトの構造を伝えるための基本的な仕組みだ。Next.js App Routerではapp/robots.tsとapp/sitemap.tsを置くだけで生成でき、サードパーティパッケージも不要。動的にページが増えるサイトでは、手動でのインデックス登録リクエストから解放されるだけでも導入する価値がある。
https://nextjs.org/docs/app/api-reference/file-conventions/metadata/sitemap