Last-Modified 响应头缺失:先查 sitemap,再修头

页面更新后 Google 迟迟不重抓。先查 sitemap lastmod,再修缺失的 Last-Modified / ETag,让 Googlebot 能做条件抓取。

你更新了一篇文章。过了几天。Google 缓存版本还显示旧内容。JSON-LD 里的 dateModified 是对的、页面本身渲染的也是新内容,但 Google 就是没重抓。

先说最快的那一步: Google 官方文档里真正写明的重抓信号是 XML sitemap 里准确的 <lastmod>,而不是 HTTP 头。所以第一步是确认 sitemap 里这篇的修改日期是新的,并且 Search Console 显示 sitemap 已被读取。Last-Modified HTTP 头是第二道杠杆:对抓过的页面,Googlebot 会带 If-Modified-Since 再来请求,正确的 Last-Modified 能让你的服务器回 304 Not Modified,省下抓取预算,让 Google 把额度花在真正变了的页面上。如果服务器从不发 Last-Modified(或所有页都发同一个值),条件抓取就跑不起来,重抓效率随之下降。

这个头的问题在静态站上最常见。Vercel 和 Netlify 通常会正确设 Last-ModifiedETag;Firebase Hosting、GitHub Pages,以及一些自托管 nginx 不会。

先排除更大的原因:sitemap lastmod

Google Search Central,只有当 sitemap 的 <lastmod> 一直保持准确时,Google 才会用它来排重抓优先级。如果你以前在 <lastmod> 上撒过谎(比如所有页都盖上构建时间),Google 会调低你整个 sitemap 的信任分,并开始忽略这个字段。截至 2026 年 6 月,这仍是首要的新鲜度信号,而且 <lastmod> 不是排名因素。

动 HTTP 头之前先查它:

# 确认这篇在 sitemap 里的 lastmod 反映了真实编辑日期
curl -s "https://yoursite.com/sitemap.xml" | grep -A2 "yoursite.com/article"

如果 <lastmod> 缺失、过时,或所有 URL 都是同一个值,先修这个。在 Google Search Console 打开 Indexing -> Sitemaps,确认 sitemap 状态是 Success,然后对这篇具体页面用 URL Inspection -> Test Live URL -> Request Indexing 强制触发一次重抓。

只有当 sitemap 诚实之后,Last-Modified 头才是真正值得修的东西。

诊断:你属于哪一类?

跑一条命令,对照输出:

curl -sI "https://yoursite.com/article" | grep -iE 'last-modified|etag|cache-control'
你看到的可能原因跳到
没有 last-modified、也没有 etag主机两个都不发(Firebase / GitHub Pages / 裸 nginx)原因 1
源站有、公网 URL 没有CDN 把头剥了原因 2
每个页面的 last-modified 都一样值是构建时间,不是内容时间原因 3
出现 cache-control: no-store校验器被打折原因 4
etag、没 last-modified校验器只配了一半原因 5
页面是动态的、头缺失SSR/worker 路由没设头原因 6

常见原因

按命中率从高到低。

1. 托管平台不设 Last-Modified

Firebase Hosting、GitHub Pages、某些定制 nginx 配置不会自动给静态文件设 Last-Modified。截至 2026 年 6 月,Firebase Hosting 默认仍发 Cache-Control: max-age=3600,且不会从其 CDN 可靠地发出基于内容的 Last-Modified 或由 ETag 驱动的 304,所以这些头得你自己设。

怎么判断

curl -sI "https://yoursite.com/article" | grep -i last-modified

为空 = 没设。

2. CDN 把头剥了

源站返回 Last-Modified,Cloudflare 或别的 CDN 为缓存原因把它剥掉(或改写成回源填充的时间)。

怎么判断:跑两次 curl -I,一次直连源站(绕 CDN),一次正常。源站有 Last-Modified、公网 URL 没有,就是 CDN 的问题。

3. Last-Modified 一直是同一个值(构建时间)

某些静态站生成器把 Last-Modified 设为构建时间,所有页面共享一个值。Google 看不到页面之间的差异,就把它当噪声而非信号,跟 sitemap 里一堆相同 <lastmod> 引发的信任问题一模一样。

怎么判断curl -I 两个不同页面。共享同一个 Last-Modified 就是 bug。

4. Cache-Control: no-store 覆盖

如果设了 Cache-Control: no-storeLast-Modified 就没意义了——客户端根本不缓存,Googlebot 也没法发出有用的条件请求。

怎么判断

curl -sI "https://yoursite.com/article" | grep -iE 'cache-control|last-modified'

cache-control: no-storelast-modified 同时出现就是冲突。对要被收录的 HTML,通常该用 public, max-age=...no-cache(重新验证),而不是 no-store

5. 只用 ETag 但漏 Last-Modified

只发 ETag 是合法的,Googlebot 可以用 If-None-Match 来重新验证。但 Last-Modified 是人能读的日期、本身也是个新鲜度提示,两个一起配最稳妥。只发 ETag 算对了一半,缺了那个更易读的校验器。

怎么判断:头里有 ETagLast-Modified。两个都加。

6. 页面由 worker / SSR 函数生成、没设头

Cloudflare Worker 或 Vercel/Next.js Edge function 生成的页默认不加 Last-Modified,而且除非你自己写逻辑,否则它不会算 304。

怎么判断:页面是动态生成的(看有没有 cf-ray 头或函数的 server 头)且 Last-Modified 缺失。

最短修复路径

第 1 步:确认头缺失

curl -sI "https://yoursite.com/article" | head -n 12

Last-Modified 行。没有就需要加。

第 2 步:按平台启用

Vercel — 静态文件默认有。SSR / route handler 在响应里设:

return new Response(html, {
  headers: { 'Last-Modified': new Date(article.modifiedAt).toUTCString() },
});

Netlify — 默认有。用 _headers 文件自定义:

/article/*
  Last-Modified: Wed, 21 Oct 2025 07:28:00 GMT

_headers 不支持按请求变的动态值;值要随文章变时,用 Netlify Function 或 Edge Function。

Firebase Hosting — 在 firebase.json 里声明头:

{
  "hosting": {
    "headers": [{
      "source": "**/*.html",
      "headers": [
        { "key": "Last-Modified", "value": "Wed, 21 Oct 2025 07:28:00 GMT" },
        { "key": "Cache-Control", "value": "public, max-age=600, must-revalidate" }
      ]
    }]
  }
}

Firebase 静态托管不会按文件算日期,所以在 firebase deploy 之前,用 build 脚本从每篇文章的真实修改时间生成 firebase.json(或按 glob 分条)。头配置的最新语法见 Firebase Hosting 缓存文档

GitHub Pages — 不支持自定义响应头。如果 Last-Modified 的准确性很重要,把站点(或至少 HTML 路由)迁到支持的主机,比如 Cloudflare Pages、Netlify,或配了构建期头脚本的 Firebase。

Cloudflare Workers — 手动设到 Response 上:

return new Response(body, {
  headers: {
    'Last-Modified': lastModifiedDate.toUTCString(),
    'Cache-Control': 'public, max-age=3600',
  },
});

第 3 步:值要反映真实内容变化

按每篇文章从真实编辑时间设,绝不要所有页都用构建时间:

const lastModified = new Date(article.modifiedAt || article.publishedAt).toUTCString();

这是最关键的一点。一个正确但到处都相同的时间戳,效果等同于没信号。

第 4 步:用真正的 304 回应 If-Modified-Since

在动态路由上,别只发头,还要处理进来的条件请求,让 Googlebot 拿到便宜的 304

const ims = request.headers.get('If-Modified-Since');
const lastModified = new Date(article.modifiedAt).toUTCString();
if (ims && new Date(ims) >= new Date(lastModified)) {
  return new Response(null, { status: 304, headers: { 'Last-Modified': lastModified } });
}
return new Response(html, { headers: { 'Last-Modified': lastModified } });

静态主机会帮你做这件事;SSR/worker 路由不加就不会做。

第 5 步:别让 CDN 剥掉

Cloudflare -> Caching -> Configuration:确认 Last-Modified 没被 Transform Rule 或 “Remove response headers” 规则去掉。默认情况下 Cloudflare 会保留源站的 Last-Modified 并用自己的 ETag 做重新验证,所以罪魁通常是某条自定义规则,而不是默认行为。

第 6 步:源站和 CDN 都验证一致

# 源站(用源站 IP、账号级的 workers.dev URL,或绕 CDN 的主机名)
curl -sI "https://origin.yoursite.com/article" | grep -i last-modified

# 公网 CDN 服务的 URL
curl -sI "https://yoursite.com/article" | grep -i last-modified

两个都应有 Last-Modified。源站有、公网 URL 没有,就是 CDN 在剥。

怎么确认修好了

复现 Googlebot 实际发的那种请求——条件 GET,确认你拿到 304

# 1. 取当前的 Last-Modified
LM=$(curl -sI "https://yoursite.com/article" | grep -i '^last-modified:' | sed 's/^[Ll]ast-[Mm]odified: //I' | tr -d '\r')
echo "Last-Modified: $LM"

# 2. 带 If-Modified-Since 再请求;预期 "HTTP/2 304"
curl -sI -H "If-Modified-Since: $LM" "https://yoursite.com/article" | head -n 1

返回 304 Not Modified 说明条件抓取现在能用了。返回 200 说明你的服务器(或 CDN)忽略了校验器,Googlebot 会一直付全额抓取成本。改过文章之后,新的 Last-Modified 应严格晚于旧的,然后同一个请求会返回 200,直到下一次条件检查。

接着在 Search Console 闭环:用 URL Inspection -> Test Live URL 确认 Google 抓到了新内容,再盯着 Page indexing 报告,看接下来几天里 “Last crawl”(上次抓取)日期有没有往后推。

预防建议

  • 让 sitemap 的 <lastmod> 保持诚实、每页准确。这是首要的重抓信号;HTTP 头是它底下的效率层。
  • 每次部署后立刻 curl -I 三个代表页面,确认 Last-Modified 存在且每页不同。
  • Last-Modified 从每篇文章真实的 modifiedAt 推导,绝不用构建时间。
  • CDN 配置里不要剥头,也别给要收录的 HTML 钉死 Cache-Control: no-store
  • Last-ModifiedETag 一起,这样 If-Modified-SinceIf-None-Match 任一都能做重新验证。
  • 对 SSR / worker 路由,实现条件 GET 分支,让 Googlebot 真能收到 304

常见问题

加了 Last-Modified,Google 会马上重抓吗? 不会。它主要是让以后的抓取更便宜(靠 304),并给 Google 一个新鲜度校验器。更快的触发器是准确的 sitemap <lastmod> 加上在 Search Console 里手动 Request Indexing。只改头可能要好几天才见效。

Last-Modified 是排名因素吗? 不是。无论是这个头还是 sitemap 的 <lastmod> 都不参与排名。它们影响的是抓取调度和效率,不是名次。新鲜内容可能排得更好,但日期元数据本身不是那根杠杆。

ETag 还是 Last-Modified,该用哪个? 能用就两个都用。ETag 是内容指纹(用 If-None-Match 验证),更精确;Last-Modified 是日期(用 If-Modified-Since 验证),人能读懂。多数会设其一的主机也会设另一个。

我的头在每个页面都是同一个日期,有问题吗? 有。全站相同的 Last-Modified(通常是构建时间)等于没告诉 Google 哪个页面变了,就被当成没信号。要从每篇文章的编辑时间生成这个值。

Firebase Hosting 不肯加 Last-Modified,怎么办? Firebase 不会按文件算 Last-Modified。截至 2026 年 6 月,受支持的做法是用一个读取每篇文章真实修改时间的 build 脚本,把头(按 glob 或按页)写进 firebase.json,再 firebase deploy。Firebase 静态托管没有运行期条件 GET,所以值必须在部署时设好。

为什么我的 SSR 路由对 If-Modified-Since 回的是 200 因为 SSR/edge 函数不会自动处理条件请求。你得读进来的 If-Modified-Since 头,跟文章的 modifiedAt 比较,自己返回 304(见第 4 步)。

相关阅读

标签: #SEO #排查