新文章不收录:内链深度与链接孤岛排查

一个SEO结构调整,三个月后才爆出来

今年三月底我为了「让文章页更干净」,把博客模板里侧边栏的「相关文章」模块挪到了正文底部,同时把每篇文章底部的「上一篇/下一篇」链接改成只显示标题、不再显示指向分类页的链接。改动上线后,页面更美观,加载更快,我自己很满意。

四月中旬,Google Search Console 的「已发现但尚未编入索引」从 300 多条涨到 1400 多条,「抓取统计信息」的日均抓取请求从 2600 次掉到 900 次。首页展示量在一个月内从每天 1100 降到 280。我去查了页面本身:标题没变、内容没变、服务器没挂、robots.txt 没动、状态码全 200。

最后定位到的原因很朴素:我把站内的链接结构改乱了,新文章进入「抓取路径」的深度从 2 层变成了 5 层,爬虫不愿意爬了。

这篇讲怎么系统地看「爬虫到底能不能走到你的页面」,以及那些不体现在页面 HTML 上、但直接影响收录的结构性问题。不谈关键词、不谈外链,只谈路径。

收录问题的三个层次,先分清是哪一层

「新文章不收录」这句话太笼统。在 GSC 的「页面编入索引」报告里,问题被分成几类,每一类对应完全不同的原因和修法:

  • 已发现但尚未编入索引:爬虫知道这个 URL 存在(从 sitemap 或某个链接看到的),但没去抓,或者抓了没索引。多数是抓取预算问题或链接质量太差。
  • 已抓取但尚未编入索引:抓了,内容也拿到了,但 Google 判断不值得索引。这是内容质量问题,或者是重复内容、canonical 指错。
  • 重复网页,Google 未选择与用户所选规范网页相同:你的页面和别的页面太像,或者站内两个 URL 内容一样。
  • 已排除(被 noindex 或 robots 阻止):自己把自己挡住了。

这四类的修法完全不同。第一类是路径问题(本篇主题),第二类要改内容,第三类要修 canonical,第四类去查模板。

判断是否属于「路径问题」有一个很直接的信号:看新文章发布后的 48 小时内,服务器日志里有没有 Googlebot 的请求。如果一篇新文章发了三天,日志里一次 Googlebot 都没出现,那就是爬虫根本不知道或者不愿意来;如果来了但没索引,才需要往内容层面查。

# 新文章发布 48 小时后,查日志里有没有爬虫来过
sudo grep 'Googlebot' /var/log/nginx/access.log | grep '/20261015.html' | head -5
# 顺便看看它抓了什么、拿到什么状态码
sudo awk '$9==200 && /Googlebot/ {print $7}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -20

第二条命令统计的是「Googlebot 最近抓了哪些页面、各抓了几次」。如果前 20 名里全是首页、分类页、标签页,一篇文章都没有,说明爬虫进来了但没往深处走。

内链深度:为什么新文章会被饿死

搜索引擎的爬虫有预算。它每天愿意花在你站上的请求次数是有限的,而且这个额度会根据你的站点质量、响应速度、更新频率动态调整。爬虫进来之后,会按「链接深度优先」的方式往下走,深度每增加一层,被抓的概率就掉一半。

页面深度从 2 层变 5 层,听起来只是「多点了三次」,实际上概率是从「几乎必抓」掉到「可能不抓」。我那次改动的具体影响是这样的:

  • 原来:首页 → 分类页 → 文章。文章在第二层,每个分类页上有 30 篇文章的链接,爬虫从首页进去能一次摸到 300 篇。
  • 改后:首页只显示 8 篇最新文章(我把「最新文章」区块缩短了),分类页的入口在侧边栏,而侧边栏在移动端是折叠的(爬虫按桌面 UA 抓,能看到,但链接被隐藏到 display:none 的菜单里)。文章到第三层甚至第四层。

Google 能抓 display:none 里的链接,但优先级会降低。更致命的是:我把「上一篇/下一篇」里的分类链接去掉了,等于切断了文章之间横向互相传递权重的通道,所有文章变成孤岛,只能从分类页进入。

怎么量自己的内链深度

不用装工具,一个爬虫脚本二十行就够了。核心是记录每个 URL 的「最短入链深度」和「入链数量」。这里用 Python 的 urllib 就够了,不需要 scrapy。

import urllib.request, urllib.parse, re, collections

def fetch(url):
    req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'})
    with urllib.request.urlopen(req, timeout=10) as r:
        return r.read().decode('utf-8', 'replace')

start = 'https://www.example.com/'
depth = {start: 0}
queue = collections.deque([start])
links_count = collections.Counter()

while queue:
    page = queue.popleft()
    if depth[page] > 4:
        continue
    try:
        html = fetch(page)
    except Exception:
        continue
    for href in re.findall(r'href="(/[^"#?]*)"', html):
        u = urllib.parse.urljoin(start, href)
        links_count[u] += 1
        if u not in depth:
            depth[u] = depth[page] + 1
            queue.append(u)

deep = {u: d for u, d in depth.items() if d >= 3}
print('depth>=3 页面数:', len(deep))
for u, d in sorted(deep.items(), key=lambda x: -x[1])[:20]:
    print('depth=%d 入链=%d %s' % (d, links_count[u], u))

这段脚本的核心是 depth 和 links_count 两个字典:前者记录每个 URL 的最短入链深度,后者统计它被多少页面链接过。重点是:如果爬出来的页面里有大量深度 3 以上的 URL,而它们又只有 1 个入链,这些页面基本不会被抓。修复方法很具体——在首页或分类页给它们加一个入口。

三类链接孤岛,和它们的症状

深度问题之外,还有三类「看起来有链接、实际上爬不到」的孤岛:

用JS渲染的链接

如果模板用了前端框架,链接是运行时生成的,Googlebot 虽然能执行 JS,但也有预算。而且它执行 JS 是排在「先抓 HTML」之后的第二遍,第二遍的预算小得多。判断方法很简单:curl 抓下来的 HTML 里没有这些链接,就说明它们是 JS 生成的。

# 直接看 HTML 源码里有没有目标链接,不执行 JS
curl -s https://www.example.com/ | grep -c 'href="/2026'
# 返回 0 说明链接全在 JS 里生成

只靠「加载更多」的翻页

分类页最下面一个「加载更多」按钮,点一下 AJAX 拉一页。这种设计对爬虫是灾难:它不会点按钮。修法是给每个「加载更多」配一个真链接,指向一个可分页的 URL,并且加上 rel="next" 与 rel="prev"(虽然 Google 已经不用这两个标签做索引信号,但它至少是一个真实可抓的 URL)。

只有文章正文里偶然提到的链接

有些站的新文章只靠老文章正文里提到才被爬到。如果那个老文章本身没被索引,链条就断了。这类孤岛在日志里表现为「某个时间的文章突然集中被爬」,因为某一篇被重新抓取,链条上的其它文章跟着被访问。

这三类的共同特点:页面本身完全正常,只是在「链接图谱」上是孤立的。用上面的脚本跑一遍,把入链数为 1 且深度 >= 3 的 URL 列出来,就是待修复清单。

sitemap不是万能药,但用好了很有用

很多人以为「提交了 sitemap 就一定会收录」,实际不是。sitemap 只是「告诉爬虫这些 URL 存在」,它会进「已发现」队列,但抓不抓、什么时候抓,还是看你的站点和链接结构。

不过 sitemap 有两个确实有效的用法:

  • 只放 200 状态码、可索引、canonical 自指的 URL。如果一个 sitemap 里混着 404、重定向、noindex 页面,Google 会降低对这个 sitemap 的信任度,整体抓取频率都会受影响。这是很多站长忽略的一点。
  • lastmod 必须真实。如果你每次全量提交都把 lastmod 设成今天,Google 会认为这个字段不可信,直接忽略。只更新真正改过的页面,并且 lastmod 用 ISO 8601 带时区。
# 检查 sitemap 里的 URL 是否全部 200(抽样 30 条)
curl -s https://www.example.com/sitemap.xml | grep -oE '<loc>[^<]+' | sed 's/<loc>//' | head -30 | \
while read u; do printf "%s %s\n" "$(curl -o /dev/null -s -w '%{http_code}' "$u")" "$u"; done | sort | uniq -c -w3

返回里如果出现 301、404,就说明 sitemap 里混了坏 URL,先清掉再谈收录。

用日志验证:爬虫到底在不在来

所有关于 SEO 的判断,最后一律回到服务器日志。GSC 的数据有延迟(通常是 2~3 天),日志是实时的。

需要看的四个数:

  • Googlebot 每天的请求总量:这个数掉了一半,说明站点在爬虫那儿的评价下降了。
  • Googlebot 抓到的 4xx/5xx 比例:超过 5% 就会被降频。
  • 抓取的 URL 分布:多少是首页/分类,多少是文章。文章占比太低就是深度问题。
  • 有没有抓到「不该抓的」:搜索页、带参数的 URL、session ID 页面,这些会吃掉大量预算。
# 每日四数:总量、状态码分布、抓取最多的路径前缀
LOG=/var/log/nginx/access.log.1
echo "== 总请求"
grep -c 'Googlebot' $LOG
echo "== 状态码"
awk '/Googlebot/ {print $9}' $LOG | sort | uniq -c
echo "== 抓得最多的前 10 个路径"
awk '/Googlebot/ {print $7}' $LOG | sed 's#/\([0-9]\+\)\.html#/ARTICLE#' | sort | uniq -c | sort -rn | head -10

这条命令我每天看一次,跑了一个月后,图景很清楚:Googlebot 的请求里 62% 是文章页(/ARTICLE),12% 是分类页,剩下的分类标签和搜索页。这就是健康的分布。如果文章页占比掉到 20% 以下,说明爬虫在门口打转,不往深处走。

那个改动的具体修法

找到原因之后,修复很简单,花了两个小时:

  • 在首页恢复「最新 20 篇」区块,全部用真链接(<a href>),不折叠、不加 display:none。
  • 每篇文章底部恢复「同分类的 5 篇」链接,这既是给用户的相关推荐,也是横向内链,能显著降低平均深度。
  • 分类页的分页从「加载更多」改成真实分页 URL /category/tech/page/2/。
  • 检查并提交了新的 sitemap,把 404 和重定向的 URL 全部清掉。

效果不是立刻的。修复上线后第一周,GSC 的抓取量只回到 1400;第二周 1900;第三周接近原来的 2600。展示量恢复得更慢,因为排名需要重新计算,前后大概用了六周才回到原来的水平。

这里有一个很重要的心理预期:结构性问题修复后,收录和流量恢复的周期以「周」为单位,不是以「天」。很多人修完三天没看到变化就认为「没用」,然后又去改别的地方,结果变量太多,彻底无法归因。我的做法是每次只改一处,记录上线日期,观察三周再动下一处。

每周十五分钟的结构体检

最后给一份可以每周照着做的清单。全部用现成命令,不需要工具:

  • 第一件:抽新发的五篇文章,curl 看 HTML 源码里是否有指向它们的链接(不是 JS 生成),以及它们距离首页多少跳。
  • 第二件:看 GSC 的「已发现但尚未编入索引」数量,如果一周涨了 10% 以上,去查 sitemap 里的坏 URL 和新页面有无入链。
  • 第三件:日志里 Googlebot 的 5xx/4xx 占比,超过 3% 就查相应 URL。
  • 第四件:随机抽三个文章页,curl -sI 看 canonical 是不是自己,跳转链是不是 0。
  • 第五件:确认 sitemap 里没有 ?s=、?page=、session 之类的参数 URL。

五件事加起来十五分钟,但能提前几周发现问题。我那次事故的代价是一个月流量下滑,如果当时每周做一次第一条检查(新文章在 HTML 里有没有链接),改动当天就会发现问题。

结构是地基。内容和外链决定你能爬多高,但结构决定爬虫能不能走到你面前。地基塌了,再怎么优化标题也没用。

Last modification:October 24th, 2026 at 07:26 pm

Leave a Comment