Discuz 门户 sitemap 免插件实现:portal_rss.php 改造、分页 sitemapindex 与缓存策略完整教程

Discuz 门户 sitemap 用插件存在升级失效、性能压顶、白名单缺失三大痛点。本文给出基于官方 portal_rss.php 复制改造的免插件方案,覆盖 X3.4 与 X3.5 版本,并扩展到分页 sitemapindex、文件缓存、定时静态生成、image sitemap、与板块 forum 联动等实战配置。

更新 34 分钟阅读 2,848 阅读

Discuz 门户(portal)的站点地图绝大多数运营者都装了第三方插件,但插件的稳定性参差不齐,每次 Discuz 升级补丁都可能导致 sitemap 输出乱码或者 500。其实 Discuz 自带的 portal_rss.php 已经有一份「按时间倒序输出门户文章」的成熟代码,把它改两段就能直接产出符合 sitemap.xml 协议的 XML。这种改法的好处是:一次性、不依赖插件、官方任何升级都不会冲掉你的代码(因为是新文件 portal_sitemap.php),代码维护成本接近于零。本文给出 Discuz X3.4 / X3.5 两个主流版本的完整改造步骤,并补充缓存、分页、调用频率、提交搜索引擎、与板块 sitemap 联动等实战配置。

为什么用 RSS 改 sitemap 而不是用插件

插件方案的三个常见痛点

市面上常见的 Discuz sitemap 插件大概有四五款,免费的几款我都装过:dz_sitemap、Discuz! 站点地图静态生成器、Pocketgo、UC Sitemap。运营两三个月后回头看,痛点集中在:

  • 升级失效。Discuz 官方版本号从 X3.2 演化到 X3.5 中间更新了好几版数据库表结构(特别是 forum_thread.dateline、portal_article.aid 等字段的索引调整),插件需要跟着更新但作者已不维护。
  • 性能压顶。多数插件在每次访问 sitemap.xml 时实时遍历 forum_thread 与 portal_article,几十万行的论坛 30 秒生成不完,被 Googlebot 抓的时候还触发 504。
  • 白名单缺失。插件默认输出全部帖子链接,连同回收站里的、待审的、隐藏板块的都进去,被搜索引擎抓到一堆 404。

RSS 改 sitemap 的优势

portal_rss.php 是 Discuz 官方代码的一部分,权限校验、字符集处理、URL 重写、catid 过滤这套逻辑都已经写好。你只需要把输出格式从 RSS 2.0 的 <item> 块换成 sitemap 协议的 <url> 块,逻辑层零修改。这一改完之后跟着 Discuz 升级都不会受影响——因为你新建的是 portal_sitemap.php 文件,官方覆盖不到它;只有 portal.php 入口加的一行白名单是个微小的合并冲突点,每次升级补一下即可。

完整代码改造步骤(适用于 X3.2 - X3.4)

第一步:在 portal.php 入口加 sitemap 路由

编辑站点根目录下的 portal.php,搜索这一行:

if(empty($_GET['mod']) || !in_array($_GET['mod'], array('list', 'view', 'comment', 'portalcp', 'topic', 'attachment', 'rss', 'block'))) $_GET['mod'] = 'index';

把数组里加上 'sitemap':

if(empty($_GET['mod']) || !in_array($_GET['mod'], array('list', 'view', 'comment', 'portalcp', 'topic', 'attachment', 'rss', 'block', 'sitemap'))) $_GET['mod'] = 'index';

这一步告诉 Discuz「mod=sitemap 是合法路由」,否则 portal.php 会把它默认改成 index 走首页流程。

第二步:复制 portal_rss.php 为 portal_sitemap.php

定位到 source/module/portal/portal_rss.php,复制一份重命名为 portal_sitemap.php,放在同目录下。Discuz 的模块路由会基于 mod 参数自动找 source/module/portal/portal_{mod}.php 文件,所以新文件名必须严格匹配。

第三步:替换 RSS 头为 sitemap urlset

编辑 portal_sitemap.php,定位到这段输出 RSS 头的代码:

echo "<?xml version=\"1.0\" encoding=\"".$charset."\"?>\n".
"<rss version=\"2.0\">\n".
" <channel>\n".
(count($catarray) > 1 ?
" <title>{$_G[setting][bbname]}</title>\n".
" <link>{$_G[siteurl]}forum.php</link>\n".
" <description>Latest $num articles of all categories</description>\n"
:
" <title>{$_G[setting][bbname]} - $catname</title>\n".
" <link>{$_G[siteurl]}portal.php?mod=list&catid=$rsscatid</link>\n".
" <description>Latest $num articles of $catname</description>\n"
).
" <copyright>Copyright(C) {$_G[setting][bbname]}</copyright>\n".
" <generator>discuz! Board by Comsenz Inc.</generator>\n".
" <lastBuildDate>".gmdate('r', TIMESTAMP)."</lastBuildDate>\n".
" <ttl>$ttl</ttl>\n".
" <image>\n".
" <url>{$_G[siteurl]}static/image/common/logo_88_31.gif</url>\n".
" <title>{$_G[setting][bbname]}</title>\n".
" <link>{$_G[siteurl]}</link>\n".
" </image>\n";

整段替换为:

echo "<?xml version=\"1.0\" encoding=\"".$charset."\"?>\n".
"<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\n";

注意 xmlns 命名空间不能省略,否则部分搜索引擎(特别是百度)会报「不是合法 sitemap」。

第四步:替换文章循环里的 item 为 url

定位到这段输出 RSS item 的代码:

echo " <item>\n".
" <title>".$article['subject']."</title>\n".
" <link>$_G[siteurl]".($rewriteflag ? rewriteoutput('portal_article', 1, '', $article[aid]) : "portal.php?mod=view&aid=$article[aid]")."</link>\n".
" <description><![CDATA[".dhtmlspecialchars($article['description'])."]]></description>\n".
" <category>".dhtmlspecialchars($article['catname'])."</category>\n".
" <author>".dhtmlspecialchars($article['author'])."</author>\n".
($attachfile ? '<enclosure url="'.$filename.'" length="'.$attachsize.'" type="image/jpeg" />'."\n" : '').
" <pubDate>".gmdate('r', $article['dateline'])."</pubDate>\n".
" </item>\n";

整段替换为:

echo " <url>\n".
" <loc>$_G[siteurl]".($rewriteflag ? rewriteoutput('portal_article', 1, '', $article[aid]) : "portal.php?mod=view&aid=$article[aid]")."</loc>\n".
" <lastmod>".date('Y-m-d', $article['dateline'])."</lastmod>\n".
" <changefreq>daily</changefreq>\n".
" <priority>1.0</priority>\n".
" </url>\n";

第五步:替换尾部的 channel 关闭标签

定位结尾这两行:

echo " </channel>\n".
"</rss>";

替换为:

echo "</urlset>\n";

第六步:调整输出条数

portal_sitemap.php 顶部有一行:

$num = 20;

这个 20 是 RSS 默认输出条数,对 sitemap 来说太少。改成 1000:

$num = 1000;

注意:单文件 sitemap 协议规定最多 50,000 个 URL 与文件大小不超过 50MB 未压缩。我建议单文件不要超过 5,000 行(约 1.5MB),让 Googlebot 一次性下载更可靠。如果文章总数超过 5000,需要做分页 sitemap,后面有专门一节讲。

访问与提交

本地访问验证

改完之后浏览器访问:

https://你的域名/portal.php?mod=sitemap

应当看到一份完整的 XML 文档,前 5 行类似:

<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
 <url>
 <loc>https://你的域名/portal.php?mod=view&aid=123</loc>
 <lastmod>2024-09-01</lastmod>

如果 URL 显示成 portal.php?mod=view&aid=123 这种动态地址,说明你站点没开启 URL 重写。可以在后台「全局-URL 静态化设置」里开启 portal_article 重写规则,刷新一遍 sitemap,URL 会自动切换成 article-123-1.html 这种静态化形态。

把 sitemap 暴露成 /sitemap.xml

portal.php?mod=sitemap 是一个动态地址,搜索引擎仍然能识别但视觉上不够友好。两种方案让它变成 /sitemap.xml:

方案 A:nginx rewrite

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

方案 B:apache rewrite

RewriteRule ^sitemap\.xml$ portal.php?mod=sitemap [L]

选 nginx rewrite 的好处是:搜索引擎抓 sitemap.xml 时返回的 Content-Type 还是 application/xml;apache 的 RewriteRule 默认会把 .xml 后缀的 MIME 类型设为 text/xml,部分搜索引擎对此会发出警告。如果你想用 apache 方案,最好在 .htaccess 里再补一句 AddType application/xml .xml

提交到搜索引擎

四个主流搜索引擎的提交入口:

  • Google Search Console:左侧菜单「站点地图」-填入 sitemap.xml-提交。提交后 24-48 小时会显示「已成功」。
  • 百度站长平台(百度搜索资源平台):「资源提交」-「Sitemap」-填入完整 URL。百度对 sitemap 抓取频率较低,提交后可能两三天才开始抓。
  • Bing Webmaster Tools:「Sitemaps」-填入 URL。
  • 360 搜索 / 神马 / Yandex:各自后台都有 sitemap 提交入口,按需操作。

分页 sitemap 与 sitemapindex

当门户文章总数超过 5000 时,单个 sitemap 文件压力大且 Googlebot 抓取慢。这时改用 sitemapindex 拆分:

设计思路

第一层文件 sitemap.xml 改成 sitemapindex 格式,列出所有子 sitemap 文件:

<?xml version="1.0" encoding="UTF-8"?>
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
 <sitemap>
  <loc>https://example.com/portal.php?mod=sitemap&page=1</loc>
  <lastmod>2024-09-01</lastmod>
 </sitemap>
 <sitemap>
  <loc>https://example.com/portal.php?mod=sitemap&page=2</loc>
  <lastmod>2024-09-01</lastmod>
 </sitemap>
</sitemapindex>

每个子 sitemap 接受一个 page 参数,按 LIMIT offset 取对应分页。

portal_sitemap.php 增加分页处理

在 portal_sitemap.php 顶部增加:

$page = max(1, intval($_GET['page']));
$pageSize = 1000;
$offset = ($page - 1) * $pageSize;

找到原本的 $articles 查询语句(在 portal_rss.php 里通常是 "SELECT ... FROM ".DB::table('portal_article_title')..." LIMIT $num"),改为:

"SELECT ... FROM ".DB::table('portal_article_title')..." LIMIT $offset, $pageSize"

当请求 portal.php?mod=sitemap 不带 page 参数时输出 sitemapindex;带 page 参数时输出对应分页的 urlset。代码大致:

if (!isset($_GET['page'])) {
    // 输出 sitemapindex
    $totalArticles = DB::result_first("SELECT COUNT(*) FROM ".DB::table('portal_article_title')." WHERE status=0");
    $totalPages = ceil($totalArticles / $pageSize);
    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 <= $totalPages; $p++) {
        echo " <sitemap>\n";
        echo "  <loc>{$_G['siteurl']}portal.php?mod=sitemap&page={$p}</loc>\n";
        echo "  <lastmod>".date('Y-m-d')."</lastmod>\n";
        echo " </sitemap>\n";
    }
    echo "</sitemapindex>\n";
    exit;
}
// 否则继续走原有的 urlset 输出逻辑

提交时只交 sitemap.xml

分页方案下只需要给搜索引擎提交 sitemap.xml(即 sitemapindex 入口),搜索引擎会自动按 sitemapindex 抓取所有子 sitemap。

性能优化:缓存策略

每次请求都查一遍数据库对中大型站点是浪费。建议给 portal_sitemap.php 加缓存:

方案 A:文件缓存

在脚本最开始加入:

$cacheFile = DISCUZ_ROOT.'./data/cache/sitemap_'.intval($_GET['page']).'.xml';
$cacheTtl = 3600; // 1 小时
if (file_exists($cacheFile) && (time() - filemtime($cacheFile) < $cacheTtl)) {
    header('Content-Type: application/xml; charset=UTF-8');
    readfile($cacheFile);
    exit;
}
ob_start();

在脚本最后加入:

$content = ob_get_contents();
file_put_contents($cacheFile, $content);
ob_end_flush();

这样每页 sitemap 在 1 小时内只查一次数据库。50000 个 URL 的站点 sitemap 抓取压力从每秒几次 SQL 查询降到几乎为零。

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

对超大站点(10 万+ 文章)建议直接定时任务生成静态 XML 文件。在 Discuz 后台「系统设置-计划任务」新建一条计划任务,每小时执行一次自定义脚本:

<?php
$urls = ['https://example.com/portal.php?mod=sitemap']; // sitemapindex
for ($p = 1; $p <= 100; $p++) {
    $urls[] = "https://example.com/portal.php?mod=sitemap&page={$p}";
}
foreach ($urls as $url) {
    $xml = file_get_contents($url);
    $filename = 'sitemap'.(strpos($url, 'page=') ? '_'.parse_url($url, PHP_URL_QUERY) : '').'.xml';
    file_put_contents(DISCUZ_ROOT.'./'.$filename, $xml);
}

这样搜索引擎抓取的就是真正的静态文件,速度最快。

板块帖子的 sitemap 用同样思路实现

portal 是门户(CMS 模块),bbs 是板块论坛。如果你站点的内容主要在板块帖子里,sitemap 应该覆盖 forum 而不是 portal。同样的思路改造 forum_rss.php:

  1. 在 forum.php 入口加 sitemap 路由(与 portal.php 改法对应)。
  2. 复制 source/module/forum/forum_rss.php 为 forum_sitemap.php。
  3. 把 RSS 输出格式替换为 sitemap urlset。
  4. 查询逻辑里加上「不抓回收站、不抓隐藏板块、不抓未审核帖子」的过滤条件:WHERE displayorder >= 0 AND closed = 0 AND status = 0

常见的实战坑

坑 1:URL 重写规则没匹配

很多站点会把 sitemap.xml 通过 nginx rewrite 转到 portal.php?mod=sitemap,但同时根目录又有一个 sitemap.xml 静态文件(之前用插件留下的)。nginx 的 location 匹配优先级里静态文件优先,rewrite 不生效。解决:删除根目录的旧 sitemap.xml 静态文件,或者用 try_files 指令显式跳过文件查找。

坑 2:URL 里 & 没转义为 &amp;

portal.php?mod=view&aid=123 这种 URL 在 sitemap 协议里 & 必须写成 &amp;。Discuz 的 dhtmlspecialchars 函数会做这个转义,但部分二开版本去掉了这一步。如果搜索引擎报 sitemap 格式错误,先检查 URL 里是不是 & 而不是 &amp;。原代码里 portal.php?mod=view&amp;aid=$article[aid] 这种写法是正确的,注意保留。

坑 3:utf-8 与 gbk 字符集冲突

Discuz 老站点常见 GBK 编码。sitemap 协议要求 UTF-8。在 portal_sitemap.php 顶部强制:

header('Content-Type: application/xml; charset=UTF-8');
$charset = 'UTF-8';
// 如果原始数据是 GBK,每条 URL 的 title/desc 需要 mb_convert_encoding

对 GBK 编码的标题做转换:

$loc = mb_convert_encoding($loc, 'UTF-8', 'GBK');

坑 4:lastmod 时间格式错

sitemap 协议接受 YYYY-MM-DD 或 YYYY-MM-DDThh:mm:ss+TZ 两种格式。原代码用的是 date('Y-m-d', $article['dateline']),是合法的。如果你想精确到秒以让 Google 知道更新时刻:

date('Y-m-d\TH:i:sP', $article['dateline'])

这样输出 2024-09-01T15:30:00+08:00 形式。

坑 5:404、301、403 的 URL 进了 sitemap

有些已删除文章在 portal_article_title 里仍有记录,但 status=1(待审核)或 status=2(已删除)。原 SQL 查询可能没过滤这些状态。修复:在 SQL WHERE 后加 AND status = 0

坑 6:抓取频率过高反而触发反爬

Googlebot 在 sitemap 提交后短时间内会高频访问。如果你的 sitemap 没缓存且服务器配置不高,可能会因为高 SQL 压力被 fail2ban 误判封 IP。建议:在 nginx 层针对 Googlebot 单独限速但不阻断;或者直接使用方案 B 的静态文件,从根上避免动态生成。

X3.5 版本的细微差异

Discuz X3.5 把部分 source 目录的文件做了重组,但 portal_rss.php 与 portal_sitemap.php 的位置仍然在 source/module/portal/ 下。核心改动:

  • X3.5 默认走 PHP 7.4+,原来 PHP 5.x 的某些写法会触发 deprecated 警告。原 RSS 代码里没有 deprecated 调用,本文改造也不引入。
  • X3.5 的 URL 重写默认开启 SEO 静态化为 article-{aid}-{page}.html,rewriteoutput 函数行为没变,本文代码无需调整。
  • X3.5 后台的 sitemap 提交入口在「全局-性能优化-Sitemap」,这里只能配置插件型 sitemap,本文方案不依赖它。

与 Google Search Console 的诊断协作

提交 sitemap 后 24-48 小时去 Google Search Console「站点地图」页面查看抓取情况。最常见的几种结果:

  • 「成功 - 已发现 N 个 URL」:正常状态。N 应当与你预期的文章数接近,差几条以内属正常波动。
  • 「无法获取站点地图」:通常是 sitemap.xml 返回 5xx 或 robots.txt Disallow 了 sitemap 路径。
  • 「sitemap 中有 URL 错误」:点开错误详情,常见是 lastmod 格式不对、URL 中有空格、URL 跨域。
  • 「已发现但未编入索引」:sitemap 没问题,但 Google 评估这些 URL 的内容质量后选择不索引。这是内容质量问题,不是 sitemap 技术问题。

常见问题解答

portal.php?mod=sitemap 与根目录的 sitemap.xml 文件冲突怎么办?

Web 服务器的优先级是先找静态文件再走 PHP 动态。如果根目录有同名 sitemap.xml 静态文件,nginx rewrite 不会生效。两个解决:直接删除根目录的旧静态文件;或者在 nginx 的 location ~ \\.xml$ 块里用 try_files 显式跳过文件检查。

portal_sitemap.php 输出空白页是什么原因?

三个排查点:第一,portal.php 入口的白名单数组是否真的加了 'sitemap',没加会被默认改成 mod=index 走首页;第二,文件名是否严格匹配 portal_sitemap.php,差一个字母路由就找不到;第三,php-fpm 错误日志里看是否有 fatal error,常见的是 PHP 7+ 不识别老的 dhtmlspecialchars 调用方式。

sitemap 里要不要包含 list 页(分类页)?

建议包含。分类页(portal.php?mod=list&catid=N)也是有价值的索引页,特别是分类内文章很多的情况下。改 portal_sitemap.php 在文章 URL 之外,再循环输出一遍分类 URL:

foreach ($_G['cache']['portalcategory'] as $cat) {
    $listUrl = $_G['siteurl'].($rewriteflag ? rewriteoutput('portal_list', 1, '', $cat['catid']) : "portal.php?mod=list&catid={$cat['catid']}");
    echo " <url>\n  <loc>{$listUrl}</loc>\n  <changefreq>daily</changefreq>\n  <priority>0.6</priority>\n </url>\n";
}

sitemap 里要不要带文章配图作为 image sitemap?

对图文为主的门户站点,强烈建议加 image sitemap。在每个 url 块里增加:

<image:image>
 <image:loc>https://example.com/article-cover.jpg</image:loc>
 <image:title>文章标题</image:title>
</image:image>

同时在 urlset 标签里增加命名空间:xmlns:image="http://www.google.com/schemas/sitemap-image/1.1"。这样 Google 会单独索引图片,给你带 image search 流量。

百度对 sitemap 的抓取慢怎么办?

百度抓取 sitemap 的频率确实比 Google 低,提交后可能两三天才开始动。三个加速手段:第一,配合百度推送 API 主动推(百度站长平台-资源提交-推送 API);第二,sitemap 在 robots.txt 里显式声明:Sitemap: https://example.com/sitemap.xml;第三,新文章发布时实时推送单 URL 给百度。

多语言站点 sitemap 怎么处理?

每个语言一份 sitemap,在 sitemapindex 里都列出来。每个 url 块里用 <xhtml:link rel="alternate" hreflang="en"/> 标记其它语言版本。这是 Google 推荐的多语言 sitemap 协议。

sitemap 文件大小超过 50MB 怎么办?

分页 sitemap 是直接的解决方案。另一种做法是 gzip 压缩:把 sitemap.xml 用 gzip 压缩成 sitemap.xml.gz,搜索引擎都支持。Apache/Nginx 配置 mod_deflate / gzip 自动压缩 .xml 即可。

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

「发现 N 个 URL」是 sitemap 提交成功的指标,通常 1-2 天内显示。「编入索引」需要 1-4 周,取决于站点权威度与内容质量。新站点 sitemap 提交后 1 个月才把所有 URL 索引完是正常状态。

原来用 dz_sitemap 插件,怎么平滑切到本文方案?

三步:第一,停用插件但暂时不卸载(保留旧 sitemap.xml 文件以防搜索引擎正在抓);第二,部署本文方案,访问 portal.php?mod=sitemap 验证内容正确;第三,把 nginx rewrite 加上让 /sitemap.xml 指向新方案,同时删掉旧静态 sitemap.xml 文件。这个切换对搜索引擎透明,URL 没变。

本方案对 Discuz 升级有影响吗?

影响极小。新增的 portal_sitemap.php 文件官方升级不会动它。portal.php 入口加的一行白名单是你需要在每次升级后手动合并的,但这一行改动很容易识别。如果想完全免维护,可以把这行白名单写成插件 hook(plugin/your_plugin/portal.inc.php),通过 Discuz 插件机制注入,升级完全无感。

分享到
标签
版权声明

本文标题:《Discuz 门户 sitemap 免插件实现:portal_rss.php 改造、分页 sitemapindex 与缓存策略完整教程》

本文链接:https://zhangwenbao.com/discuz-portal-sitemap.html

版权声明:本文原创,转载请注明出处和链接。许可协议: CC BY-NC-SA 4.0

继续阅读
发表评论
分享到微信 或在下方手动填写
支持 Ctrl + Enter 提交