内链孤岛排查:从访问日志找出全站没人链接的页面

一个搜不到的词,让我发现了整站的链接断层

几个月前我给站点加了一个新栏目,写了几篇自认为质量不错的教程。按照以往的经验,新文章发布后一周内就会有零星的自然搜索流量进来。结果这一次完全反常:两周过去,那个栏目在搜索里一条收录都没有,连「site:域名 关键词」都搜不到任何一篇文章。

我先怀疑是抓取问题:查日志,蜘蛛确实来了,状态码 200,没有拦截;查 sitemap,那几篇文章都在里面且 lastmod 正确;查 robots.txt,没有任何屏蔽。三件事全都正常,但收录就是不来。直到我做了一件事——把那一栏目的所有文章列出来,逐一检查「除了首页导航,还有哪些页面能链接到它」,答案让我有点尴尬:零。除了那个栏目列表页,全站没有任何一篇文章链向它们。

这就是内链断层。这篇文章讲的是我后来总结出的一套内链体检与修补方法:怎么用数据而不是靠感觉找出「孤岛页面」、锚文本该怎么写才有效、什么时候该加、加多少会过头,以及为什么内链对收录的影响远比你想象的大。

一、内链为什么能影响收录:从爬虫的行走方式说起

搜索引擎的爬虫进入一个站,是从少数入口(首页、外部链接指向的页面、sitemap 里的 URL)出发,沿着页面里的 <a href> 一层层走下去。它不读你的分类逻辑,也不理解你的栏目规划,它只认「从这个页面能不能走到那个页面,需要几步」。

所以对爬虫来说,一个站点实际上是一张有向图。首页是根,深度为 0;首页直接链接的页面深度为 1;再往下递推。爬虫的抓取资源是有限的,它会优先抓深度浅、被链接多的页面,深在第五层、只有一条入口的页面,可能要等上几周甚至永远排不上。

更关键的是「权重传递」这件事。虽然各家搜索引擎都不承认有一个叫 PageRank 的数值,但「被更多页面链接的页面,在抓取优先级和排序上都更有优势」这一点,从行为上是可观测的。一个只有栏目页一条链接的页面,拿到的信号是「这个站自己都不太看重它」。

我在修复前后做过一次对比:同样是五篇新教程,加内链之前,第一篇被收录用了十九天;给每篇补了三到五条来自相关内容页的链接之后,同栏目下一批新文章平均收录时间缩短到四天。这个数字不是严格的实验,但差异足够大,大到不能归结为偶然。

二、第一步:把站点的链接图画出来

不要靠肉眼看页面找孤岛,几百篇文章的时候眼睛一定会骗你。正确的做法是从日志或者站点结构里把链接关系抽出来,做成一张表,然后用 SQL 问问题。

最省事的方案是直接解析 Nginx 日志里的 Referer。它记录了「用户是从哪个页面点进这一页的」,虽然不是爬虫视角,但对找孤岛非常有效——如果一篇文章在三个月里从来没有一个站内 Referer,那它大概率就是个孤岛。

# 从访问日志里统计每条路径的站内来源分布(近 30 天)
zcat -f /var/log/nginx/access.log*.gz /var/log/nginx/access.log \
| awk -F'"' '$2 ~ /^GET / {
    split($2, a, " ");
    uri = a[2];
    ref = $4;
    sub(/^https?:\/\/[^\/]+/, "", ref);       # 去掉域名,只留路径
    if (ref != "" && ref != uri) print uri "\t" ref;
}' \
| sort | uniq -c | sort -rn \
| awk '$1 >= 2 {print}' \
| head -40

这条管道的输出是「目标路径 → 来源路径 → 次数」的列表。反过来用,就能找出「只有站外来源和直接访问、没有站内来源」的页面:

# 找出近 30 天被访问过、但没有任何站内 Referer 的页面
# 第一趟:记录所有被访问的路径
zcat -f /var/log/nginx/access.log*.gz /var/log/nginx/access.log \
| awk -F'"' '$2 ~ /^GET / {split($2,a," "); print a[2]}' | sort -u > /tmp/all_uri.txt

# 第二趟:记录被站内其他页面链到过的路径(即出现过的 Referer 目标)
zcat -f /var/log/nginx/access.log*.gz /var/log/nginx/access.log \
| awk -F'"' '$2 ~ /^GET / && $4 ~ /yourdomain\.com/ {
    split($2,a," "); sub(/^https?:\/\/[^\/]+/, "", $4);
    if ($4 != a[2]) print a[2];
}' | sort -u > /tmp/has_inlink.txt

# 差集就是候选孤岛页面
comm -23 /tmp/all_uri.txt /tmp/has_inlink.txt \
| grep -E '\.html$' | grep -v -E '^/(tag|page)/' | head -40

分两趟写虽然多扫一遍日志,但逻辑清楚得多,出问题也容易单独调试。核心就是一个集合运算:被访问过的页面 - 有站内入链的页面 = 候选孤岛。

如果你不想碰日志,还有一条更轻的路:从数据库里把文章列表和正文都取出来,用正则或者 DOM 解析抽出正文里的站内链接,构成「A 链向 B」的关系,再算每个页面的入链数。这个方法的好处是它看到的是爬虫看到的(模板产出的链接不算正文链接),坏处是拿不到真实的访问热度。

两个方法结合起来最准:日志告诉你「用户实际怎么走」,正文抽取告诉你「作者安排了怎么走」。两者的差集,就是结构上的问题。

三、第二步:区分四种「孤岛」,处理方式完全不同

找到孤岛之后不要急着全部加链接。孤岛至少有四种,每一种的成因和修法都不一样。

第一种,真正的新页面。刚发布的文章还没有任何人链过去,这是正常的。处理方式是「发布即内链」:写新文章时,顺手在正文里找一个语义相关的老文章位置插一条链接。这一步花不了两分钟,但效果比事后补十条都强。

第二种,被遗忘的老页面。这类只出现在 sitemap 里,没有任何人链接。它们往往是早期写的不错的文章,只是后来的栏目改版时链接被删掉了。处理方式是在同主题的新文章里补链,同时在栏目页保证它排在合理位置。

第三种,结构性的死胡同。比如「标签页」「归档页」「分页的第二页之后」。这些页面本身不该承载权重,处理方式是给它们加 noindex 或者在 robots 里排除,而不是硬加内链。

第四种,本该是孤岛。比如隐私政策、免责声明、联系页面。这些页面不需要内链,也不需要流量,加了反而稀释。判断一个页面该不该有内链,标准是「它是不是有价值的内容页」,不是「它有没有被链接」。

我第一年做这件事的时候犯的错就是一刀切:把所有没有入链的页面都加了一遍链接,结果栏目页被塞了几十条例行「相关文章」,用户点击率反而下降。后来改成只处理前两类,栏目页保持精简,效果明显好得多。

四、第三步:锚文本怎么写才有用

内链的价值一半在「链」,一半在「锚文本」。搜索引擎通过锚文本理解目标页面讲什么,也通过它判断来源页与被链页的相关性。写得好的锚文本能让一条链接顶十条无意义链接。

几条我实践下来比较管用的原则:

用目标页面的核心关键词或它的自然变体。链向一篇讲 Nginx 缓存的文章,锚文本写成「Nginx 缓存怎么配」远比「点击查看」有用。但不要整站所有地方都用一模一样的锚文本,那看起来像刻意堆砌。同一个页面可以有「Nginx 缓存」「缓存配置」「proxy_cache 那篇」几种写法,这是自然的。

锚文本要出现在句子里,而不是单独一行。「关于这个问题,可以看看Nginx 缓存那篇」这种自然嵌入,比「相关阅读:xxx」的列表块更有价值。后者我仍然保留,但只放在正文末尾,不作为主要的内链形式。

不要用 title 属性代替锚文本。很多主题模板会把标题塞进 title="",但搜索引擎对 title 属性的权重远低于锚文本本身,鼠标悬停才看得到的内容也不是用户能看到的。

数量控制:一篇文章三到五条站内链接足够。多了会让正文变得像目录,而且会稀释每条链接的信号。我在一篇文章里最多出现过八条,那篇的正文阅读体验明显下降,后来减到五条以内。

<!-- 推荐的写法:锚文本在句子中,语义完整 -->
<p>如果请求延迟高但 CPU 不高,先按
<a href="/2106.html">慢请求四层定位</a>那篇的顺序拆一遍,
再决定要不要动 PHP-FPM 的参数。</p>

<!-- 不推荐的写法:锚文本是「这里」「点击查看」 -->
<p>更多内容请<a href="/2106.html">点击这里</a>。</p>

五、第四步:让内链自动长出来,而不是靠人手补

手工补链在文章数量超过一百之后就不现实了。我的做法是把「相关文章」做成一个基于标签和关键词的自动推荐,然后定期抽查质量。

最简单的实现不依赖任何插件:在数据库里维护一张「文章-关键词」的关联表,文章发布时从标题和正文里抽取几个关键词,推荐时按共同关键词数量排序,取前五条,并排除已经在正文里链过的。

-- 推荐候选:共同关键词数降序,排除已链过的
SELECT p.cid, p.title, COUNT(*) AS overlap
FROM post_keyword a
JOIN post_keyword b ON a.keyword = b.keyword AND a.cid <> b.cid
JOIN contents p ON p.cid = b.cid
WHERE a.cid = ?            -- 当前文章
  AND b.cid NOT IN (SELECT target FROM post_link WHERE source = ?)
  AND p.status = 'publish'
GROUP BY p.cid, p.title
ORDER BY overlap DESC, p.created DESC
LIMIT 5;

这段 SQL 的效果取决于关键词表的质量。我的做法是关键词不用自动抽取,改成手工给每篇文章打三到五个标签,成本很低但质量高得多。标签不追求多,只求能准确描述主题。

这里有个坑要提醒:自动推荐一定要去重和限流。我第一版没有做「排除已链」的条件,结果同一篇文章的正文里和推荐区里出现了同一个链接,页面上看起来像是故障,实际上只是模板没想到这种情况。

六、第五步:验证有没有效果,看三个数

改完之后不能凭感觉说「好像有用了」,要拿数据说话。我固定看三个数:

一是新页面的收录天数。从发布到第一次被蜘蛛抓取的时间,以及从被抓取到出现在搜索结果里的时间。内链修好之后,这两个数都应该下降。

二是孤岛页面的数量。每个月重跑一遍前面的分析脚本,看「零站内入链」的内容页面还剩多少。这个数字应该逐月收敛,收敛到只剩新发布的文章为止。

三是内链页面的点击率。在日志里统计正文内链被点击的次数相对于该页面浏览量的比例。如果加了链但没人点,说明锚文本写得不对或者放的上下文不自然,需要重新调整。

# 从 Referer 里统计某个来源页的所有站内出链点击
zcat -f /var/log/nginx/access.log*.gz /var/log/nginx/access.log \
| awk -F'"' -v src="/2106.html" '$4 ~ src {
    split($2, a, " "); print a[2];
}' | sort | uniq -c | sort -rn | head -15

七、三个反直觉的结论

结论一:内链比外链更容易见效,尤其是在新站。外链要等别人,内链完全在自己控制之内。我在新栏目上先做内链,两周内收录就开始改善;而那批外链争取了两个月,效果反而更慢。

结论二:链接数量不是越多越好,「相关」才是核心。一百条来自无关页面的链接,不如五条来自同主题页面的链接。搜索引擎判断相关性的依据是上下文,而不是数量。

结论三:内链也是给用户用的,不只是给爬虫。做内链的时候我最后都会自问一句:一个读到这里的真人,会不会想点这个链接?如果答案是不会,那这条链接对搜索引擎的帮助也有限,因为它的点击率会很差。

八、小结:内链本质上是内容规划的一部分

写到这里,我最大的体会是:内链不是一个技术优化项,它是内容规划的一部分。你在写一篇文章的时候,心里应该清楚它属于哪个主题簇、它会链向谁、谁将来会链向它。这个习惯建立起来之后,孤岛页面几乎不会再出现。

如果你现在就想动手,最小可行的一步是:把最近发布的十篇文章列出来,检查每一篇的正文里有没有链向任何一篇同主题的老文章。如果没有,今天就补上三到五条。这十篇文章补完,你已经能感觉到差别了。

Last modification:November 9th, 2026 at 10:23 pm

Leave a Comment