跳到正文
曾亮懿 ZengLiangYi Blog

打破 Google 收录僵局:基于 Astro 与 Indexing API 的增量缓存推送实践

利用 Astro 构建期生成的 sitemap lastmod 时间戳与本地状态缓存,精准推送增量与未收录页面,打破静态博客收录停滞并杜绝配额浪费。

曾亮懿

曾亮懿

阅读约 4 分钟
目录 展开 收起

对于内容更新频繁的独立静态博客,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 条。如果每次流水线都盲目全量提交:

  1. 配额瞬间见顶:单次构建即触碰 429 报错,排在后面的新文章无法送达。
  2. 无效重复刷新:未曾修改的历史文章重复占用抓取资源,稀释未收录页面的优先级。
  3. 低质页面抢占预算:标签归档页、分页目录等薄内容(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 set updatedAt: YYYY-MM-DD in its frontmatter. This controls <lastmod> in the sitemap and informs SEO indexers of content changes.

将该规则明确沉淀后,日常无论是手工撰写还是 Agent 辅助润色,均会在修改内容时同步校准日期,从而使得“内容修改 -> Sitemap 产出 -> 增量比对 -> Google 爬虫触达”形成全自动的正向闭环。

分享:

从文章到代码

到我的 GitHub 看看 ChatCrystal、开发者工具和日常学习记录,欢迎交流实现细节。