Tag 归档页 0 篇文章:清理后空架——审计、设最低门槛、用 410 让 Google 快速删除

tag 页能渲染但 0 篇已发布文章。审计每 tag 计数、加 prebuild 最低门槛、对空归档返回 410 Gone,让 Google 尽快删掉。

你审计 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 Gone410 是让 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-toolsai-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 有 12 篇。两者都低于你将在 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

一个单纯停止构建的页面,通常会返回你托管层的 404404 也行,但 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 状态码(301302303307308),没有 410!。要用一个 Pages Function(或 Worker 路由)对这些死路径返回 new Response("Gone", { status: 410 })

  • Firebase Hosting —— firebase.json 里的 redirects 只支持 301302308。要返回真正的 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 → RemovalsNew requestTemporarily remove URL。这会把它从搜索里隐藏、并清掉缓存快照,约 6 个月——但它是临时的,只是争取时间;永久移除仍来自 410/404。两者搭配用:临时移除立即隐藏,410 彻底去索引。

如何确认已修复

  1. 重跑审计(Step 1)。每个仍有路由的 tag 都应报告 >= MIN;不该有 ORPHAN/THIN 行对应到一个已构建的页面。
  2. 构建后 grep 产物。 构建后确认死 tag 的 HTML 没了:find dist -path '*tags/deprecated-tag*' 应该什么都不返回。
  3. 检查状态码。 curl -I https://yoursite.com/en/tags/deprecated-tag/ 应返回 HTTP/2 410(如果走了那条路就是 404),而不是 200
  4. 检查 sitemap。 打开 /sitemap.xml(或 sitemap 索引),确认没有 <loc> 指向门槛以下的 tag。
  5. 盯 Search Console。 接下来 1-2 周,tag URL 在 Soft 404Crawled - 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-tool vs ai-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 更早重抓。

相关

标签: #内容运营 #站点质量 #站点审计 #排查 #标签页