博客 SEO 实战:GSC 索引诊断、i18n 清理与 IndexNow 推送

背景

zcblog 上线之后,GSC 隔三差五报索引问题。顺便把 IndexNow 推送和 GitHub Pages 部署也一并整了。这篇文章就是把排查过程记下来,省得以后忘了。

GSC 索引覆盖诊断

GSC 报的索引问题分了四类:

问题 1:robots.txt 屏蔽页 → 不需要处理

8 个页面被 robots.txt 屏蔽,全是 /redirect/?url=... 跳转中转页。这些页面本就该被屏蔽,外链源头来自 zmki.cn 等外部站点,不影响本站。

robots.txt 配置:

User-agent: *
Allow: /
Disallow: /redirect/
Sitemap: https://marxchou.com/sitemap-index.xml

问题 2
页面 → 死链修复

两篇删除的文章仍被 Google 索引,返回 404。处理方式:

  • vercel.json 或构建配置中添加 301 重定向,指向首页或相关文章
  • 或者直接在 GSC 中提交移除请求

问题 3:虚假英文版 URL —— 最坑的一个

Google 索引了大量 /en/article/... 页面,但文章全是中文,Google 觉得这些页面质量不行。

三个层面同时生成了虚假英文版 URL:

  1. @astrojs/sitemap i18n 配置 —— 为所有页面生成了 /en/ sitemap 条目
  2. BaseHead.astrohreflang 标签 —— 告诉搜索引擎 “此页有英文版”
  3. Astro i18n routing —— 自动生成 /en/article/... 路由,但内容并未翻译

而实际上 zcblog 没有一篇文章设了 lang: en,26 篇全是中文。

修复步骤:

步骤 1:sitemap 过滤,只包含当前语言

astro.config.mjs
import sitemap from "@astrojs/sitemap";
export default defineConfig({
integrations: [
sitemap({
i18n: {
defaultLocale: "zh",
locales: {
zh: "zh-CN",
},
},
filter: (page) => {
// 不生成 /en/ 页面的 sitemap
return !page.startsWith("https://marxchou.com/en/");
4 collapsed lines
},
}),
],
});

步骤 2:清理 BaseHead 的 hreflang

<!-- 只在确实有翻译时才输出 hreflang -->
{
pageLang === "en" && (
<link rel="alternate" hreflang="en" href={enUrl} />
)
}

步骤 3:英文版页面加 noindex

---
// 如果文章没有英文翻译,不索引英文版路由
const shouldIndex = frontmatter.lang !== "en" &&
!Astro.url.pathname.startsWith("/en/");
---
{!shouldIndex && <meta name="robots" content="noindex" />}

问题 4:http:// 旧链接 → 外部引用

部分外部站点引用使用了 http:// 而非 https://。本站已全局强制 HTTPS(Vercel 自动处理),并在 BaseHead 中声明 canonical:

<link rel="canonical" href={new URL(Astro.url.pathname, Astro.site)} />

GSC 虚假英文版 URL 的三个源头

IndexNow 集成:主动告诉搜索引擎

传统的 SEO 是被动的 —— 更新了网站只能等爬虫路过。IndexNow 反过来了,你更新完直接推送给搜索引擎。

原理

传统方式:网站更新 → 等爬虫路过 → 可能几天后才收录
IndexNow:网站更新 → POST 请求通知搜索引擎 → 几乎实时处理

支持的搜索引擎有 Bing、Yandex、Seznam。Google 不直接支持 IndexNow,但据说会通过 Bing 的索引间接收到信号。

集成到 Astro 构建流程

在 Astro 集成中挂载构建完成后的钩子,自动提交新 URL:

src/integrations/indexnow.ts
import type { AstroIntegration } from "astro";
const INDEXNOW_KEY = import.meta.env.INDEXNOW_KEY;
const SITE_URL = "https://marxchou.com";
export default function indexnow(): AstroIntegration {
return {
name: "indexnow",
hooks: {
"astro:build:done": async ({ pages }) => {
const urls = pages
.filter((p) => !p.pathname.startsWith("/en/"))
.map((p) => `${SITE_URL}${p.pathname}`);
20 collapsed lines
const key = INDEXNOW_KEY;
if (!key || urls.length === 0) return;
// 提交给 IndexNow
await fetch("https://api.indexnow.org/indexnow", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
host: new URL(SITE_URL).hostname,
key,
keyLocation: `${SITE_URL}/${key}.txt`,
urlList: urls,
}),
});
console.log(`IndexNow: submitted ${urls.length} URLs`);
},
},
};
}

同时需要在网站根目录放置 key 验证文件 /{key}.txt,内容是 key 本身。

IndexNow 主动推送 vs 传统等爬虫

GitHub Pages 双仓部署架构

zcblog 的部署从 Vercel 单仓演进到 GitHub Pages 双仓架构:

架构

你写文章 (Obsidian)
↓ git push
zcblog-articles (私有仓)
│ push 触发 repository_dispatch
zcblog (公开仓)
│ GitHub Actions 构建
GitHub Pages (marxchou.com)

关键设计

1. 文章仓私有、代码仓公开

文章源码(.md + 图片)放在 zcblog-articles 私有仓,代码放在 zcblog 公开仓。这样:

  • 草稿不会提前泄露
  • 图片资源不占用公开仓库 LFS 配额
  • 公开仓代码可被 fork 学习

2. 构建触发

zcblog-articles 收到 push 后,通过 repository_dispatch 事件通知 zcblog

zcblog-articles/.github/workflows/notify.yml
- name: Trigger build
uses: peter-evans/repository-dispatch@v3
with:
token: ${{ secrets.PAT }}
repository: smart-chou/zcblog
event-type: articles-updated

zcblog 监听该事件自动构建部署:

zcblog/.github/workflows/deploy.yml
on:
repository_dispatch:
types: [articles-updated]
push:
branches: [main, theme]

3. theme 分支独立部署

theme 分支用于主题开发预览,推送后单独构建部署到 theme.marxchou.com,不影响主站。

之前踩的坑

一开始用的是 peaceiris/actions-gh-pages 推到外部仓库 smart-chou.github.io。后来发现 GitHub Pages 内置托管更省事 —— 仓库 Settings → Pages 里选分支和目录就行,不需要额外 action 和外部仓库。

双仓部署流水线

后面注意

  • 每季度看一眼 GSC,关注索引覆盖和核心网页指标
  • sitemap 别让它脏了,删掉的页面要及时清
  • IndexNow key 换了的话记得更新 key 文件
  • robots.txt 的 Disallow 定期审一遍,别误伤