内链腐烂:文章链向已改名/已删除的 slug——301 重定向 + CI 断链检查

重命名 slug 没回头更新 47 篇 linker——内链一半 404。用 linkinator 或 lychee 在 CI 跑、维护 redirects 文件、prebuild 内链断链直接 fail。

读者点你文章底部的”相关:GPT Tips”——落到 404。你 6 个月前把那个 slug 改成了 chatgpt-tips,但没回头更新 47 篇链到旧 gpt-tips 的文章。Search Console 默默记着 404——Google 开始把”链向破坏邻居”的页面从索引里丢。你的内部 PageRank 图从里头烂掉了。

内链腐烂是无声的:build 不挂、页面正常渲染、坏链躺在文章底部多数读者根本不滚到的位置。但每一条悬空链都是泄漏的权威信号 + 更差的用户体验。修法:CI 跑链检查(linkinator 或 lychee)、维护 redirects.json 配重命名 slug、prebuild 在任何内链指向不存在 slug 时 fail。

常见原因

1. 改了 slug 没更新引用方

你把 gpt-tips.mdx 改成 chatgpt-tips.mdx——所有指向 /en/articles/gpt-tips/ 的文章现在 404。无 build error、无重定向、无 warning。

如何判断:grep 旧 slug。

grep -r "/articles/gpt-tips/" src/content/articles/ | wc -l

2. 删了一篇文章没查入链

你把一篇薄文废了。43 篇还链着它——这些链现在 404。

如何判断:删前扫入链。

grep -r "/articles/SLUG-TO-DELETE/" src/content/articles/

非零——必须更新或重定向。

3. Markdown 链接目标拼错

你写了 /en/articles/chatgpt-tipss/(多了一个 s)——页面照常渲染、build 不校验、用户点完 404。

如何判断:只有真链接检查器抓得到。用 frontmatter urlSlug 做正则比对得到合法 slug 全集。

4. 外链腐烂(第三方网站搬了页)

SEO 影响小,但 UX 仍然糟。你链到 https://example.com/great-article/,对方重构——现在 302 到首页或 404。

如何判断:linkinator 或 lychee 加 --check-external

5. 重命名章节的 anchor-only 链接

你链到 /en/articles/foo/#step-3。文章被重写——## Step 3 现在叫 ## Step 3: Verify,anchor slug 变成 step-3-verify——anchor 404 但悄无声息:页面加载但只跳顶。

如何判断:anchor 检查器更少;lychee 加 --include-fragments 能抓。

最短修复路径

Step 1:在构建产物上跑 lychee 或 linkinator

装并跑 lychee:

# https://github.com/lycheeverse/lychee
brew install lychee
npm run build
lychee --offline --include-fragments dist/**/*.html > link-report.txt

或 JS 原生选 linkinator:

npx linkinator dist --recurse --silent --skip "^https://(facebook|twitter)" > link-report.txt

输出每条悬空 URL——按入链数排序优先处理。

Step 2:为改名 slug 建 redirects 文件

gpt-tips 改成 chatgpt-tips——对的答案是 permanent 301,不是重写每个 linker。维护 public/_redirects(Netlify/Cloudflare)或 astro.config.mjs 的 redirects:

// astro.config.mjs
export default defineConfig({
  redirects: {
    '/en/articles/gpt-tips/': '/en/articles/chatgpt-tips/',
    '/zh/articles/gpt-tips/': '/zh/articles/chatgpt-tips/',
  },
});

301 保留权威。redirect 上线后悬空链还能用、Google 转发 link equity——可以慢慢修文本(也可以不修)。

Step 3:prebuild 在内链断链直接 fail

重定向救改名;prebuild 检查抓拼写和删除。用 frontmatter 建 slug 索引,校验正文每条内链:

# scripts/check-internal-links.mjs
import fs from "node:fs";
import path from "node:path";
import matter from "gray-matter";

const validSlugs = new Set();
const articleDirs = [
  "src/content/articles/en/troubleshooting",
  "src/content/articles/zh/troubleshooting",
];
for (const dir of articleDirs) {
  for (const f of fs.readdirSync(dir)) {
    const { data } = matter(fs.readFileSync(path.join(dir, f), "utf8"));
    if (data.urlSlug) validSlugs.add(`/${data.lang}/articles/${data.urlSlug}/`);
  }
}

let broken = 0;
for (const dir of articleDirs) {
  for (const f of fs.readdirSync(dir)) {
    const txt = fs.readFileSync(path.join(dir, f), "utf8");
    const links = [...txt.matchAll(/\(\/(?:en|zh)\/articles\/([^)#]+)\/?\)/g)];
    for (const m of links) {
      const url = `/${m[0].split("/")[1]}/articles/${m[1]}/`;
      if (!validSlugs.has(url)) {
        console.error(`BROKEN in ${f}: ${url}`);
        broken++;
      }
    }
  }
}
process.exit(broken > 0 ? 1 : 0);

接到 prebuild——PR 落不了悬空内链。

Step 4:按批修复存量

每个断链目标:

- 改名:加 redirect,正文不动
- 已删但内容仍有价值:从 git 恢复
- 主动删除:grep-and-sed 批量更新 linker——指向他处或剥链

不要留 404——要么 redirect 要么重写。

Step 5:重提 sitemap + 请求重抓

修完后在 Search Console 重提 sitemap、对最坏的几页请求 indexing——让 Google 重抓看到干净邻居。

预防

  • 每个 PR 在 CI 跑 lychee 或 linkinator——内链断链 fail
  • prebuild 内链校验器对 frontmatter slug 索引
  • 改 slug 必须同 PR 加 redirects 条目——lint 规则强制
  • 删除清单:先扫入链、redirect 或重写再 merge
  • 外链检查每周跑一次(非每 PR——太抖)允失败
  • 季度回顾 redirects 表:折叠链路(A -> B -> C 改成 A -> C)

相关

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