Use Cases (更新: 2026/6/3)

Claude Codeでサイトマップを自動生成する実践ガイド

Claude CodeでXMLサイトマップを自動生成し、多言語SEOとSearch Console確認まで整える手順。

Claude Codeでサイトマップを自動生成する実践ガイド

サイトマップは「公開したURLを検索エンジンに整理して渡す」ための台帳

Claude Codeで記事生成やリライトを回していると、本文の品質だけでなく「検索エンジンが新しいページを見つけられる状態か」がすぐ問題になります。特にブログ、ドキュメント、商品ページ、多言語ページを増やす運用では、手作業で sitemap.xml を直すたびに抜け漏れが起きます。

サイトマップは、検索エンジンに登録を保証する魔法のファイルではありません。Google Search Centralの説明どおり、検索結果に出したい正規URLを伝え、更新日や多言語版の関係を補足するためのヒントです。だからこそ、Claude Codeに任せるべき作業は「毎回それっぽいXMLを書くこと」ではなく、公開対象、正規URL、lastmodhreflang、分割ルール、検証手順を同じ基準で生成する仕組み作りです。

この記事では、Astroサイトで公式連携を使う方法と、Node.jsだけで動く自作スクリプトの両方を紹介します。さらに、10言語のような多言語サイト、記事数が増えたサイト、Search Consoleで失敗を見つける運用まで、ClaudeCodeLabの公開前チェックに沿って実務寄りにまとめます。サイトマップをSEO全体の一部として見直す場合は、先に Claude CodeでSEOを改善する実践ガイド も読んでおくと判断しやすくなります。

先に押さえるべき公式仕様

最初に仕様を固定します。ここが曖昧なままClaude Codeへ「サイトマップを作って」と頼むと、古いSEO記事にある priority やping送信をそのまま生成してしまうことがあります。

項目実務での判断
URL相対パスではなく https://example.com/page/ のような絶対URLを書く
文字コードUTF-8で保存し、XML内の &<" などはエスケープする
lastmod本文、構造化データ、重要リンクなどが実際に変わった日だけ更新する
changefreq / prioritysitemaps.orgには要素があるが、Googleは利用しないため省略してよい
ファイル上限1サイトマップは50,000 URLまたは非圧縮50 MBまで
大規模サイト複数ファイルに分割し、サイトマップインデックスをSearch Consoleへ送る
通知方法Googleのサイトマップpingは廃止済み。robots.txt とSearch Consoleで知らせる

参照元は Googleのサイトマップ作成ガイドGoogleのping廃止告知sitemaps.orgのプロトコル です。古いコード例で https://www.google.com/ping?sitemap=... を見かけても、2023年6月26日の告知以降は使わない方針に寄せます。

ユースケース1: Astro公式連携で静的ページを漏れなく出す

Astroで普通のページやブログを静的生成しているなら、まずは公式の @astrojs/sitemap が最短です。Claude Codeへ依頼するときは「Astro公式連携を使い、site を必ず設定し、下書きや検索対象外のURLを除外して」と指定すると、独自XMLを過剰に作る前に安定した形へ寄せられます。

npx astro add sitemap
// astro.config.mjs
import { defineConfig } from 'astro/config';
import sitemap from '@astrojs/sitemap';

export default defineConfig({
  site: 'https://claudecodelab.com',
  integrations: [
    sitemap({
      filter: (page) => !page.includes('/draft/') && !page.includes('/preview/'),
      i18n: {
        defaultLocale: 'ja',
        locales: {
          ja: 'ja',
          en: 'en',
          zh: 'zh-CN',
          ko: 'ko',
          es: 'es',
          fr: 'fr',
          de: 'de',
          pt: 'pt-BR',
          hi: 'hi',
          id: 'id',
        },
      },
    }),
  ],
});

この設定は astro build の出力にサイトマップインデックスと分割ファイルを作ります。Astro公式ドキュメントでも、sitehttp:// または https:// から始まる公開URLとして設定する必要があると説明されています。ローカルの localhost、ステージングURL、末尾スラッシュの揺れが混ざると、Search Consoleでは「送信されたURLが正規URLとして選択されていない」といった調査が増えます。

ただし、公式連携だけでは記事frontmatterの updatedDate をどこまで反映するかはサイト設計に依存します。更新日を厳密に扱いたいメディア、10言語の記事群を同じslugで束ねたいサイト、商品データベースからURLを出したいサイトでは、次のNode.jsスクリプトのように「公開対象の収集」を明示した方が運用しやすくなります。

ユースケース2: Node.jsだけで多言語サイトマップを生成する

次の例は依存パッケージなしで動く generate-sitemap.mjs です。site/src/content/blog を日本語、blog-en を英語、blog-zh を中国語のように持つ構成を想定しています。updatedDate があれば優先し、なければ pubDate、それもなければファイル更新時刻を使います。

// scripts/generate-sitemap.mjs
import { mkdir, readdir, readFile, stat, writeFile } from 'node:fs/promises';
import path from 'node:path';

const SITE_URL = (process.env.SITE_URL ?? 'https://example.com').replace(/\/$/, '');
const OUT_DIR = 'public';
const OUT_FILE = path.join(OUT_DIR, 'sitemap.xml');

const collections = [
  { dir: 'site/src/content/blog', prefix: '/blog', hreflang: 'ja' },
  { dir: 'site/src/content/blog-en', prefix: '/en/blog', hreflang: 'en' },
  { dir: 'site/src/content/blog-zh', prefix: '/zh/blog', hreflang: 'zh-CN' },
  { dir: 'site/src/content/blog-ko', prefix: '/ko/blog', hreflang: 'ko' },
  { dir: 'site/src/content/blog-es', prefix: '/es/blog', hreflang: 'es' },
  { dir: 'site/src/content/blog-fr', prefix: '/fr/blog', hreflang: 'fr' },
  { dir: 'site/src/content/blog-de', prefix: '/de/blog', hreflang: 'de' },
  { dir: 'site/src/content/blog-pt', prefix: '/pt/blog', hreflang: 'pt-BR' },
  { dir: 'site/src/content/blog-hi', prefix: '/hi/blog', hreflang: 'hi' },
  { dir: 'site/src/content/blog-id', prefix: '/id/blog', hreflang: 'id' },
];

function escapeXml(value) {
  return String(value).replace(/[<>&'"]/g, (char) => ({
    '<': '&lt;',
    '>': '&gt;',
    '&': '&amp;',
    "'": '&apos;',
    '"': '&quot;',
  })[char]);
}

async function* walk(dir) {
  let items;
  try {
    items = await readdir(dir, { withFileTypes: true });
  } catch (error) {
    if (error.code === 'ENOENT') return;
    throw error;
  }

  for (const item of items) {
    const fullPath = path.join(dir, item.name);
    if (item.isDirectory()) {
      yield* walk(fullPath);
    } else if (/\.(md|mdx)$/.test(item.name)) {
      yield fullPath;
    }
  }
}

function frontmatterOf(source) {
  return source.match(/^---\n([\s\S]*?)\n---/)?.[1] ?? '';
}

function dateField(frontmatter, key) {
  return frontmatter.match(new RegExp(`^${key}:\\s*["']?(\\d{4}-\\d{2}-\\d{2})`, 'm'))?.[1];
}

function routeSlug(collectionDir, filePath) {
  return path
    .relative(collectionDir, filePath)
    .replace(/\\/g, '/')
    .replace(/\.(md|mdx)$/, '')
    .replace(/\/index$/, '');
}

function encodeRoute(slug) {
  return slug.split('/').map(encodeURIComponent).join('/');
}

async function collectEntries() {
  const bySlug = new Map();

  for (const collection of collections) {
    for await (const filePath of walk(collection.dir)) {
      const source = await readFile(filePath, 'utf8');
      const frontmatter = frontmatterOf(source);
      if (/^draft:\s*true\s*$/m.test(frontmatter)) continue;

      const info = await stat(filePath);
      const slug = routeSlug(collection.dir, filePath);
      const lastmod =
        dateField(frontmatter, 'updatedDate') ??
        dateField(frontmatter, 'pubDate') ??
        info.mtime.toISOString().slice(0, 10);

      const route = `${collection.prefix}/${encodeRoute(slug)}/`;
      const variant = {
        loc: `${SITE_URL}${route}`,
        hreflang: collection.hreflang,
        lastmod,
      };

      const variants = bySlug.get(slug) ?? [];
      variants.push(variant);
      bySlug.set(slug, variants);
    }
  }

  return [...bySlug.values()].flatMap((variants) =>
    variants.map((variant) => ({
      ...variant,
      alternates: variants.map(({ hreflang, loc }) => ({ hreflang, loc })),
    })),
  );
}

function buildSitemap(entries) {
  const urls = entries.map((entry) => `  <url>
    <loc>${escapeXml(entry.loc)}</loc>
    <lastmod>${entry.lastmod}</lastmod>
${entry.alternates.map((alt) => `    <xhtml:link rel="alternate" hreflang="${escapeXml(alt.hreflang)}" href="${escapeXml(alt.loc)}" />`).join('\n')}
  </url>`).join('\n');

  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">
${urls}
</urlset>
`;
}

const entries = await collectEntries();
if (entries.length === 0) {
  throw new Error('サイトマップに出力するURLがありません。公開対象のディレクトリを確認してください。');
}

await mkdir(OUT_DIR, { recursive: true });
await writeFile(OUT_FILE, buildSitemap(entries), 'utf8');
console.log(`${OUT_FILE} に ${entries.length} URLを書き出しました。`);

実行は次の形です。

SITE_URL=https://claudecodelab.com node scripts/generate-sitemap.mjs

このスクリプトは、多言語ページごとに xhtml:link を出します。Googleの多言語ページガイドでは、XMLサイトマップで hreflang を伝える場合、それぞれのURLが自分自身を含む全言語版を列挙する必要があります。つまり、日本語ページだけに英語・中国語へのリンクを書くのではなく、英語ページ側にも同じ対応表を書くのが安全です。

ユースケース3: 記事、商品、ドキュメントを分割して管理する

ページ数が少ないうちは public/sitemap.xml だけで足ります。しかし記事、タグ、商品、ヘルプ、画像付きLPが増えると、1ファイルに詰め込む運用はレビューしづらくなります。上限は50,000 URLまたは非圧縮50 MBですが、実務では45,000 URL程度で余裕を持って分ける方が障害調査が楽です。

<?xml version="1.0" encoding="UTF-8"?>
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <sitemap>
    <loc>https://example.com/sitemap-pages.xml</loc>
    <lastmod>2026-06-03</lastmod>
  </sitemap>
  <sitemap>
    <loc>https://example.com/sitemap-blog.xml</loc>
    <lastmod>2026-06-03</lastmod>
  </sitemap>
  <sitemap>
    <loc>https://example.com/sitemap-products.xml</loc>
    <lastmod>2026-06-03</lastmod>
  </sitemap>
</sitemapindex>

分割の利点は、Search Consoleで「ブログのサイトマップだけ取得できない」「商品だけインデックス率が低い」と切り分けられることです。Claude Codeに実装を頼むなら、「URL種別ごとに分割し、各ファイルの件数と最終更新日をログに出す」「50,000 URLを超える前にチャンク化する」「インデックスには同一サイト上のサイトマップだけを入れる」と依頼すると、保守しやすい出力になります。

robots.txtとSearch Consoleで発見経路を作る

生成しただけでは不十分です。公開URLから取得できる場所に置き、robots.txt とSearch Consoleで知らせます。

User-agent: *
Allow: /

Sitemap: https://claudecodelab.com/sitemap.xml

robots.txt には複数の Sitemap: 行を書けます。通常はサイトマップインデックスのURLを1つ書けば十分です。公開後はGoogle Search Consoleの「サイトマップ」画面に sitemap.xml または sitemap-index.xml を送信し、取得ステータス、検出URL数、エラーを確認します。Bing向けには Bing Webmaster Tools で同じURLを登録します。

ここで古い自動pingスクリプトを残さないことが重要です。Googleのpingエンドポイントは廃止済みで、今は robots.txt とSearch Consoleが基本です。CI/CDに組み込むなら、Claude CodeでCI/CDパイプラインを構築するガイド のように、ビルド後に「サイトマップを生成する」「HTTP 200で取れるか確認する」「XMLとして壊れていないか確認する」までをチェックに入れます。

// scripts/verify-sitemap.mjs
const sitemapUrl = process.env.SITEMAP_URL ?? 'https://example.com/sitemap.xml';
const response = await fetch(sitemapUrl);

if (!response.ok) {
  throw new Error(`サイトマップを取得できません: HTTP ${response.status}`);
}

const xml = await response.text();
if (!xml.includes('<urlset') && !xml.includes('<sitemapindex')) {
  throw new Error('サイトマップXMLのルート要素が見つかりません。');
}

console.log(`${sitemapUrl} を取得できました。サイズ: ${xml.length} bytes`);

よくある失敗例と落とし穴

1つ目は、lastmod を毎ビルドの日時にしてしまうことです。Googleは、実際の重要更新と一致している場合に lastmod をクロール計画の参考にします。本文が変わっていないのに全URLを今日の日付にすると、更新日の信頼性を落とします。

2つ目は、下書き、noindex、リダイレクト元、404予定のURLを混ぜることです。サイトマップには「検索結果に出したい正規URL」だけを入れます。似たURLが複数あるならcanonicalで選んだURLに寄せます。

3つ目は、多言語ページの対応表が片方向になることです。/blog/foo//en/blog/foo/ を指しているのに、英語側が日本語へ戻していないと、hreflang のクラスタが弱くなります。自己参照も忘れないでください。

4つ目は、XMLエスケープ不足です。URLに ?a=1&b=2 がある場合、XML内では &amp; にします。Nodeスクリプトに escapeXml を入れておくと、Claude Codeが生成したURL一覧でも事故が減ります。

5つ目は、サイトマップだけで内部リンクを補えると考えることです。サイトマップは発見の補助であり、読者とクローラが自然にたどれる内部リンクの代替ではありません。関連する記事群を整理するなら、Claude Codeコンテンツファネル監査 の視点で、記事から講座、教材、相談導線までをつないでください。

Claude Codeに依頼するときのプロンプト

ClaudeCodeLabのAstroサイト向けに、XMLサイトマップ生成を実装してください。

条件:
- 公開対象は site/src/content/blog* のMDXだけ
- draft: true、noindex: true の記事は除外
- URLは絶対URLで末尾スラッシュあり
- updatedDate を優先し、なければ pubDate を lastmod にする
- lastmod をビルド日時で一律更新しない
- 10言語の記事は同じslugで hreflang を相互に出す
- XML値はエスケープする
- 50,000 URLまたは50 MBを超える場合は分割し、sitemap indexを作る
- robots.txt とSearch Console登録手順もREADMEに追記
- pingエンドポイントは使わない

実装後に確認すること:
- 生成XMLが well-formed である
- descriptionやdraft記事が混ざっていない
- Search Consoleで送信できるURLになっている

Claude Codeには「書いて」だけでなく「何を除外するか」「更新日の根拠は何か」「多言語の戻りリンクをどう作るか」まで渡すのがコツです。これで疑似コードではなく、運用事故を防ぐ実装に寄せられます。

収益化導線まで含めて設計する

サイトマップは広告収益や講座販売を直接増やすものではありません。ただし、検索に出したいページを整理し、古い薄いページを除外し、重要な教材ページへ内部リンクを集める土台になります。ClaudeCodeLabのようにAdSense品質を守りながら記事を増やすなら、「毎日大量投稿」よりも「1本ずつ検証済みの記事を公開し、サイトマップと内部リンクで見つけやすくする」方が安全です。

実装に慣れてきたら、無料記事から Claude Code導入相談 や教材ページへ自然に進める導線も見直してください。サイトマップで検索流入の入口を整え、記事内CTAで読者の次の行動を用意することで、SEOが単なるアクセス数ではなく収益につながる運用になります。

この記事で紹介した内容を実際に試した結果

MasaがClaudeCodeLabの記事構成でこの手順を試したところ、最も効果があったのは「全URLを今日更新にしない」「10言語の同一slugを hreflang で相互に出す」「pingを削除してSearch Console確認に寄せる」の3点でした。特に公開済み記事をリライトした場合、updatedDatelastmod が一致していると、レビュー時にどの言語版が更新済みか確認しやすくなります。サイトマップ生成は一度作れば終わりではなく、公開前レビュー、CI/CD、Search Console確認まで含めて運用して初めて価値が出ました。

#Claude Code #サイトマップ #SEO #XML #自動化
無料

無料PDF: Claude Code はじめてのチートシート

まずは無料PDFで基本コマンドと最初の使い方をまとめて確認してください。登録後はそのままテンプレート集や導入相談にも進めます。

スパムは送りません。登録情報は厳重に管理します。

Claude Codeを仕事で使える形にしませんか?

無料PDFで基礎を固めたあと、すぐ使えるテンプレート集で試し、必要なら業務自動化や導入相談まで進められます。

Masa

この記事を書いた人

Masa

Claude Codeの実務活用、導入設計、収益導線改善を検証しているエンジニア。10言語の技術メディアを運営中。

PR

関連書籍・参考図書

この記事のテーマに関連する書籍を楽天ブックスで探せます。

※ 当サイトは楽天市場のアフィリエイトプログラムに参加しています。上記リンクから商品をご購入いただくと、運営者に紹介料が支払われる場合があります。