Sitemap lastmod 永远是今天,Google 不再信任

sitemap 里每个 URL 的 `lastmod` 都是今天。Googlebot 抓取率不升反降,因为 Google 已经把这个字段当作不可靠信号忽略掉了。

最快的修法:别再在 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/sitemapnext-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 字段可用(publishedAtmodifiedAtupdatedAt)。
  • 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-LD dateModified 的来源是同一个 —— 一个真相来源。
  • 真正不会变的静态归档页,宁可不写 lastmod,也别挂一个过期的值。
  • 每季度审一次边缘 / CDN 的响应转换,盯住静默改写。

常见坑

  • “保险起见”把 lastmod 完全删掉。真实的 lastmod 是有用的优先级信号,只有在没靠谱来源时才不写。
  • 改了导航或 footer 之后给每个页面 bump lastmod。站点级模板改不是单页内容变化。
  • 修个错别字就把 lastmod 改成”现在”。琐碎编辑应该不动 lastmod;跟你给 dateModified 定的”有意义的编辑”规则保持一致。
  • lastmoddateModified 分别由两个独立生成器算 —— 一定会漂移;合并。
  • 一天内重交 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:changefreqpriority 现在还有用吗?

没用。Google 对 <changefreq><priority> 两个都完全忽略(截至 2026 年 6 月),它只看 URL 和 <lastmod>。把精力花在把 lastmod 做诚实上,别去调那两个。

外部参考

标签: #SEO #排查 #Sitemap #lastmod #抓取预算