1. 糟糕的开头
周一早上九点,我例行打开 Google Search Console,差点从椅子上摔下来。
已索引页面数:12。
前一天还是 639。我的三语工具站 daima.life 有 213 个工具,乘以中英日三种语言,加上文章页,理论上至少 650+ 个页面应该被收录。现在只剩 12 个?我第一反应是 Google 把我手动惩罚了——难道是哪篇文章触发了什么审核?
但点进"页面"报告一看,比惩罚更恐怖:639 个页面状态全变成了 Excluded - Not found (404)。
Google 在告诉我:你的 sitemap 指向的那些 URL,我全找不到。
2. 我的思考:为什么 sitemap 能造成这么大的破坏?
很多开发者觉得 sitemap.xml 就是个可有可无的东西——"反正 Google 自己会爬"。这种想法在 2026 年是致命的。
现代搜索引擎的爬虫预算(Crawl Budget)是有限的。对于小站来说,Google 不会浪费资源去无限次重试你的每一个页面。sitemap.xml 本质上是你主动告诉 Google:"这些 URL 是有效的,请优先来抓。" 如果你的 sitemap 指向的全是 404,Google 的算法会做出一个非常合理的判断:这个站挂了,降权。
更可怕的是,这种降权不是即时的,而是渐进式的。Google 每隔几天重新爬一次 sitemap,如果连续几次发现全是 404,它会逐步把你已有的索引也清掉。我发现问题的时候,已经过去了三天——也就是说,Google 至少已经验证了两轮。
3. 技术硬核区:那个 Bug 到底在哪?
daima.life 用 Next.js 14 的 App Router,sitemap 是动态生成的。逻辑大致如下:
// app/sitemap.ts — 动态生成 sitemap
import { toolsConfig } from '@/lib/tools-config';
const LOCALES = ['zh', 'en', 'ja'];
const BASE_URL = 'https://daima.life';
export default function sitemap() {
const toolUrls = toolsConfig.flatMap(category =>
category.items.flatMap(tool =>
LOCALES.map(locale => ({
url: `${BASE_URL}/${locale}/tools/${tool.id}`,
lastModified: new Date(),
changeFrequency: 'weekly',
priority: 0.8,
}))
)
);
return [...toolUrls, ...articleUrls, ...staticUrls];
}
看起来没问题对吧?问题出在我那天做的一个"小改动"。
我当时在重构多语言路由,把默认语言从 /zh/tools/json 改成了无前缀的 /tools/json(即中文用户访问时不再带 /zh)。这个改动确实让 URL 更简洁了,但我忘了同步更新 sitemap 的生成逻辑。
结果就是 sitemap 里写的是:
https://daima.life/zh/tools/json ← sitemap 声称的 URL
但实际路由已经变成了:
https://daima.life/tools/json ← 真实可访问的 URL
旧的 /zh/... 路径现在返回 301 重定向到无前缀版本。Google 爬到 sitemap 里的 URL,得到 301,去了新 URL,但 sitemap 里没有新 URL 的记录——Google 认为这些页面"不在 sitemap 内",同时 sitemap 内的页面全是 301/404。
两头都不对,Google 直接判定全站失效。
修复的核心代码改动其实就一行:
// 修复前
LOCALES.map(locale => ({
url: `${BASE_URL}/${locale}/tools/${tool.id}`,
}))
// 修复后:默认语言不带前缀
LOCALES.map(locale => ({
url: locale === 'zh'
? `${BASE_URL}/tools/${tool.id}`
: `${BASE_URL}/${locale}/tools/${tool.id}`,
}))
4. FAQ 模块
Q1: 修完 sitemap 后,Google 多久能恢复收录?
别指望立竿见影。我的经历是:提交修复后的 sitemap 并在 Search Console 点击"请求重新编入索引"后,第一批页面在 48 小时内回来了大约 30%。完全恢复到 639 花了整整 11 天。期间流量掉了大概 40%。教训是:sitemap 出错的每一天,都是在烧 SEO 资产。
Q2: 怎么在部署前自动检测 sitemap 的有效性?
我现在在 CI/CD 流程里加了一个 post-build 校验脚本:
// scripts/validate-sitemap.js
const { parseStringPromise } = require('xml2js');
const fs = require('fs');
async function validate() {
const xml = fs.readFileSync('.next/server/app/sitemap.xml', 'utf-8');
const result = await parseStringPromise(xml);
const urls = result.urlset.url.map(u => u.loc[0]);
for (const url of urls.slice(0, 20)) { // 抽样检查前 20 条
const res = await fetch(url, { redirect: 'manual' });
if (res.status !== 200) {
console.error(`SITEMAP ERROR: ${url} returned ${res.status}`);
process.exit(1);
}
}
console.log(`Validated ${urls.length} sitemap URLs, sample check passed.`);
}
validate();
这个脚本在每次 npm run build 之后自动跑。如果 sitemap 里任何一条 URL 不返回 200,构建直接失败。宁可部署不上线,也不能让有毒的 sitemap 提交给 Google。
Q3: Next.js 的 sitemap.ts 有没有官方的最佳实践?
有,但很不够用。官方文档只教你返回一个数组,不会告诉你多语言场景下 alternateRefs 怎么配、hreflang 怎么加。我的最终方案是给每个 URL 加上 alternates 字段,让 Google 明确知道同一页面的三语版本是什么关系:
{
url: 'https://daima.life/tools/json',
alternates: {
languages: {
en: 'https://daima.life/en/tools/json',
ja: 'https://daima.life/ja/tools/json',
}
}
}
这样 Google 不会把三个语言版本当成重复内容去重。
5. 结尾
sitemap 这个东西,你不出事的时候觉得它就是个 XML 文件,出了事才知道它是你整个 SEO 体系的脊椎。
现在每次 push 代码,我都会下意识地先打开 /sitemap.xml 看一眼。就像老司机上车先系安全带一样——不是怕出事,是因为出过事。
下一篇,想聊聊 robots.txt 和 canonical 标签配合 sitemap 的三角联防策略。一个 disallow 写错位置,后果可能比 sitemap 翻车还惨……