站点地图 · Case 03
这是我知道的最难自查的一类故障:页面全部 200,站内导航正常,用户访问没有任何异常,唯一的症状是「提交了站点地图,但没搜到任何网址」。而这句话只在搜索引擎的后台里能看到。
<sitemap><loc>https://haowordtool.comsitemap.xml</loc></sitemap>
<sitemap><loc>https://haowordtool.comde/sitemap.xml</loc></sitemap>
<sitemap><loc>https://haowordtool.comja/sitemap.xml</loc></sitemap>
六条 <loc>,全部缺一个斜杠。https://haowordtool.com 直接拼上了 sitemap.xml,域名和文件名连在一起,成了六个不存在的地址。
因为它不产生任何错误。请求 /sitemap-index.xml 返回 200,内容是合法 XML,格式完全正确——只是里面的 URL 打不开。整条链路里没有任何环节会报错:
搜索引擎那边看到的是「索引文件里六个子图都取不到」,最终表现为「已提交,未发现任何网址」。这个描述不会指向「你的 URL 拼错了」,因为从它那一侧看,只是一个取不到的资源。
const SITE = 'https://haowordtool.com'; // 结尾没有 /
`${SITE}${p}` // p 开头也没有 /
这种 bug 的特征是:写的时候两边看起来都对,跑起来也不报错,只有把输出打印出来才看得见。而且它藏在生成器里,手改 sitemap 文件没用——下次生成会覆盖回去。
改法不是加一个斜杠,而是让「拼绝对 URL」只有一个入口:
const abs = (p) => `${SITE}/${String(p).replace(/^\/+/, '')}`;
所有生成 loc 的地方都走它,包括子 sitemap 列表和索引文件。然后加一道落盘前校验:任何一条 loc 不以 https://haowordtool.com/ 开头,直接抛错,文件不写。
校验逻辑本身也要有:
function assertLocs(xml, where) {
const bad = [...xml.matchAll(/<loc>([^<]*)<\/loc>/g)]
.map(m => m[1])
.filter(u => !u.startsWith('https://haowordtool.com/'));
if (bad.length) throw new Error(`${where}: ${bad.length} 条 loc 非法,例如 ${bad[0]}`);
return count;
}
排查时发现有两个脚本都在写 sitemap.xml:一个按页面状态生成,一个在「SEO 优化」流程里顺手重写。两者输出的条目集不同,互相覆盖。表现是 CI 的 --check 永远报 pending——因为它比对的基准一直在被另一个脚本改。
处理方式是收权:build-sitemap 独占 sitemap 的写入,负责主站 + 各语种 + 索引三件套;另一个脚本只改 head 信号,不再碰 sitemap。「哪个脚本负责哪个产物」必须明确到单点,否则两个都对的实现会互相破坏。
之前每个 URL 都带 <lastmod>,值取文件修改时间。问题在于 CI 每次检出,文件时间都会变——于是每天生成的 sitemap 都在告诉搜索引擎「全站 189 个页面今天全更新了」。这属于制造信号,不如省略:没有 lastmod 是合法的,虚假的 lastmod 会稀释可信度。
修一次不够,同类问题还会再发生。所以加了两道 CI 检查:
sitemap-index.xml 引用的每个子 sitemap 必须存在,每条 loc 必须是合法绝对 URL。第二道的价值在于它不是「格式对不对」,而是「内容和现实是否同步」。sitemap 最容易腐坏的方式不是写错,而是页面加了 noindex、改了 canonical 之后它没跟着变。
修完线上逐条跟随验证:
| 子图 | 状态 | 条数 |
|---|---|---|
/sitemap.xml | 200 | 149 |
/de/sitemap.xml | 200 | 13 |
/es/sitemap.xml | 200 | 14 |
/ja/sitemap.xml | 200 | 11 |
/fr/sitemap.xml | 200 | 1 |
/zh/sitemap.xml | 200 | 1 |
操作细节:推送后立刻请求会拿到上一版。这次前四次探测全是 404,第五次才生效,窗口约 40 秒。验收一律带 ?cb=$RANDOM 之类的破缓存参数,否则会把「还在部署」误判成「没上线」——然后再去排查一个不存在的故障。
这条最值得记:凡是「由脚本生成的线上文件」,都必须有一条独立校验能在 CI 里跑。不能靠人肉 curl,因为没人会每天去 curl 一个 sitemap-index——它坏了三个月也不会有人发现,直到收录数据对不上。