对于内容更新频繁的独立静态博客,Google Search Console(GSC)中最常见的困局莫过于“已发现 - 尚未编入索引”。爬虫通过 Sitemap 或外链发现了页面地址,但受限于站点初始权重与抓取预算(Crawl Budget),大量长尾文章被排入漫长的等待队列,数周乃至数月未见收录推进。
在 GSC 网页端手动进行“网址检查 -> 请求编入索引”虽然能插队优先抓取,但每日仅有 10–15 次配额,手动复制粘贴低效且不可持续。引入自动化推送是必然选择,但若缺少合理的过滤与增量机制,又会迅速撞上 API 配额墙。本文介绍如何结合 Astro 的构建输出与本地状态缓存,构建一套零配额浪费的增量推送流水线。
认识 Google Indexing API 与免费配额上限
Google 官方提供了 Web Search Indexing API,支持开发者主动向搜索引擎通知页面的更新(URL_UPDATED)或下线(URL_DELETED)。尽管官方文档最初声明该接口主要针对招聘信息(JobPosting)与直播活动(BroadcastEvent),但在全球 SEO 与开发者实践中,该 API 被广泛证实能够强行唤醒 Googlebot,在数小时至两天内对常规网页发起高优先级的抓取与渲染。
然而,每个 Google Cloud 项目对该 API 的默认免费配额为 每日 200 次请求。如果一个站点拥有上百篇文章、加上多语言对照、分页和标签聚合页,总 URL 数量会迅速超过 300 条。如果每次流水线都盲目全量提交:
- 配额瞬间见顶:单次构建即触碰 429 报错,排在后面的新文章无法送达。
- 无效重复刷新:未曾修改的历史文章重复占用抓取资源,稀释未收录页面的优先级。
- 低质页面抢占预算:标签归档页、分页目录等薄内容(Thin Content)消耗了本应留给深度长尾博文的宝贵额度。
第一层过滤:剔除聚合噪点,锁定核心内容
在推送前,必须从站点 URL 树中剥离低价值目录。对于基于内容集合(Content Collections)构建的 Astro 博客,核心内容是各语言下的博文与独立项目页:
function isEligibleContentPage(url) {
if (!url) return false;
// 排除标签聚合页、列表分页等薄内容
if (url.includes('/tag/') || url.includes('/page/')) {
return false;
}
// 严格匹配博文详情页与项目展示页,排除 /blog/ 与 /projects/ 列表主页
const isBlogArticle =
/^https:\/\/zengliangyi\.com\/(?:[a-z]{2}\/)?blog\/[a-z0-9-]+(?:\/)?$/.test(url) &&
!url.endsWith('/blog/') &&
!url.endsWith('/blog');
const isProjectPage =
/^https:\/\/zengliangyi\.com\/(?:[a-z]{2}\/)?projects\/[a-z0-9-]+(?:\/)?$/.test(url) &&
!url.endsWith('/projects/') &&
!url.endsWith('/projects');
return isBlogArticle || isProjectPage;
}
通过这一层剪枝,全站几百个 URL 会被收敛为 130 多篇具备完整图文结构的实质页面,从源头确保每次 API 调用都作用于最具搜索价值的长尾资产。
第二层比对:读取构建期生成的 sitemap lastmod
如何准确判断一篇文章是否被修改过?文件系统的 mtime 在 CI 或容器重新检出时会全部失效,不能作为内容修改依据。
在 Astro Rocket 的内容体系中,核心知识沉淀在 blog 内容集合内。构建期通过 src/content.config.ts 为博客提供 publishedAt 与可选的 updatedAt。构建集成(scripts/article-sitemap.mjs)解析生产 HTML 头的 BlogPosting 元数据,将准确的修改日期注入 dist/client/sitemap-0.xml 的 <lastmod> 标签中:
<url>
<loc>https://zengliangyi.com/blog/tailwind-v4-vite-setup/</loc>
<lastmod>2026-10-04T00:00:00.000Z</lastmod>
</url>
推送脚本直接解析该 XML 产物,提取每个有效 URL 及其对应的 lastmod 字符串。对于拥有确定时间戳的博文,这提供了精准比对的绝对依据;而对于尚未配置 lastmod 的展示页面,则由下层的状态缓存提供首次入库与兜底保护。
第三层持久化:本地状态缓存与增量决策
我们在本地维护一个轻量级的状态缓存文件 .indexing-cache.json(已加入 .gitignore 防止凭证与本地状态污染代码库)。该文件记录每个 URL 上次成功推送的时间及当时对应的 lastmod:
{
"https://zengliangyi.com/blog/tailwind-v4-vite-setup/": {
"lastmod": "2026-10-04T00:00:00.000Z",
"submittedAt": "2026-10-13T07:16:30.000Z"
}
}
在执行推送前,核心比对逻辑如下:
const pending = [];
for (const item of allItems) {
const cached = cache[item.url];
if (!cached) {
// 1. 从未提交过的新页面或未入库页面
pending.push({ ...item, reason: 'new/unsubmitted' });
} else if (item.lastmod && cached.lastmod && new Date(item.lastmod) > new Date(cached.lastmod)) {
// 2. 页面内容修改,更新了 updatedAt / lastmod
pending.push({ ...item, reason: 'updated' });
} else if (!cached.lastmod && item.lastmod) {
// 3. 历史缓存无 lastmod,本次构建补齐了时间戳
pending.push({ ...item, reason: 'lastmod-attached' });
} else {
// 4. 无任何变更,跳过
skippedCount++;
}
}
该机制带来了显著的工程优势:
- 首轮平滑过渡:初次运行时,脚本按配额提交前 200 条;次日再次运行,自动跳过昨日已入库条目,继续消化剩余未推送内容。
- 日常极速静默:日常构建若无文章改动,比对耗时小于 10 毫秒且立即退出,不产生任何网络请求。
- 内容更新即响应:一旦某篇文章修正了代码或补充了细节,并在 frontmatter 标记了新的
updatedAt,脚本就会在下一次构建后精确锁定并推送这单篇内容。
纳入团队与 Agent 规范
自动化脚本的高效运行,高度依赖数据源的真实性。如果开发者或 AI Agent 修改了已有博文却未更新日期,增量流水线就无法感知变更。
因此,我们在仓库治理规范(AGENTS.md)中确立了硬性约束:
Content updates require
updatedAt: When modifying an existing blog post or project, update or setupdatedAt: YYYY-MM-DDin its frontmatter. This controls<lastmod>in the sitemap and informs SEO indexers of content changes.
将该规则明确沉淀后,日常无论是手工撰写还是 Agent 辅助润色,均会在修改内容时同步校准日期,从而使得“内容修改 -> Sitemap 产出 -> 增量比对 -> Google 爬虫触达”形成全自动的正向闭环。