保哥笔记

WordPress 免插件 sitemap 实战指南:动态优先级、image:image、sitemapindex 分页与缓存策略

WordPress 生态里 sitemap 插件多到挑花眼:Yoast SEO、Rank Math、Google XML Sitemaps、All in One SEO 都自带 sitemap 功能,WordPress 5.5+ 还内置了原生 wp-sitemap.xml。但插件大多数把 sitemap 当作附属功能,配置项分散、URL 形态不可控、对超大站点(10 万+ URL)性能差。本文给出基于 wp-blog-header.php 直接生成 sitemap 的免插件方案,可定制 URL 类型组合、按 lastmod 优先级排序、自动分页输出 sitemapindex、支持百度移动 sitemap 命名空间,并扩展到与 WordPress 6.x 原生 sitemap 共存、CDN 缓存策略、与 GSC/百度站长平台的提交细节。

WordPress sitemap 的几种实现路径

路径一:原生 wp-sitemap.xml

WordPress 5.5+ 自动生成 wp-sitemap.xml,访问任意 WP 站点的根域加 /wp-sitemap.xml 都能看到。优点:零配置;缺点:URL 形态固定(每页 2000 条、按文章类型分页),无法自定义优先级、无法过滤特定栏目、无法加百度移动命名空间。

如果你完全没特殊需求,原生 sitemap 够用。本文方案适合需要定制的场景。

路径二:插件方案

Yoast、Rank Math 等插件覆盖了大多数定制需求。月流量 10 万以下的站点用插件最省事。但插件的代价是:增加 PHP 内存占用、与其它插件可能冲突、部分高级功能(lastmod 精确控制)需要付费 Pro 版。

路径三:自定义 PHP 文件(本文方案)

把 sitemap.php 放在站点根目录,require wp-blog-header.php 加载 WordPress 完整环境,然后用 get_posts、get_pages、get_terms 等 WP API 直接生成 XML 输出。优点:完全可控、零依赖、性能可调优;缺点:需要懂 PHP,超大站点要手写分页逻辑。

完整 sitemap.php 代码(增强版)

以下代码放到 WordPress 站点根目录的 sitemap.php:

<?php
/**
 * WordPress 自定义 sitemap 生成器
 * 输出 sitemap.xml 含首页、文章、单页、分类、标签
 */
require __DIR__ . '/wp-blog-header.php';

header('Content-Type: text/xml; charset=UTF-8'); header('HTTP/1.1 200 OK'); header('X-Robots-Tag: noindex, follow', true);

/* 配置 */ $config = [ 'posts_per_page' => 1000, // 单 sitemap 文件最多 URL 数(协议上限 50000) 'include_pages' => true, 'include_categories' => true, 'include_tags' => true, 'include_authors' => false, // 多作者站点可开 'exclude_post_types' => ['attachment', 'revision', 'nav_menu_item'], 'exclude_categories' => [], // 隐藏栏目 term_id 数组 'exclude_tags' => [], ];

echo '<?xml version="1.0" encoding="UTF-8"?>' . "\n"; echo '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" ' . 'xmlns:xhtml="http://www.w3.org/1999/xhtml" ' . 'xmlns:image="http://www.google.com/schemas/sitemap-image/1.1" ' . 'xmlns:mobile="http://www.baidu.com/schemas/sitemap-mobile/1/">' . "\n";

/* 工具函数:输出一条 url */ function output_url($loc, $lastmod = '', $changefreq = 'weekly', $priority = '0.5', $images = []) { echo ' <url>' . "\n"; echo ' <loc>' . esc_url($loc) . '</loc>' . "\n"; if (!empty($lastmod)) { $time = is_numeric($lastmod) ? $lastmod : strtotime($lastmod); echo ' <lastmod>' . gmdate('Y-m-d\TH:i:s+00:00', $time) . '</lastmod>' . "\n"; } echo ' <changefreq>' . $changefreq . '</changefreq>' . "\n"; echo ' <priority>' . $priority . '</priority>' . "\n"; foreach ($images as $img) { echo ' <image:image>' . "\n"; echo ' <image:loc>' . esc_url($img['url']) . '</image:loc>' . "\n"; if (!empty($img['title'])) { echo ' <image:title><![CDATA[' . $img['title'] . ']]></image:title>' . "\n"; } echo ' </image:image>' . "\n"; } echo ' </url>' . "\n"; }

/* 1. 首页 */ $last_modified = get_lastpostmodified('GMT'); output_url(home_url('/'), $last_modified, 'daily', '1.0');

/* 2. 文章 */ $post_types = get_post_types(['public' => true]); $post_types = array_diff($post_types, $config['exclude_post_types']);

$args = [ 'post_type' => $post_types, 'post_status' => 'publish', 'numberposts' => $config['posts_per_page'], 'orderby' => 'modified', 'order' => 'DESC', ]; $myposts = get_posts($args); foreach ($myposts as $post) { setup_postdata($post);

/* 取文章首图 */ $images = []; if (has_post_thumbnail($post->ID)) { $thumb_url = get_the_post_thumbnail_url($post->ID, 'large'); if ($thumb_url) { $images[] = ['url' => $thumb_url, 'title' => get_the_title($post->ID)]; } }

/* 优先级算法:根据更新频率与点击量动态计算 */ $age_days = (time() - get_the_modified_time('U', $post->ID)) / 86400; if ($age_days < 7) { $changefreq = 'daily'; $priority = '0.9'; } elseif ($age_days < 30) { $changefreq = 'weekly'; $priority = '0.7'; } elseif ($age_days < 365) { $changefreq = 'monthly'; $priority = '0.6'; } else { $changefreq = 'yearly'; $priority = '0.4'; }

output_url( get_permalink($post->ID), get_the_modified_time('U', $post->ID), $changefreq, $priority, $images ); } wp_reset_postdata();

/* 3. 单页面 */ if ($config['include_pages']) { $pages = get_pages(['sort_column' => 'post_modified', 'sort_order' => 'desc']); foreach ($pages as $page) { output_url( get_page_link($page->ID), get_post_modified_time('U', false, $page->ID), 'weekly', '0.6' ); } }

/* 4. 分类 */ if ($config['include_categories']) { $cats = get_terms([ 'taxonomy' => 'category', 'hide_empty' => true, 'exclude' => $config['exclude_categories'], ]); if (!is_wp_error($cats)) { foreach ($cats as $cat) { output_url(get_term_link($cat), '', 'weekly', '0.7'); } } }

/* 5. 标签 */ if ($config['include_tags']) { $tags = get_terms([ 'taxonomy' => 'post_tag', 'hide_empty' => true, 'exclude' => $config['exclude_tags'], ]); if (!is_wp_error($tags)) { foreach ($tags as $tag) { output_url(get_term_link($tag), '', 'monthly', '0.4'); } } }

/* 6. 作者 */ if ($config['include_authors']) { $authors = get_users(['who' => 'authors', 'has_published_posts' => true]); foreach ($authors as $author) { output_url(get_author_posts_url($author->ID), '', 'monthly', '0.3'); } }

echo '</urlset>' . "\n"; echo '<!-- Generated at ' . gmdate('c') . ' -->' . "\n";

相比原文方案的改进

  1. 动态优先级:按文章修改时间动态计算 changefreq 与 priority,新文章优先级高、老文章优先级低。Google 抓取调度更友好。
  2. image:image 子标签:每篇文章带上特色图(如有),让 Google 同步索引图片,带 image search 流量。
  3. 多 post_type 支持:自动覆盖所有公开的自定义文章类型(products、portfolio、event 等),不局限于普通 post。
  4. 排除配置:可配置 exclude_categories 与 exclude_tags 跳过隐私栏目(仅会员可见)。
  5. Authors 可选:多作者博客可开启作者页索引;个人博客关闭。
  6. X-Robots-Tag:sitemap 本身 noindex 不进 SERP,避免「sitemap.xml」这个 URL 被搜索引擎索引。
  7. UTF-8 显式声明:避免中文字符在某些编码环境下乱码。

暴露为 /sitemap.xml 的 rewrite 配置

Apache (.htaccess)

在 .htaccess 文件 RewriteBase / 之下加:

RewriteRule ^sitemap\.xml$ /sitemap.php [L]

Nginx

站点配置 server 块内加:

location = /sitemap.xml {
    rewrite ^ /sitemap.php last;
}

避免与原生冲突

WP 5.5+ 自动接管 wp-sitemap.xml 路径。如果你不想走 WP 原生而走自定义,加 functions.php:

/* 关闭 WordPress 原生 sitemap */
add_filter('wp_sitemaps_enabled', '__return_false');

分页 sitemapindex 处理超大站

什么时候需要分页

sitemap 协议规定单文件最多 50000 URL 与 50MB(未压缩)。但实际上为了让 Google 抓取更稳定,建议单文件 5000-10000 URL。10 万 URL 站点必须分页。

分页方案

把 sitemap.php 改造为「不带参数输出 sitemapindex,带 page 参数输出 urlset」:

<?php
require __DIR__ . '/wp-blog-header.php';
header('Content-Type: text/xml; charset=UTF-8');

$page_size = 5000; $page = isset($_GET['page']) ? max(1, intval($_GET['page'])) : 0;

if ($page === 0) { /* 输出 sitemapindex */ $total_posts = wp_count_posts('post')->publish; $total_pages = max(1, (int)ceil($total_posts / $page_size));

echo '<?xml version="1.0" encoding="UTF-8"?>' . "\n"; echo '<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">' . "\n"; for ($p = 1; $p <= $total_pages; $p++) { echo ' <sitemap>' . "\n"; echo ' <loc>' . home_url('/sitemap.xml?page=' . $p) . '</loc>' . "\n"; echo ' <lastmod>' . gmdate('c') . '</lastmod>' . "\n"; echo ' </sitemap>' . "\n"; } /* 单独的 sitemap:分类、标签、单页 */ echo ' <sitemap><loc>' . home_url('/sitemap.xml?type=taxonomy') . '</loc></sitemap>' . "\n"; echo '</sitemapindex>' . "\n"; exit; }

/* 输出某分页的 urlset */ $offset = ($page - 1) * $page_size; $myposts = get_posts([ 'post_type' => 'post', 'post_status' => 'publish', 'numberposts' => $page_size, 'offset' => $offset, 'orderby' => 'modified', 'order' => 'DESC', ]);

echo '<?xml version="1.0" encoding="UTF-8"?>' . "\n"; echo '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">' . "\n"; foreach ($myposts as $post) { echo ' <url>' . "\n"; echo ' <loc>' . esc_url(get_permalink($post->ID)) . '</loc>' . "\n"; echo ' <lastmod>' . gmdate('c', get_post_modified_time('U', false, $post->ID)) . '</lastmod>' . "\n"; echo ' </url>' . "\n"; } echo '</urlset>' . "\n";

性能优化:缓存策略

问题:每次请求都查 DB

1 万文章的 sitemap 单次生成耗时 5-10 秒。如果 Googlebot 高频请求 sitemap.xml(通常每天 5-20 次),CPU 与 DB 压力都不小。

方案 A:文件缓存

给 sitemap.php 加一段:

$cache_file = WP_CONTENT_DIR . '/cache/sitemap_' . md5($_SERVER['QUERY_STRING']) . '.xml';
$cache_ttl = 3600; // 1 小时

if (file_exists($cache_file) && (time() - filemtime($cache_file) < $cache_ttl)) { readfile($cache_file); exit; }

ob_start(); /* ... 原 sitemap 生成逻辑 ... */ $content = ob_get_contents(); file_put_contents($cache_file, $content); ob_end_flush();

方案 B:定时任务生成静态文件

WordPress 有 wp-cron,注册定时任务:

add_action('wp', function() {
    if (!wp_next_scheduled('regenerate_sitemap')) {
        wp_schedule_event(time(), 'hourly', 'regenerate_sitemap');
    }
});
add_action('regenerate_sitemap', function() {
    $url = home_url('/sitemap.php');
    $content = wp_remote_retrieve_body(wp_remote_get($url));
    file_put_contents(ABSPATH . 'sitemap-cache.xml', $content);
});

nginx 优先返回静态 sitemap-cache.xml,没有时回源到 sitemap.php。

方案 C:发文章触发增量更新

挂 hook 到 publish_post:

add_action('publish_post', function() {
    /* 立刻重新生成 sitemap,覆盖缓存 */
    $url = home_url('/sitemap.php');
    $content = wp_remote_retrieve_body(wp_remote_get($url));
    file_put_contents(ABSPATH . 'sitemap-cache.xml', $content);
});

这样新文章发布即刻反映到 sitemap,搜索引擎下次抓取就能拿到。

提交到搜索引擎

Google Search Console

左侧菜单「站点地图」-填入 sitemap.xml-提交。提交后 Google 通常 24-48 小时内开始抓取。GSC 后台能看到「已发现 N 个 URL」「已索引 M 个 URL」的进度。

百度站长平台

百度搜索资源平台 - 资源提交 - sitemap。注意百度对 sitemap 抓取频率较低,提交后 2-3 天才开始。

Bing Webmaster Tools

同 Google 流程。Bing 还能直接 ping:http://www.bing.com/ping?sitemap=https://example.com/sitemap.xml

robots.txt 声明

在 robots.txt 里声明 sitemap 位置:

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

所有遵守 robots.txt 的爬虫(包括 Google、Bing、Yandex)会自动发现。

常见故障

故障 1:sitemap.xml 404

多数是 rewrite 规则没生效。Apache 检查 .htaccess 是否启用(mod_rewrite 装了吗);Nginx 检查 location 规则放置位置(可能被前面的 location 拦截)。

故障 2:XML 解析错误

sitemap.php 输出前有 BOM 或空白字符。检查 require wp-blog-header.php 这一行之前没有任何 echo / print / 空白行。文件保存为 UTF-8 无 BOM。

故障 3:与 wp-sitemap.xml 内容重复

WP 原生 sitemap 与你的自定义 sitemap 同时存在,搜索引擎会同时抓取造成混乱。在 functions.php 里关掉原生:add_filter('wp_sitemaps_enabled', '__return_false');

故障 4:内存溢出

get_posts numberposts=-1 拉所有文章会让 PHP 内存爆炸。改成分批:每次取 1000 条,循环输出,用 wp_reset_postdata 释放对象。或者改用 WP_Query 的 paged 参数迭代。

故障 5:sitemap 里的 URL 是 HTTP 但站点已切 HTTPS

WP siteurl 设置错误。后台「设置-常规」里 WordPress 地址与站点地址都改成 https。或者 functions.php 强制:force_ssl_admin(true);

故障 6:分类页在 sitemap 但站点设了 noindex

SEO 插件(Yoast/Rank Math)的「分类页 noindex」设置不会自动从 sitemap 排除。要在 sitemap.php 里手动判断:

$noindex = get_post_meta($post->ID, '_yoast_wpseo_meta-robots-noindex', true);
if ($noindex == '1') continue;

故障 7:lastmod 时间错乱

WP 默认时区与 UTC 转换没处理好。统一用 GMT 时间:gmdate('c', $time),时区固定 UTC。

常见问题解答

原生 wp-sitemap.xml 与自定义 sitemap 哪个更好?

原生省事但定制能力弱。如果你需要自定义 priority / 加 image / 排除特定栏目,用自定义。否则原生即可。

sitemap 多久更新一次?

用文件缓存方案(1 小时)够新鲜。重要发布触发即时刷新。Googlebot 一般每天访问 sitemap 1-5 次,1 小时缓存能覆盖 90% 抓取请求。

多语言站点怎么做 sitemap?

每个语言一份 sitemap,通过 sitemapindex 列出。每个 url 块加 xhtml:link rel=alternate hreflang 标记其它语言版本。WPML/Polylang 插件有自己的 sitemap 生成器但不一定符合需求,可以参考本文方案魔改。

需要给图片单独建 image-sitemap.xml 吗?

不需要。Google 推荐把图片信息嵌入文章 sitemap 里(image:image 子标签),而不是单独的 image sitemap。本文方案已经做了。

sitemap 提交后多久能在 GSC 看到 URL 全部被抓?

「已发现 N 个 URL」通常 1-2 天显示。「已索引」需要 1-4 周。新站完成全部索引可能 1-3 个月。

能否用 Action 钩子让插件向 sitemap 注入额外 URL?

能。在 sitemap.php 加 do_action:do_action('custom_sitemap_extra_urls');,其它插件可以 add_action 在这里 echo 额外的 url 块。

百度移动 sitemap 命名空间有什么用?

声明 xmlns:mobile 后,可以给某些 URL 加 <mobile:mobile type="mobile" /> 标记为移动版本。百度搜索会按移动适配规则索引。Google 不读这个命名空间。

sitemap 文件超过 50MB 怎么办?

分页 sitemapindex 是首选。次选 gzip 压缩输出 sitemap.xml.gz,Google 与百度都支持。

能否给 sitemap 设访问频率限制?

能。nginx 层 limit_req 限制 sitemap.xml 每 IP 每分钟 10 次。但建议不限,因为正常爬虫都不会超频。

sitemap 里要不要包含已删除文章的 URL?

不要。已删除文章会返回 404,sitemap 里出现死链会让搜索引擎降低对你的信任度。get_posts post_status=publish 已经自动过滤。

因本文不是用Markdown格式的编辑器书写的,转换的页面可能不符合AMP标准。