你更新了一篇文章。过了几天。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-Modified 和 ETag;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-store,Last-Modified 就没意义了——客户端根本不缓存,Googlebot 也没法发出有用的条件请求。
怎么判断:
curl -sI "https://yoursite.com/article" | grep -iE 'cache-control|last-modified'
cache-control: no-store 和 last-modified 同时出现就是冲突。对要被收录的 HTML,通常该用 public, max-age=... 或 no-cache(重新验证),而不是 no-store。
5. 只用 ETag 但漏 Last-Modified
只发 ETag 是合法的,Googlebot 可以用 If-None-Match 来重新验证。但 Last-Modified 是人能读的日期、本身也是个新鲜度提示,两个一起配最稳妥。只发 ETag 算对了一半,缺了那个更易读的校验器。
怎么判断:头里有 ETag 没 Last-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-Modified配ETag一起,这样If-Modified-Since或If-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 步)。