搜索全站
插件 17 分钟阅读

XML Sitemap 拆分与分页内容收录策略:大站 Sitemap 索引的组织方式

五万篇文章塞进一个 sitemap.xml?服务端超限、爬虫抓不完、更新也跟不上。本文讲清 sitemap 索引的拆分规则、单文件 5 万条/50 MB 限制、按 CPT 与按时间两种拆法,以及 `lastmod` 的正确取值。

U
UFO_PF 2018-09-03 · 阅读 17 分钟
XM 插件
阅读提示
本文约 6,593 字,预计阅读 17 分钟。文中命令与配置均经过实测,可直接参考执行。

引言:一个 42 MB 的 sitemap.xml,加载 19 秒后返回 500

站点过了三万篇文章之后,最常见的硬件式故障不是数据库慢,而是站点地图本身先崩了。Yoast/Rank Math 默认把所有 URL 塞进一组 sitemap 文件,每个 CPT 一份。当月更新频繁时,这个文件越来越大:XML 序列化耗时增加、内存占用上升(simplexml_load_string 对大文件特别吃内存)、CDN 把它当作大文件跳过缓存、Nginx 的 fastcgi_read_timeout 或 PHP 的 max_execution_time 直接把它掐断——于是 Google 隔三差五在 GSC 里报”sitemap 无法读取”。

规范上对 sitemap 有明确的硬性限制(sitemaps.org 协议):单个 sitemap 文件最多 50,000 条 URL,且解压后体积不得超过 50 MB。超过就必须拆分成 sitemap index(索引文件),由索引指向多个子 sitemap。WordPress 核心从 5.5 起自带的 WP_Sitemaps 就是这么干的,它默认每个子 sitemap 装 2000 条 URL(wp_sitemaps_get_max_urls 过滤器可改)。

本文讲怎么把大站的 sitemap 组织得既能被完整抓取、又不会拖垮服务器,以及一个常被忽略的话题:被分页腰斩的内容(<!--nextpage--> 分页文章)该怎么处理。

原理分析:索引文件的三层结构与三种拆分维度

sitemap_index.xml            ← 索引,只列子 sitemap 的 URL 与 lastmod
 ├── post-sitemap1.xml       ← 文章 1-2000
 ├── post-sitemap2.xml       ← 文章 2001-4000
 ├── page-sitemap.xml
 ├── product-sitemap1.xml    ← CPT 独立
 └── image-sitemap1.xml

三种常见拆分维度,各有用武之地:

维度 适用场景 优点 缺点
按内容类型(CPT) 所有站点必做 结构清晰,便于在 GSC 分别观察各类覆盖率 单一类型过大时仍需二级拆
按数量分页 单类型 >5 万条 符合协议上限 URL 变化会随分页移动
按时间(年/月) 资讯/博客类大站 历史数据稳定不变,可强缓存 需要每次发布都重建当月文件

第二个要点是 lastmod 的语义。协议允许的值必须是 W3C Datetime 格式(2026-10-10T08:00:00+08:00),而且它必须真实反映内容的最后修改时间。很多插件错误地把 lastmod 写成当前时间(gmdate('c')),结果搜索引擎每次抓取都认为全站内容刚更新过,抓取预算(crawl budget)被大量浪费在”重新验证其实没变”的页面上。这是一项隐藏但影响巨大的 SEO 损耗。

第三个要点是分页文章内容。<!--nextpage--> 产生的 /2/、/3/ 页面,对于有完整内容价值的文章可以考虑收录;但那些”为了增加 PV 而硬拆”的分页,把一篇文章切成 10 页,每页几百字,属于典型的低质量页面。Google 的重复内容处理策略会将其一部分折叠甚至在索引中丢失。

上一篇文章已处理多份 sitemap 的冲突问题:WordPress 站点地图冲突排查

保姆级实操

步骤 1:确认单份 sitemap 的规模

# 文件大小与 URL 条数(<url> 节点计数)
curl -s https://example.com/post-sitemap.xml -o /tmp/ps.xml
ls -lh /tmp/ps.xml
grep -o '<url>' /tmp/ps.xml | wc -l

# 索引里的子地图数量
curl -s https://example.com/sitemap_index.xml | grep -o '<loc>' | wc -l

只要单文件条数接近 5 万或体积接近 50 MB,就必须拆。

步骤 2:调整 WordPress 核心地图的分页粒度

// 把核心 sitemap 每个子文件的 URL 数从默认 2000 降到 1000,降低单次渲染压力
add_filter( 'wp_sitemaps_max_urls', function( $max_urls, $object_type ) {
    if ( 'post' === $object_type ) {
        return 1000;
    }
    return $max_urls;
}, 10, 2 );

// 剔除不想进地图的内容(比如 landing page CPT)
add_filter( 'wp_sitemaps_post_types', function( $post_types ) {
    unset( $post_types['landing'] );
    return $post_types;
} );

步骤 3:修正 lastmod 取值

不少插件用 Resource tags 或当前时间。自己控制时必须用文章真实修改时间:

add_filter( 'wp_sitemaps_posts_entry', function( $entry, $post ) {
    if ( isset( $entry['lastmod'] ) ) {
        // 用文章真实 GMT 修改时间,而不是 gmdate('c')
        $entry['lastmod'] = mysql2date( 'c', $post->post_modified_gmt, false );
    }
    return $entry;
}, 10, 2 );

步骤 4:按年份/月份拆分(资讯大站的推荐写法)

下面是一个用 WP-CLI 定期生成的静态化方案静态文件比动态生成省资源得多:

#!/usr/bin/env bash
# gen-sitemap.sh —— 按月生成静态 sitemap,cron 每天执行一次
# 用法:0 3 * * * /usr/local/bin/gen-sitemap.sh >/dev/null 2>&1

OUT=/var/www/example.com/public/sitemaps
BASE=https://example.com
mkdir -p "$OUT"
LIMIT=45000            # 留一点余量,别顶到 50000 上限

for ym in $(wp post list --post_type=post --post_status=publish \
              --format=csv --fields=ID,post_date 2>/dev/null \
              | tail -n +2 | cut -d, -f2 | cut -c1-7 | sort -ru); do

    y=${ym%-*}; m=${ym#*-}
    file="$OUT/post-$ym.xml"
    {
        echo '<?xml version="1.0" encoding="UTF-8"?>'
        echo '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">'
        while IFS=, read -r id perm mod; do
            printf '<url><loc>%s</loc><lastmod>%s</lastmod></url>\n' "$perm" "$mod"
        done < <(wp post list --post_type=post --post_status=publish \
                   --year="$y" --monthnum="$((10#${m}))" \
                   --fields=ID,url,post_modified_gmt --format=csv 2>/dev/null | tail -n +2)
        echo '</urlset>'
    } > "$file"
    echo "generated $file"
done

生成的每月文件几乎不变,可以给它们配长缓存(Nginx 里 expires 7d;),这比每次动态渲染节省大量 CPU。

步骤 5:写索引文件

<?php
// gen-index.php:扫描 sitemaps/ 目录,重建 sitemap_index.xml
$out   = __DIR__ . '/public/sitemap_index.xml';
$files = glob( __DIR__ . '/public/sitemaps/*.xml' );
$xml   = '<?xml version="1.0" encoding="UTF-8"?>' . "\n"
       . '<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">' . "\n";
foreach ( $files as $f ) {
    $xml .= sprintf(
        "<sitemap><loc>https://example.com/sitemaps/%s</loc><lastmod>%s</lastmod></sitemap>\n",
        basename( $f ),
        gmdate( 'c', filemtime( $f ) )
    );
}
$xml .= '</sitemapindex>';
file_put_contents( $out, $xml );
echo "index rebuilt with " . count( $files ) . " children\n";

注意:写在 index 里的 child <loc> 必须与 index 本身同源(协议要求),跨域指向会被忽略。

步骤 6:处理被 <!--nextpage--> 分页的文章

原则:不要让所有分页都进 sitemap。合理做法只对确实有独立价值、每页内容完整的长文分页单独提交:

// 只让 karatatsu 型(>3 页且每页内容充实)的文章分页进地图,其余只提交主 URL
add_filter( 'wp_sitemaps_index_entry', function( $entry ) {
    // 示例:不单独提交 comment-page 这类低价值分页
    if ( false !== strpos( $entry['loc'], 'comment-page-' ) ) {
        return array();
    }
    return $entry;
} );

步骤 7:Nginx 层面加缓存与限速保护

location ^~ /sitemap {
    expires 1h;
    add_header Cache-Control "public, max-age=3600";
    # 防止被刷:同一 IP 每分钟最多 30 次
    limit_req zone=sitemap burst=20 nodelay;
}

常见踩坑

  1. 索引指向了 http:// 而站点是 https://:sitemap 协议要求 child 与 index 同协议同 host,跨协议会被”部分忽略”,GSC 里显示”无法抓取”。
  2. lastmod 用 gmdate('c') 导致抓取预算被榨干:见上文,务必用 post_modified_gmt。
  3. 案例:某小说站每天更新 200 章,sitemap 用插件动态生成,文件一度到 38 MB / 4 万多条。Googlebot 每次抓取平均耗时 11 秒并偶发 502,GSC 里 sitemap 状态在”成功 / 无法读取”之间反复横跳,索引量长期卡在 1.2 万。改成按月静态生成 + expires 7d 后:单文件最大降到 1.4 MB、抓取耗时降到 300 ms 以内,三个月后索引量涨到 4.6 万。教训:大站站点地图要走静态化 + 缓存,不要指望 PHP 每次现场序列化几万条数据。
  4. 图片 sitemap 单独却没在 robot-world 声明:图片、视频有各自的 extension 命名空间(<image:loc>、<video:video>),必须在 <urlset> 里带上对应 xmlns,否则会被当作普通 URL 处理。
  5. <?xml 之前出现空格/BOM:有些主题在这个文件输出前 include 了一个带 BOM 的 php 文件,导致 XML 解析失败(”XML declaration allowed only at the start”)。用 curl -s ... | head -c 20 | xxd 检查开头字节。
  6. 把 noindex 的页面还留在 sitemap 中:这是自相矛盾的信号,GSC 会报”已提交但未编入索引 → 已排除 robots meta”。Yoast 会自动剔除,自建方案要手动排除。
  7. 更新后没有 Ping 搜索引擎:虽然 Sitemap: 写在 robots.txt 里会被定期发现,但重要更新建议主动 ping。注意百度与 Google 的 ping 端点已多次变更,优先以平台后台手动提交为准。
  8. CDN 把索引缓存了旧版本:见上面的 expires 1h,不要用极长 TTL 缓存 index 本身(子文件或可以用长 TTL,因为它们几乎不变)。

延伸阅读:WordPress 站点地图冲突排查

总结

大站 sitemap 的三条硬规则:①单文件 ≤50,000 条且 ≤50 MB,超了必须拆索引;②lastmod 必须是内容真实修改时间,写当前时间等于自杀式浪费抓取预算;③优先静态化 + CDN/缓存,别让 PHP 在每次爬虫请求时现场序列化几万条数据。组织方式上:CPT 独立、单类型再按数量或按月份二级拆。至于 <!--nextpage--> 分页这种伪内容,默认不要让它们单独进地图——先把精力放在让主体 URL 被完整索引上。

U
UFO_PF作者

专注 WordPress 生态的实战作者,内容以「可复制、可验证」为第一原则。

留下你的想法

邮箱地址不会被公开。带 * 的为必填项。