关于 no-outlinks-audit
no-outlinks-audit 是用于查找博客或内容页面内部链接死角的 SEO 审计 Skill。它按页面框架检测站内出链,并为每个死角页面提供相关目标页面、锚文本和可直接使用的链接文案建议。
主要功能
- 01从站点地图发现内容页面
- 02按框架检测页面内链
- 03验证死角页面识别结果
- 04借助 Ahrefs 研究页面关键词
- 05为死角页生成三条内链建议
适用场景
- 01查找没有站内出链的博客页面
- 02为内容页面规划相关内链
- 03获取带锚文本的链接位置与文案建议
MCP 相关
该 Skill 使用 Ahrefs MCP 的 site-explorer-top-pages 工具获取页面关键词与流量数据。
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ infrasity-labs/ dev-gtm-claude-skills --skill no-outlinks-audit安装后在 Claude Code 中输入/no-outlinks-audit确认已加载。
附带文件README.mdreferences/report-style-reference.html
详细介绍
技能:死胡同页面 — 无站内出链审计
描述
找出网站上正文内容中没有任何指向同一域名下 URL 的出链的所有博客/内容页面。这些页面吸收链接权重,却不传递任何权重——它们是结构性死胡同,会悄然削弱主题权威性,并降低整个网站的抓取效率。
对于每个死胡同页面,该技能会生成 3 条建议:同一网站上死胡同页面应该链接到的页面,包括准确的锚文本(目标页面的首要关键词)、在死胡同页面中放置链接的位置,以及一条可直接粘贴、已嵌入链接的上下文句子。
此技能与孤立页面技能在结构上互为反向:
- 孤立页面:没有入站站内链接的页面(没有人链接到它们)
- 死胡同页面:没有出站站内链接的页面(它们没有链接到任何页面)
工具
- 第 1 步使用 bash_tool 中的 curl 发现站点地图
- 第 2 步使用 bash_tool 中的 curl 检测框架
- 第 3 步使用 bash_tool 中的 curl 检测出站链接(方法因框架而异)
- 第 5 步使用 Ahrefs MCP(
site-explorer-top-pages)进行关键词研究
工作流
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 第 1 步 — 发现所有页面 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
为何需要此步骤 你需要一份完整的博客/内容页面清单,以便检查。基于 curl 的站点地图发现速度快、免费,而且反映的是网站的当前状态,而不是过时的抓取缓存。
子步骤 1A — 查找站点地图
按顺序运行,直到有一个返回有效的 XML:
curl -sL --max-time 15 "https://www.DOMAIN.com/sitemap.xml" | head -50
curl -sL --max-time 15 "https://www.DOMAIN.com/sitemap_index.xml" | head -50
curl -sL --max-time 15 "https://www.DOMAIN.com/robots.txt" | grep -i sitemap
如果都不行,请直接抓取博客索引:
curl -sL "https://www.DOMAIN.com/blog/" | grep -oP 'href="[^"]*"' | grep '/blog/'
子步骤 1B — 提取所有内容 URL
curl -sL "https://www.DOMAIN.com/sitemap.xml" \
| grep -oP '(?<=<loc>)[^<]+' \
| grep '/blog/' \
| grep -v '/blog/$' \
| grep -v '/page/' \
| sort -u > /tmp/blog_urls.txt
wc -l /tmp/blog_urls.txt
将此列表称为 LIST_ALL。记录 TOTAL_PAGES = LIST_ALL 的长度。
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 第 2 步 — 检测网站框架 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
为何需要此步骤 出站链接的检测方法完全取决于网站渲染 HTML 的方式。使用错误的方法会导致结果完全错误——此步骤可以避免这种情况。
运行两项检查:
# Check response headers
curl -sI "https://www.DOMAIN.com" | grep -i "x-powered-by\|x-nextjs\|x-vercel\|server\|generator"
# Check HTML source for framework signals
curl -sL "https://www.DOMAIN.com" \
| grep -o 'name="generator"[^>]*\|__NEXT_DATA__\|_next\|__nuxt\|ng-version\|gatsby'
判定表:
| 发现的信号 | 框架 | 方法 |
|---|---|---|
| 响应头中包含 x-nextjs-prerender | Next.js | 第 3A 步 |
| HTML 中包含 _next 或 NEXT_DATA | Next.js | 第 3A 步 |
| 响应头中包含 x-vercel-cache 且包含 _next | Next.js/Vercel | 第 3A 步 |
| HTML 源码中包含 gatsby | Gatsby (SSG) | 第 3B 步 |
| name="generator" content="WordPress" | WordPress | 第 3B 步 |
| x-powered-by: PHP | 传统 CMS | 第 3B 步 |
| 静态 HTML(存在 标签) | 静态/SSG | 第 3B 步 |
| curl 响应中没有 标签 | 未知 SPA | 第 3C 步 |
| HTML 中包含 __nuxt | Nuxt.js | 第 3C 步 |
重要:运行完整列表前,务必先在 3 个示例页面上测试检测方法。确认一个已知含有链接的页面会返回链接,并且一个已知内容稀少的页面返回的链接很少或没有。如果结果看起来不对,请重新阅读此步骤。
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 第 3 步 — 检测每个页面的出站站内链接 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
运行第 2 步确定的方法。
──────────────────────────────────────── 第 3A 步 — NEXT.JS 网站(RSC 响应头方法) ────────────────────────────────────────
工作原理
使用 React Server Components 的 Next.js 会公开一个服务器渲染的负载端点。通过发送 RSC: 1 请求头,服务器会返回完整的渲染页面数据,序列化为 React 组件树,其中包括所有链接,无需使用无头浏览器。此方法适用于所有使用 App Router 的 Next.js 网站(Next.js 13+)。
对于任何运行在 Next.js / Vercel 上的网站,无论使用 ISR、SSG 还是 SSR,这都是正确的方法。RSC 载荷始终包含已渲染的内容,其中所有内部链接都以纯文本路径的形式显示。
如何验证抓取成功 有效的 RSC 响应:
- 始终大于 10KB(大多数博客文章为 50KB–250KB)
- 以
$Sreact.fragment、J:或1:"$等 React 标记开头 - 如果页面返回的数据少于 10KB,或前 500 个字符中没有
$,则抓取失败——将该页面标记为需人工审核,切勿将其标记为无出站链接页面
import subprocess, re, time
domain = "DOMAIN.com" # without www, e.g. "infrasity.com"
blog_prefix = "/blog/" # content prefix, e.g. "/blog/"
with open('/tmp/blog_urls.txt') as f:
urls = [u.strip() for u in f if u.strip()]
# Asset extensions to exclude — these are not outgoing content links
SKIP_EXT = (
'.png','.webp','.jpg','.jpeg','.gif','.svg','.ico',
'.woff2','.woff','.ttf','.eot','.otf',
'.css','.js','.jsx','.ts','.tsx',
'.xml','.txt','.pdf','.zip','.mp4','.mp3','.webm'
)
# Path prefixes to exclude — assets, CDN, internal Next.js paths
SKIP_PREFIX = (
'/PostImages/', '/images/', '/fonts/', '/favicon',
'/_next/', '/_vercel/', '/static/',
'/api/', '//cdn.', '//assets.'
)
def get_outgoing_links(url):
result = subprocess.run([
'curl', '-sL', '--max-time', '10',
'-H', 'RSC: 1',
'-H', 'Next-Router-State-Tree: %5B%22%22%2C%7B%22children%22%3A%5B%22__PAGE__%22%2C%7B%7D%5D%7D%2Cnull%2Cnull%2Ctrue%5D',
url
], capture_output=True, text=True, timeout=15)
data = result.stdout
# Validate RSC fetch succeeded
if len(data) < 10000 or not any(m in data[:500] for m in ['$S', 'J:', '1:"']):
return None # Fetch failed — flag for manual review
# Extract all paths pointing to the same domain
# Extract both absolute domain links and relative paths starting with /
# (excluding protocol-relative links starting with //)
raw = re.findall(
rf'(?:(?:https?://)?(?:www\\.)?{re.escape(domain)}|(?<!/))/(?!/)([^\\s\"\\\'\\\\),\\]]+)',
data
)
raw = ['/' + r for r in raw]
clean = set()
for l in raw:
# Normalise: strip trailing junk
l = l.split('\\')[0].split('"')[0].split("'")[0].rstrip('),.]\\/')
if len(l) < 2:
continue
# Exclude assets
if any(l.lower().endswith(e) for e in SKIP_EXT):
continue
if any(l.startswith(p) for p in SKIP_PREFIX):
continue
# Exclude self-link
slug = url.rstrip('/').split('/')[-1]
if l.rstrip('/') == blog_prefix.rstrip('/') + '/' + slug:
continue
clean.add(l)
return clean
dead_ends = []
has_outlinks = []
failed = []
for i, url in enumerate(urls):
slug = url.split('/')[-1]
try:
links = get_outgoing_links(url)
if links is None:
failed.append(url)
print(f"[{i+1:3d}] FAILED {slug}")
elif len(links) == 0:
dead_ends.append(url)
print(f"[{i+1:3d}] DEAD-END {slug}")
else:
has_outlinks.append(url)
print(f"[{i+1:3d}] LINKS({len(links):2d}) {slug}")
except Exception as e:
failed.append(url)
print(f"[{i+1:3d}] ERROR {slug}: {e}")
time.sleep(0.15)
with open('/tmp/dead_end_urls.txt', 'w') as f:
f.write('\n'.join(dead_ends))
print(f"\nTotal: {len(urls)}")
print(f"Dead-ends: {len(dead_ends)}")
print(f"Has outlinks: {len(has_outlinks)}")
print(f"Failed: {len(failed)} (verify manually)")
print(f"Dead-end rate: {round(len(dead_ends)/len(urls)*100)}%")
──────────────────────────────────────── 步骤 3B — 静态网站 / WORDPRESS / 传统 CMS(标准 CURL) ────────────────────────────────────────
工作原理 对于直接提供完整渲染 HTML 的网站(静态文件、WordPress、Gatsby、传统 CMS),标准 curl 会抓取包含所有链接的完整页面。仅解析主要内容区域,以排除导航栏、页脚和侧边栏中的链接。
import subprocess, re, time
from urllib.parse import urljoin
domain = "DOMAIN.com"
base = "https://www." + domain
with open('/tmp/blog_urls.txt') as f:
urls = [u.strip() for u in f if u.strip()]
SKIP_EXT = (
'.png','.webp','.jpg','.jpeg','.gif','.svg','.ico',
'.woff2','.css','.js','.xml','.pdf','.zip'
)
def get_outgoing_links(url):
result = subprocess.run([
'curl', '-sL', '--max-time', '10',
'-A', 'Mozilla/5.0 (compatible; InternalLinkBot/1.0)',
url
], capture_output=True, text=True, timeout=15)
html = result.stdout
# Extract main content area only
content = ''
for tag in ['article', 'main', 'body']:
m = re.search(rf'<{tag}[^>]*>(.*?)</{tag}>', html, re.DOTALL | re.IGNORECASE)
if m:
content = m.group(1)
break
if not content:
content = html
hrefs = re.findall(r'<a\\s+[^>]*href\\s*=\\s*["\']([^"\'\\ ]+)["\']', content, re.IGNORECASE)
clean = set()
for href in hrefs:
href = href.strip()
if not href or href.startswith('#') or href.startswith('mailto:') or href.startswith('tel:'):
continue
abs_url = urljoin(base, href)
if domain not in abs_url:
continue
path = '/' + abs_url.split(domain, 1)[-1].lstrip('/')
if any(path.lower().endswith(e) for e in SKIP_EXT):
continue
if abs_url.rstrip('/') == url.rstrip('/'):
continue
clean.add(path)
return clean
dead_ends = []
has_outlinks = []
for i, url in enumerate(urls):
slug = url.split('/')[-1]
links = get_outgoing_links(url)
if links:
has_outlinks.append(url)
print(f"[{i+1:3d}] LINKS({len(links):2d}) {slug}")
else:
dead_ends.append(url)
print(f"[{i+1:3d}] DEAD-END {slug}")
time.sleep(0.2)
with open('/tmp/dead_end_urls.txt', 'w') as f:
f.write('\n'.join(dead_ends))
print(f"\nTotal: {len(urls)}")
print(f"Dead-ends: {len(dead_ends)}")
print(f"Has outlinks: {len(has_outlinks)}")
print(f"Dead-end rate: {round(len(dead_ends)/len(urls)*100)}%")
──────────────────────────────────────── 步骤 3C — 未知框架 / 纯 SPA — 标记并告知用户 ────────────────────────────────────────
如果无法确定框架(例如 Nuxt.js、Angular、Vue SPA),或者 curl 在 3 个样本页面中返回的 <a> 标签少于 10 个,请停止并告知用户:
“此网站似乎使用了客户端渲染,无法通过标准 curl 请求检测。要准确执行此审计,需要以下其中一项:
- 为此域名设置 Ahrefs Site Audit 项目(该工具使用可渲染 JS 的无头浏览器)。首次爬取完成后,请分享项目 ID。
- 从启用了 JavaScript 渲染的 Screaming Frog 导出爬取结果,并提供内部链接 CSV 文件。
- 确认所用框架的确切名称,以便应用正确的检测标头。
在至少 5 个样本页面上确认检测方法有效之前,请勿继续生成报告。”
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 步骤 4 — 验证结果 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
设置此步骤的原因 检测错误不会显现出来。配置错误的资源筛选器或抓取失败,都可能悄无声息地产生错误的无出站链接页面列表。此步骤可在生成建议前发现这些问题。
继续之前,请完成以下所有操作:
-
从 LIST_DEAD_ENDS 中随机选取 3 个页面。在浏览器中打开每个页面,并人工确认页面确实没有指向该域名的出站链接。如果其中任何页面有链接,请重新检查检测脚本的资源筛选器和域名模式。
-
从 LIST_HAS_OUTLINKS 中选取 3 个页面。在浏览器中确认它们包含指向同一域名其他页面的可见链接。
-
如果 DEAD_END_RATE > 80%,则很可能存在问题——抓取结果可能只包含资源链接,或者域名模式可能不匹配。请先调试再继续。
-
如果 DEAD_END_RATE 为 0%,筛选条件可能过宽(将资源或自链接计入了结果)。请检查已知链接较少的页面检测到了哪些链接。
-
对于步骤 3A(Next.js):如果失败列表非空,请人工检查其中的页面。有些页面可能确实没有出站链接,另一些则可能发生了错误。最终确定列表前,请逐个重新抓取失败页面。
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 步骤 5 — 关键词研究 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
设置此步骤的原因 要生成相关的出站链接建议,需要了解每个无出站链接页面以及每个潜在目标页面的主题。一次 Ahrefs 调用即可覆盖整个博客。
工具:Ahrefs:site-explorer-top-pages
参数:
target:博客前缀(例如www.DOMAIN.com/blog/)mode:prefixdate:今天的日期 YYYY-MM-DDselect:url,top_keyword,sum_trafficlimit:500order_by:sum_traffic:desc
结果:MAP_KEYWORDS — 所有页面的关键词和流量映射。
对于 MAP_KEYWORDS 中没有的任何无出站链接页面,请根据 URL slug 推导关键词: 将连字符替换为空格。切勿虚构流量数字——使用 null。 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 第 6 步 — 聚类并生成建议 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
为什么需要这一步 外链建议必须与无出路页面的主题相关。先进行聚类,再在集群内部及集群之间匹配,能生成自然且实用的建议。
如何聚类 根据所有无出路页面的关键词和标题,按主要主题进行分组。根据网站内容的广度创建 5–15 个集群。集群名称应根据实际内容调整,除非确有必要,否则不要使用“杂项”或“其他”等通用名称。
如何生成建议 对于每个无出路页面,从 LIST_ALL 中找出 3 个页面(网站上的任意页面,不限于其他无出路页面),这些页面应满足以下条件:
- 主题相关,涉及相同或相近的主题
- 对无出路页面的读者来说,是自然的下一步阅读内容
- 最好已经是有较多链接的页面,这样无出路页面就能接入更广泛的内部链接图谱
对于 3 条建议中的每一条,请提供:
锚文本 目标页面(被链接到的页面)的首要关键词。规则:
- 使用 MAP_KEYWORDS 中目标页面的
kw字段 - 如果目标页面没有关键词数据,则根据其 URL slug 推导
- 作为句子中的超链接文本时,必须读起来自然
- 锚文本及其周围不得使用 em dash(—)
放置位置 具体说明在无出路页面的哪个章节插入外链。规则:
- 具体到章节,不要只写“文章中的某处”
- 符合上下文逻辑,是读者自然想要深入了解的地方
- 示例: “在介绍核心概念的引言中” “在比较表之后,推荐后续阅读内容时” “在文章末尾的‘工具和资源’部分” “文章中段首次提及[相关主题]时”
建议上下文文案 可直接粘贴到无出路页面指定位置的句子(最多 1–2 句)。规则:
- 必须包含锚文本,并使用指向目标页面 URL 的真实 HTML 超链接: [target_kw]
- 放在“放置位置”所述上下文中时必须读起来自然
- 必须提供实际价值,不能只是“点击此处了解更多”
- 应符合无出路页面的具体语气和章节内容
- 总计 15–35 个词(不包括 HTML 标签)
- 文案中任何位置都不得使用 em dash(—)。请改用逗号,或重新组织句子
- 示例: “如果你刚刚入门,清晰概述 开发者营销策略,就能为有效运用这些策略提供框架。” “将这一做法与稳健的 B2B 内容营销策略相结合的团队,会在自然流量和销售线索两方面获得复合增长。”
将此内容存储为 D[] 中每个建议对象的 copy 字段。
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 第 7 步 — 生成 HTML 报告 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
编写任何代码之前,请阅读: → references/report-style-reference.html
参考文件是所有 CSS 变量、排版、组件样式、间距值和 JavaScript 模式的唯一准则。不要自行设计新样式,也不要替换为其他值。请从参考文件中原样复制。
报告页眉字段
Site URL: https://www.DOMAIN.com/blog/
Total Blogs: TOTAL_PAGES
No Outlinks: DEAD_END_COUNT ← red stat (fail)
Have Outlinks: PAGES_WITH_OUTLINKS ← green stat (pass)
Dead-End Rate: DEAD_END_RATE% ← amber stat (warn)
Suggestions: DEAD_END_COUNT × 3 ← indigo stat (accent)
修复标签(根据使用的方法选择):
- Next.js:
⚑ RSC payload detection (Next.js) · YYYY-MM-DD - Static/WP:
⚑ Curl HTML parse · YYYY-MM-DD - Site Audit:
⚑ Ahrefs Site Audit (project ID: XXXX) · YYYY-MM-DD
报告页脚:
DOMAIN.com · Dead-End Pages Audit · YYYY-MM-DD / Source: [detection method]
建议卡片标签(与孤立页面技能中的标签不同)
- “链接至” — 目标页面(要链接到的页面)
- “锚文本” — 目标页面的首要关键词
- “放置位置” — 无出路页面中的章节
- “上下文文案” — 包含嵌入链接、可直接粘贴的句子
数据结构 — D[] 数组
每个无出路页面对应一个对象:
{
url: "https://www.DOMAIN.com/blog/page-slug", // dead-end page URL
title: "Page Title", // display title
kw: "top keyword", // dead-end page's own keyword
tr: 123, // monthly traffic or null
cluster: "Cluster Name", // must match a key in CC{}
s: [
{
from: "Target Page Title", // page to link OUT TO
fromUrl: "https://www.DOMAIN.com/blog/target", // target URL
anchor: "target page keyword", // target's top keyword
place: "Where in the dead-end page to add the link",
copy: "Sentence with <a href=\"...\">target keyword</a> embedded."
},
// × 3 per dead-end page
]
}
字段语义说明
from 和 fromUrl 指的是目标页面(链接指向的页面),而不是来源页面。这与孤立技能的方向相反。JS 卡片构建器必须将其标记为“链接到”,而不是“来源页面”:
// In the suggestion card builder:
`<div class="sug-from-label">Link To</div>
<div class="sug-from-title">${s.from}</div>
<a class="sug-from-url" href="${s.fromUrl}" target="_blank">...</a>`
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 严格规则 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
-
编写任何 HTML 或 CSS 前,先阅读 references/report-style-reference.html。 这是强制要求,没有例外。
-
检测任何链接前,必须始终先执行步骤 2(框架检测)。绝不要根据域名或对网站的视觉检查来猜测框架。
-
对于 Next.js 网站,必须始终使用 RSC 请求头方法(步骤 3A)。绝不要在 Next.js 网站上使用标准 curl,因为它会返回空的 JS 外壳并产生错误结果。
-
必须始终验证每个页面的 RSC 获取是否成功。响应小于 10KB 或缺少 React 标记,都意味着获取失败。标记这些页面,绝不要默默地将其标记为无出站链接页面。
-
绝不要将资源链接(图片、字体、脚本、样式表)计为符合条件的出站链接。仅链接到 /logo.png 和 /styles.css 的页面属于无出站链接页面。
-
绝不要将自链接(页面链接到自身)计为符合条件的出站链接。
-
生成建议前,必须始终在示例页面上验证结果(步骤 4)。如果无出站链接页面比例高于 80% 或恰好为 0%,则说明存在问题,应先调试。
-
锚文本 = 目标页面的首要关键词,而不是无出站链接页面的关键词。
-
在建议的上下文文案中,绝不要使用长破折号(—)。改用逗号或重组句子。
-
每个无出站链接页面必须始终恰好生成 3 条建议,不多也不少。
-
无出站链接页面的 URL 和全部 3 个目标页面 URL 都必须始终是可点击的
<a href>链接,并在target="_blank"中打开。 -
绝不要偏离 report-style-reference.html 中的 CSS 值。
-
必须始终使用 D[] 和 CC[] 以数据驱动的方式构建报告。绝不要为单个博客卡片编写静态 HTML。
-
生成建议前,必须始终清理 LIST_DEAD_ENDS。移除非 200 页面、分页页面、标签/分类页面,以及任何不是独立文章的 URL。
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 特殊情况 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
NEXT.JS APP ROUTER 与 PAGES ROUTER RSC 请求头适用于 App Router(Next.js 13+)。对于较旧的 Pages Router 网站,可能无法获取 RSC 负载。检测信号:如果多个页面的响应持续小于 10KB,该网站可能使用 Pages Router。可回退为在标准 HTML 响应中检查 NEXT_DATA,其中包含序列化的页面属性,也可能包含链接数据。
网站使用的 CDN 会剥离请求头
某些 CDN 配置会在自定义请求头到达源站前将其剥离。如果 RSC 响应持续小于 10KB,可通过在 URL 末尾附加缓存绕过查询参数进行测试:?_rsc=1。如果仍然失败,CDN 很可能正在剥离 RSC 请求头,应回退为使用 NEXT_DATA 方法。
大型网站(200+ 个页面) 对于博客页面超过 200 个的网站,应分批检测,每批 50 个页面,并在请求之间间隔 0.3s,以避免触发速率限制。优先处理 MAP_KEYWORDS 中流量最高的页面。如果时间有限,则审查流量最高的 100 个页面,并在报告中注明审查范围为前 100 个页面。
所有页面都没有出站链接(比例 = 100%) 这意味着检测完全失败,可能是域名模式有误,也可能是资源过滤器过于严格。运行调试检查:打印 3 个已知包含链接的页面上的原始链接。如果没有任何结果,请修复正则表达式。如果只出现资源链接,请放宽 SKIP_EXT/SKIP_PREFIX 规则。
无出站链接页面比例为 0%
所有页面似乎都有出站链接。过滤器可能过于宽松,可能将导航栏或页脚中的链接也计入了。通过将链接搜索限制在 <article> 或 <main> 标签的内容区域内,添加 is_content 过滤。
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 计算最终指标 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ TOTAL_PAGES = len(LIST_ALL) DEAD_END_COUNT = len(LIST_DEAD_ENDS) PAGES_WITH_OUTLINKS = TOTAL_PAGES - DEAD_END_COUNT DEAD_END_RATE = round(DEAD_END_COUNT / TOTAL_PAGES * 100) TOTAL_SUGGESTIONS = DEAD_END_COUNT × 3