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

用 Claude Code 自动生成 XML 站点地图

用Claude Code生成Astro与Node站点地图,处理hreflang、lastmod、robots.txt和Search Console。

用 Claude Code 自动生成 XML 站点地图

站点地图是给搜索引擎看的公开URL清单

用 Claude Code 批量维护文章、文档或商品页时,最容易被忽略的不是页面能不能渲染,而是搜索引擎能不能稳定发现这些页面。XML站点地图会告诉Google哪些是你希望出现在搜索结果中的规范URL、这些页面什么时候有过重要更新,以及不同语言版本之间是什么关系。

它不是收录保证。Google的当前文档明确说明,Google会忽略 prioritychangefreq,只有在 lastmod 长期真实可信时才会参考它。旧的站点地图ping端点也已经废弃,所以现代流程应该使用 robots.txt、Google Search Console 提交和部署后的验证,而不是继续请求 https://www.google.com/ping?sitemap=...

本文会介绍两种可落地的做法:Astro官方站点地图集成,以及一个只依赖Node.js内置模块的多语言MDX生成脚本。若你正在整理整体SEO流程,可以一起参考 Claude Code SEO优化Claude Code CI/CD设置

先固定官方规则

项目实务判断
URL使用 https://example.com/blog/post/ 这样的绝对URL,不使用相对路径
文件上限单个站点地图最多50,000个URL,或未压缩50 MB
编码使用UTF-8,并对XML中的URL值做转义
lastmod只在正文、结构化数据或重要链接真实变化时更新
priority / changefreqGoogle不使用,可以省略
多语言页面每个URL都要列出自己和所有替代语言版本
提交方式使用 robots.txt 和Search Console,不保留ping脚本

建议收藏的官方资料包括 Google站点地图指南Google站点地图ping废弃公告Google多语言页面指南sitemaps.org协议

场景1:Astro页面和博客路由

普通的Astro静态站点可以先使用官方集成。它会在 astro build 时生成站点地图,并且可以在路由结构稳定时输出本地化URL关系。

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',
        },
      },
    }),
  ],
});

这里最常见的失败是 site 写错。不要让 localhost、预览域名、httphttps 混在生成结果里。Google会按站点地图里的URL抓取,所以这里必须和你页面上的canonical URL保持一致。

场景2:用Node.js生成多语言MDX站点地图

当内容分布在 blogblog-enblog-zh 等集合里,或者你希望把frontmatter里的 updatedDate 精确转成 lastmod 时,自定义脚本更好维护。下面的脚本只使用Node.js内置模块,并写出 public/sitemap.xml

// 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(`已将 ${entries.length} 个URL写入 ${OUT_FILE}。`);

运行方式如下:

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

场景3:文章、商品和文档分开管理

小型博客可以只有一个 sitemap.xml。页面变多后,最好按文章、静态页面、商品或帮助文档拆分。这样不仅能避开50,000 URL和50 MB上限,也能在Search Console里快速判断哪一类页面出现抓取问题。

<?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>

让 Claude Code 实现时,可以明确要求:每个文件输出URL数量日志,接近上限前自动分块,站点地图索引只引用同一站点上的文件。

robots.txt、Search Console和部署验证

公开后,在 robots.txt 写入站点地图地址:

User-agent: *
Allow: /

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

然后在Google Search Console的“站点地图”页面提交一次。部署检查可以确认公开URL返回HTTP 200,并且响应内容确实像站点地图。

// 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`);

发布前要抓住的失败例

第一,不能把所有 lastmod 都设成构建日期。页面没有重要修改却每天变成今天,会削弱更新信号的可信度。

第二,不要把草稿、noindex 页面、重定向来源、重复URL放进站点地图。站点地图应该只放希望进入搜索结果的规范URL。

第三,多语言关系不能只写单向链接。每个语言版本都要列出自己和所有替代版本,否则 hreflang 簇容易不完整。

第四,URL必须做XML转义。带查询参数的 & 在XML里要写成 &amp;,否则Search Console可能报告解析错误。

收益化和实际结果

站点地图本身不会直接带来广告收入,但它能保护可变现页面的发现路径,例如教程、对比页、免费资料和咨询页。修好站点地图后,也要检查内部链接和CTA,让读者能从免费文章自然进入 Claude Code培训 或相关资源。

Masa在ClaudeCodeLab的流程里实际验证后,最有价值的改动是删除旧ping脚本、让 lastmod 跟随 updatedDate、并用相互的 hreflang 绑定10个语言版本。这样编辑审核时能直接用MDX frontmatter确认哪个语言版本已经更新,Search Console里的排查也更清楚。

#Claude Code #站点地图 #SEO #XML #自动化
免费

免费 PDF: Claude Code 速查表

输入邮箱即可获取一页 PDF,整理常用命令、审查习惯和安全工作流。

我们会妥善保护你的信息,不发送垃圾邮件。

把 Claude Code 变成真正能带来结果的工作流

先领取中文说明的免费 PDF,再进入英文商品页选择合适的教材。如果你需要团队落地、流程设计或内容变现支持,也可以直接咨询。

Masa

关于作者

Masa

专注 Claude Code 实务流程、团队导入和内容转化的工程师。