最快的修法:别再在 build time 用 new Date() 给 lastmod 盖戳,改成从真实的内容变更时间取值(frontmatter 的 modifiedAt,缺失时回退到 publishedAt,或者该文件最后一次 commit 的时间)。如果某个 URL 实在没有可靠日期,就对它整段不写 lastmod,别造一个假的。
你每周部署一次。sitemap 生成器在 build time 用 new Date().toISOString() 给 lastmod 盖戳。Google 看到的结果是:你站点上每个页面昨天都变过,那就等于没一个真变过。这就是 sitemap 被忽略的教科书签名。Google 自己的文档(截至 2026 年 6 月)写得很清楚:只有当 lastmod “consistently and verifiably (for example by comparing to the last modification of the page) accurate”(一致且可验证地准确,比如能跟页面实际的最后修改对得上)时才会采用;John Mueller 也直接说过,给每条都写今天这个日期”just lazy”(纯粹偷懒)。一旦 Google 判定你的 lastmod 不可靠,它会对整站停用这个字段,于是真正更新过的页面重抓速率反而下降。最坏的情况是”已发现 — 当前未编入索引”越堆越多。修法是让 lastmod 来自真实的内容变更时间,而不是 build time。
常见原因
按命中率排序。
1. lastmod 在 build time 用 new Date() 算出来
sitemap 插件或自研生成器每次 build 跑一次 new Date().toISOString(),把这个值塞给每一条。每周 build 一次,每个 URL 就都”每周变一次”。在没接入按页日期的情况下,好几个常见生成器的默认行为就是这样,包括部分 @astrojs/sitemap、next-sitemap 和 Gatsby 配置,所以先查一遍你的配置,别一上来就当成是自研代码的锅。
怎么判断:抓在线 sitemap,grep lastmod,所有值都在几秒内,并且匹配你上次部署的时间。
2. lastmod 取自文件 mtime,但 build 步骤会把每个文件重写一遍
build 前的某一步(格式化、转换、压缩)会原地重写所有内容文件,mtime 全部更新。生成器拿 mtime 当 lastmod。每次构建每个文件都看起来刚改过。
怎么判断:ls -lt src/content/ 显示所有文件都在最近一次 build 窗口内被改过,包括你几个月没碰过的。
3. lastmod 取自 CI 任务的时间戳
CI 用 --depth=1 或在某层 Docker 里 checkout,mtime 被重置。生成器拿不到 git commit 时间,fallback 到 “now”。
怎么判断:所有 lastmod 都落在一个 30 秒窗口内,每次部署是不同的统一值。
4. 源里 lastmod 是对的,被 CDN / 边缘 Worker 改写了
你从 frontmatter 正确生成了 lastmod,但边缘 Worker 重写 sitemap 响应(加缓存破坏字段、规范化格式)时把值覆盖了。
怎么判断:对比直连源站和走 CDN 拿到的 sitemap。lastmod 不一致就是它。
5. lastmod 取整到日,但每个条目落在同一天
生成器把时间戳”规范化”到 UTC 0 点。如果大部分编辑都集中在部署日的某个窗口,所有条目压成同一天,Google 会判定可疑。
怎么判断:所有 lastmod 是同一天的 00:00:00Z。
6. 新增的 URL 继承”现在”而不是真实发布日期
新文章加入 sitemap 时 lastmod 直接写今天,哪怕它其实是两年前发布的。同样的逻辑在每次回填跑批时错误地 bump 所有老 URL。
怎么判断:多年没动过的老归档 URL lastmod 是最近的。
什么才算”有意义的变更”
在给 lastmod 接新来源之前,先把这条定义弄对,因为正是”琐碎编辑也 bump 日期”这件事,一开始把 Google 训练得不再信任你。Google 的文档(截至 2026 年 6 月)这样划线:
该 bump lastmod 吗? | 变更类型 |
|---|---|
| 该 | 正文被编辑(加段、改写、删段) |
| 该 | 页面上的结构化数据(JSON-LD)变了 |
| 该 | 页面上的链接有实质性变化 |
| 不该 | footer 的版权年份翻了一年 |
| 不该 | 站点级模板、导航、footer 的小改动 |
| 不该 | 改个错别字或只是空白字符变动 |
| 不该 | 自动保存或没有内容差异的重新发布 |
跟你给 JSON-LD dateModified 用的”有意义的编辑”规则保持一致,这样两者永远不会打架。
开始前
- 改之前先快照一份当前 sitemap(或顶层索引)。修完用来对照。
- 看 Search Console → Sitemaps → 报告详情,是否有 “valid but warning” 之类信息 —— Google 有时会明确警告 lastmod 不可靠。
- 决定
lastmod的真相来源:通常是 frontmatter 的modifiedAt(缺失时 fallback 到publishedAt)。 - 如果你已经按 发布日期与 JSON-LD 日期不一致 审过日期,跟那里用同一个来源。
需要收集的信息
- 当前
lastmod值的分布:是不是大多数落在 5 分钟窗口内,还是跨越好几个月? - 输出 sitemap 的生成器代码或插件。
- 每种内容类型有哪些 frontmatter 字段可用(
publishedAt、modifiedAt、updatedAt)。 - Search Console “Crawl stats” 最近的趋势 —— 是不是新增内容多,但总抓取请求数反而降了?
- sitemap 是按内容类型拆分的,还是一份大单文件。
一步步修
按代价排序。
第 1 步:先看一眼当前 sitemap
curl -s https://example.com/sitemap.xml | \
grep -oE '<lastmod>[^<]+</lastmod>' | sort | uniq -c | sort -rn | head -10
最大那一组覆盖几千个 URL 且匹配上次部署时间 —— bug 坐实。
第 2 步:让 lastmod 来自真实内容日期
替换 build-time 生成逻辑:
// before
{ loc: url, lastmod: new Date().toISOString() }
// after
{
loc: url,
lastmod: (article.modifiedAt ?? article.publishedAt),
}
没有真实时间戳的,宁可整段不写 lastmod,也别造一个。
输出时用 W3C Datetime 格式,这是 Google 要求的:要么 YYYY-MM-DD(比如 2026-06-04),要么 YYYY-MM-DDThh:mm:ss+TZD(比如 2026-06-04T17:15:30+00:00)。如果带了时间,就必须带时区偏移,否则 Google 会在 Sitemaps 报告里报错。不写时间则默认按 UTC 0 点处理。Date.prototype.toISOString() 本身就产出合法的 ...Z 形式,所以这里格式很少是 bug,问题出在时间戳的来源上。
第 3 步:frontmatter 缺日期就从 git history 回填
git log --diff-filter=AM --follow --format=%aI -- "src/content/articles/en/${slug}.mdx" \
| head -1
这返回最近一次新增或修改这个文件的 commit 时间。对没有显式日期的文章拿来当 modifiedAt。
第 4 步:让 build 前的步骤不要乱动 mtime
如果有格式化工具每次 build 都把所有文件重写一遍,改成内存里转换,或者用内容 hash 守门:
const before = await readFile(path);
const after = format(before);
if (after !== before) await writeFile(path, after);
writeFile 只在真有变化时触发。mtime 不再无意义地翻动。
第 5 步:确认 CI 保留了 git 时间戳
如果你依赖 commit 日期,确保 CI clone 拿完整历史:
- uses: actions/checkout@v4
with:
fetch-depth: 0
然后让 sitemap 生成器读 commit 时间而不是 mtime。
第 6 步:检查边缘层有没有改写
diff <(curl -s https://origin.example.com/sitemap.xml) \
<(curl -s https://example.com/sitemap.xml)
lastmod 有差异,就是 CDN 或边缘 Worker 在改写。要么绕过,要么修掉。
第 7 步:重新提交并观察抓取响应
在 Search Console 重新提交 sitemap。2-3 周内观察 “Crawl stats” 曲线,看抓取是否倾向于真正变化的 URL。“已发现 — 当前未编入索引”对真正更新的页面应该慢慢减少。
验证
- Sitemap
lastmod的分布跨越周和月,有真实方差,不是每次部署一个统一值。 - 新文章的
lastmod匹配publishedAt,不是今天。 - Search Console → Crawl stats 显示真正编辑过的 URL 重抓被优先。
- 重要内容上的”已发现 — 当前未编入索引”开始回落。
- CMS 编辑动作在一个部署周期内能流到
lastmod上。
长期预防
- 团队范围明确:
lastmod反映真实内容变化,永远不取 build time。 - CI 断言:sitemap 里超过 30% 的条目共享同一个
lastmod分钟,构建失败。 lastmod来源跟驱动 JSON-LDdateModified的来源是同一个 —— 一个真相来源。- 真正不会变的静态归档页,宁可不写
lastmod,也别挂一个过期的值。 - 每季度审一次边缘 / CDN 的响应转换,盯住静默改写。
常见坑
- “保险起见”把
lastmod完全删掉。真实的lastmod是有用的优先级信号,只有在没靠谱来源时才不写。 - 改了导航或 footer 之后给每个页面 bump
lastmod。站点级模板改不是单页内容变化。 - 修个错别字就把
lastmod改成”现在”。琐碎编辑应该不动lastmod;跟你给dateModified定的”有意义的编辑”规则保持一致。 lastmod跟dateModified分别由两个独立生成器算 —— 一定会漂移;合并。- 一天内重交 5 次 sitemap “逼” Google 重抓。完全没效果,反而可能给 sitemap 上标记。
FAQ
Q:Google 真的会惩罚刷 lastmod 这种模式吗?
没有排名惩罚,但代价是实打实的。Google 文档说,只有当 lastmod “consistently and verifiably accurate”(一致且可验证地准确)时才会采用;John Mueller 也说过,给每条都写今天这个日期”isn’t going to be something that works in favor of anyone, it’s just lazy”(对谁都没好处,纯粹偷懒)。一旦 Google 判定你的 lastmod 不可靠,它会对整站忽略这个字段,等于你压根没提供任何信号。损失的是那些真正变过的页面本可以拿到的更快重抓。
Q:要给每个 URL 都写 lastmod 吗,还是只有变更过的写?
有可靠时间戳就写,没就别写。诚实的缺失好过编造的值。
Q:CMS 只跟踪”编辑日期”,够吗?
够,只要”编辑日期”只在真正编辑时变,不在自动保存或模板改动时变。一段时间不编辑然后看这个值有没有变,能验证。
Q:lastmod 精度应该到哪一级?日、小时、秒?
跟你的跟踪精度一致。内容站点到日(2026-05-24)够用。快变 feed 用到秒合适。别造你本没有的精度。如果确实带了时间,记得带时区偏移,否则 Google 当格式错误拒掉。
Q:修好之后多久 Google 才会重新信任 lastmod?
没有公开的”开关时刻”。实践中,等值变得准确且稳定后,Google 会在接下来的几个抓取周期里重新评估,对一个被频繁抓取的站点通常是两周左右。修完重新提交一次 sitemap 就够了,别为了催它反复提交。
Q:changefreq 和 priority 现在还有用吗?
没用。Google 对 <changefreq> 和 <priority> 两个都完全忽略(截至 2026 年 6 月),它只看 URL 和 <lastmod>。把精力花在把 lastmod 做诚实上,别去调那两个。
Related
- Sitemap Pages Missing from Search Console
- Sitemap Submitted but Not Indexed
- Last-Modified Header Missing
- Article Date and JSON-LD Date Mismatch
- Robots Meta vs Sitemap Conflict
外部参考
- Build and submit a sitemap — Google Search Central —— 官方的
lastmod规则和 W3C Datetime 格式。 - Sitemaps report — Search Console Help —— 格式错误和警告会在这里出现。