一个搜不到的词,让我发现了整站的链接断层
几个月前我给站点加了一个新栏目,写了几篇自认为质量不错的教程。按照以往的经验,新文章发布后一周内就会有零星的自然搜索流量进来。结果这一次完全反常:两周过去,那个栏目在搜索里一条收录都没有,连「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七、三个反直觉的结论
结论一:内链比外链更容易见效,尤其是在新站。外链要等别人,内链完全在自己控制之内。我在新栏目上先做内链,两周内收录就开始改善;而那批外链争取了两个月,效果反而更慢。
结论二:链接数量不是越多越好,「相关」才是核心。一百条来自无关页面的链接,不如五条来自同主题页面的链接。搜索引擎判断相关性的依据是上下文,而不是数量。
结论三:内链也是给用户用的,不只是给爬虫。做内链的时候我最后都会自问一句:一个读到这里的真人,会不会想点这个链接?如果答案是不会,那这条链接对搜索引擎的帮助也有限,因为它的点击率会很差。
八、小结:内链本质上是内容规划的一部分
写到这里,我最大的体会是:内链不是一个技术优化项,它是内容规划的一部分。你在写一篇文章的时候,心里应该清楚它属于哪个主题簇、它会链向谁、谁将来会链向它。这个习惯建立起来之后,孤岛页面几乎不会再出现。
如果你现在就想动手,最小可行的一步是:把最近发布的十篇文章列出来,检查每一篇的正文里有没有链向任何一篇同主题的老文章。如果没有,今天就补上三到五条。这十篇文章补完,你已经能感觉到差别了。