你审计 tag 页,发现 23 个显示 “No articles found”。它们引用的 tag 来自你后来删掉的文章,或从未发布的草稿。这些空页仍在渲染、仍躺在 sitemap 里,Google 也仍在抓取。在 Search Console 的 Page indexing 报告里,它们标为 Crawled - currently not indexed,更糟的是 Soft 404——Google 看到一个返回 200 OK 却没有真实内容的页面,于是把它当成”不算页面”处理。
最快修复: 不再生成低于最低文章数的 tag 页(在 tag 路由的 getStaticPaths 里设 MIN = 3)、把这些 tag 从 sitemap 移除、对 Google 已经索引过的 tag URL 返回 410 Gone。410 是让 Google 快速删除的关键——截至 2026 年 6 月,410 通常 1-2 周内被删(抓取频繁的站点有时 3-5 天),而 404 需要 2-4 周。
完整修复分两层:回填(审计存量空 tag,再合并或 410)+ 预防(一条 prebuild 规则,让空 tag 页今后根本无法生成)。
你属于哪一类
先跑 Step 1 的审计脚本,再对照下表:
| 现象 | 可能原因 | 跳到 |
|---|---|---|
tag 计数为 0,文章最近被删 | 用这个 tag 的文章被删了 | 原因 1 |
tag 计数为 0,但文件还在 | 那些文章全是 draft: true | 原因 2 |
| 两个几乎相同的 tag,一个空 | 拼写 / 单复数重复 | 原因 3 |
很多 tag 恰好只有 1 篇 | 生成器对每个 frontmatter 值都建页、无下限 | 原因 4 |
| 老 tag URL 报 404,无文章引用它 | 改了 tag 名但没加 redirect | 原因 5 |
常见原因
1. 用了某 tag 的文章被删
你批量删了薄文,其中有些是某 tag 的唯一用户,那个 tag 的归档页现在 0 内容。
如何判断: 列出 frontmatter 用到的每个 tag、查文章计数(见 Step 1)。任何仍生成了路由但计数为 0 的 tag 就是 orphan。
2. 用某 tag 的文章全被设为草稿
一次质量行动把某些文章设为 draft: true。如果这些恰好是某 tag 最后的已发布用户,即便 .mdx 文件还在磁盘上,tag 页也会渲染”无文章”空态。
如何判断: 同一脚本,但计数时排除 draft: true。有文件但已发布计数为 0 的 tag 属于这一类。
3. tag 分类有拼写制造重复
你有 ai-tools 和 ai-tool 两个 tag。复数有文章,单数只有一篇散落文章或没有。一个归档页空着,另一个有内容。
如何判断: 导出所有 tag 值、排序、扫近似重复:
grep -rhoE 'tags:.*' src/content/articles/ | tr -d '[]"' | tr ',' '\n' | sed 's/tags://' | sort | uniq -c | sort -n
4. tag 生成器为每个 frontmatter 值都自动建页
你的 tag 生成器遍历 frontmatter,对每个不同 tag 值建一页、没有最小门槛。一次性 tag(单篇文章里的 tags: ["experimental-feature"])会得到一个大概率永远空着的 tag 页。
如何判断: 按 tag 计数文章。1 篇是”瘦 tag”,0 篇是 orphan。两者都是弱归档页,Google 会当成低价值。
5. 改 tag 名没加 redirect
你把 chatgpt 改成 chat-gpt(或反向)。老 tag 页不再匹配任何文章,于是从构建里消失,却还留在 sitemap 和 Google 索引里——现在变成 404 或 soft 404。
如何判断: 把 sitemap 或 Search Console 已索引 URL 里出现的 tag slug,和当前文章 frontmatter 里的 tag 对比。任何已被索引但当前不再引用的,就是改名后被遗弃的 URL。
最短修复路径
Step 1:盘点 orphan 和瘦 tag
先生成一份 tag 使用报告,这样可以逐个 tag 决策而不是靠猜。下面这段按 tag 统计已发布文章数,并标出所有低于门槛的:
// scripts/audit-tag-usage.mjs -> 运行: node scripts/audit-tag-usage.mjs
import fs from "node:fs";
import path from "node:path";
import matter from "gray-matter";
const MIN = 3; // 与路由 + sitemap 的门槛保持一致
const counts = new Map(); // key: `${lang}:${tag}`
function walk(dir) {
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
const p = path.join(dir, e.name);
if (e.isDirectory()) { walk(p); continue; }
if (!p.endsWith(".mdx")) continue;
const { data } = matter(fs.readFileSync(p, "utf8"));
if (data.draft) continue; // 只统计已发布
const lang = data.lang || "en";
for (const t of (data.tags || [])) {
const key = `${lang}:${t}`;
counts.set(key, (counts.get(key) || 0) + 1);
}
}
}
walk("src/content/articles");
for (const [key, n] of [...counts.entries()].sort((a, b) => a[1] - b[1])) {
if (n < MIN) console.log(`${n < 1 ? "ORPHAN" : "THIN"} ${key} -> ${n} article(s)`);
}
orphan tag 有 0 篇已发布文章;瘦 tag 有 1 或 2 篇。两者都低于你将在 Step 3 强制的下限。
Step 2:逐个 tag 决定——合并、去索引、还是回填
对每个 orphan 或瘦 tag,三选一:
- 合并到相似 tag:在每篇文章的 frontmatter 里改名,然后对老 tag URL 返回 410。
- 去索引:不再生成它(Step 3)+ 从 sitemap 移除(Step 4)+ 对它返回 410。
- 回填:如果这个 tag 确实有价值,写 2+ 篇真文章让它越过下限。
合并通常最好:它折叠近似重复、把内链权重集中到一个归档页上,还在同一步里清掉了被遗弃的 URL。回填只留给你真心想做成主题枢纽的 tag。
Step 3:加一条 prebuild 规则——tag 需要 N+ 篇才渲染
在 tag 路由的 getStaticPaths 里强制一个最小值(3 篇已发布是合理的下限),让空 tag 页从一开始就无法被构建:
// src/pages/[lang]/tags/[tag].astro
import { getCollection } from "astro:content";
export async function getStaticPaths() {
const all = await getCollection("articles");
const MIN = 3; // 与审计 + sitemap 同一个数
const counts = new Map();
for (const a of all) {
if (a.data.draft) continue;
for (const t of (a.data.tags || [])) {
const key = `${a.data.lang}:${t}`;
counts.set(key, (counts.get(key) || 0) + 1);
}
}
return [...counts.entries()]
.filter(([, n]) => n >= MIN)
.map(([key]) => {
const [lang, tag] = key.split(":");
return { params: { lang, tag } };
});
}
现在只有 3+ 篇已发布文章的 tag 才有页面。空 tag 和瘦 tag 页在构建期就不再存在,Google 也就没东西可抓了。
Step 4:sitemap 里去掉被去索引/移除的 tag
生成 sitemap 的代码必须套用同一个 MIN 过滤,否则你会一直在 sitemap 里宣传那些已经不再构建的 URL:
// src/pages/sitemap.xml.ts (草图) —— 复用 Step 3 里完全相同的过滤
const tagPaths = /* 与 getStaticPaths 相同的 n >= MIN 列表 */;
// 只为这些发 <url>;绝不列出门槛以下的 tag。
干净的 sitemap 能阻止对死 tag 的新抓取。但 Google 已经索引的 URL 会一直留着,直到它重新抓取并收到移除信号——那就是 Step 5。
Step 5:对已被索引的 tag URL 返回 410 Gone
一个单纯停止构建的页面,通常会返回你托管层的 404。404 也行,但 410 Gone(“永久移除、不会回来”)是更强、更快的信号——截至 2026 年 6 月,Google 大约 1-2 周内删掉 410。
多数人忽略的坑:怎么发出 410 取决于你的托管商,而多数静态 _redirects 文件根本发不了 410。
-
Netlify ——
_redirects(或netlify.toml里的[[redirects]]块)确实支持强制410。结尾的!会在匹配到文件时也强制返回该状态码:/en/tags/deprecated-tag/ /en/tags/deprecated-tag/ 410! /zh/tags/deprecated-tag/ /zh/tags/deprecated-tag/ 410! -
Cloudflare Pages —— 截至 2026 年 6 月,它的
_redirects只支持 3xx 状态码(301、302、303、307、308),没有410!。要用一个 Pages Function(或 Worker 路由)对这些死路径返回new Response("Gone", { status: 410 })。 -
Firebase Hosting ——
firebase.json里的redirects只支持301、302、308。要返回真正的410,必须把路径rewrite到一个调用res.status(410).send("Gone")的 Cloud Function / Cloud Run 服务。
如果发 410 不方便,普通 404 加干净 sitemap 也能让 URL 被删除——只是慢一两周。不要用 robots.txt 屏蔽一个仍被索引的 URL:那会阻止 Google 抓取它,于是它永远看不到 410/404,页面反而可能一直留在索引里。
Step 6:(可选)用 GSC Removals 工具加速移除
如果某个死 tag URL 此刻还在排名或可见、你想今天就把它藏起来,打开 Search Console → Removals → New request → Temporarily remove URL。这会把它从搜索里隐藏、并清掉缓存快照,约 6 个月——但它是临时的,只是争取时间;永久移除仍来自 410/404。两者搭配用:临时移除立即隐藏,410 彻底去索引。
如何确认已修复
- 重跑审计(Step 1)。每个仍有路由的 tag 都应报告
>= MIN;不该有ORPHAN/THIN行对应到一个已构建的页面。 - 构建后 grep 产物。 构建后确认死 tag 的 HTML 没了:
find dist -path '*tags/deprecated-tag*'应该什么都不返回。 - 检查状态码。
curl -I https://yoursite.com/en/tags/deprecated-tag/应返回HTTP/2 410(如果走了那条路就是404),而不是200。 - 检查 sitemap。 打开
/sitemap.xml(或 sitemap 索引),确认没有<loc>指向门槛以下的 tag。 - 盯 Search Console。 接下来 1-2 周,tag URL 在
Soft 404和Crawled - currently not indexed下的数量应该下降。对其中一个被移除的 tag 用 URL Inspection,确认 Google 现在看到的是410。
预防
- tag 路由和 sitemap 共用同一个
MIN常量(3 是合理下限),让它们永远不会跑偏。 - 若有 tag 意外跌破下限就让 CI 失败——把 Step 1 脚本加进
prebuild,发现新 orphan 时非零退出。 - 改 tag 名必须在同一个 PR 里更新每一篇用它的文章,并为老 slug 加上
410。 - 季度 tag 审计:合并近似重复、修剪瘦 tag、退役一次性 tag。
- 用受控词汇表校验 frontmatter 的 tag 值,让
ai-toolvsai-tools这类拼写在生成页面之前就被拒掉。
FAQ
移除的 tag 页该用 410 还是 404?
两者都会去索引 URL。410 Gone 表示”永久移除”,Google 通常处理得更快(截至 2026 年 6 月,约 1-2 周 vs 404 的 2-4 周)。tag 彻底不要了就用 410;以后可能恢复就用 404。绝不要让它继续返回 200。
为什么不直接给空 tag 页加 noindex 然后留着?
可以——noindex 在下次抓取后约 3-7 天让页面被删。但一个 0 篇文章的空归档即使去了索引也是累赘:它仍浪费抓取预算、对用户毫无价值。整条移除路由(Step 3)加 410,比留着一个空壳页面干净得多。
我的 _redirects 文件忽略了 410,为什么?
几乎肯定是因为你在 Cloudflare Pages 或 Firebase Hosting 上,它们的静态 redirect 文件只支持 3xx 状态码(Firebase 另外通过自定义页面提供 404)。只有 Netlify 的 _redirects 能发出强制的 410!。在其它平台上,要从 Pages Function / Worker(Cloudflare)或 rewrite 到 Cloud Function(Firebase)来返回 410。
去索引这些空 tag 会伤到站点其它部分吗?
不会——恰恰相反。移除 0 篇和低内容的归档页会提升你的平均页面质量、把抓取预算集中到真文章上。唯一风险是不小心 410 了一个确实有文章的 tag,所以退役任何东西前先用 Step 1 确认计数。
Search Console 多久才不再标记它们?
一旦 Google 重新抓取、看到 410 和清理过的 sitemap,tag URL 在 Soft 404 / Crawled - currently not indexed 下的数量应在 1-2 周内下降。可以用 URL Inspection → Request indexing 推一下某个具体 URL,让 Google 更早重抓。