站内死链排查与404页面设计:从软404到收录恢复

上个月有个读者的站彻底掉了,不是慢,是没流量。他给我看 Search Console,整片红:抓取异常里 4xx 数量从每天 20 条涨到了 9000 多条。他自己点开站内链接都正常,用浏览器访问随便哪个页面都能打开,所以一开始完全不信是死链问题。

最后查出来的原因是他半年前把 URL 结构从 /archives/{cid}/ 改成了 /{cid}.html,老链接没有做跳转,而是直接返回了首页内容——HTTP 状态码 200。搜索引擎看到的是「同一个首页、九千个不同地址」,于是把大量重复内容判成低质量,整站权重被拖垮。

这件事让我意识到,死链处理是个人站最容易忽略、又最容易被判罚的一块。这篇文章把我自己的死链排查、404 页面设计和恢复流程写下来。

先把三种「打不开」分开

很多站长把所有「打不开」都叫死链,但搜索引擎对这三种情况的处理完全不同,修法也完全不同。分开是排查的第一步。

第一种:真死链,服务器明确说不存在

返回 404 Not Found 或 410 Gone。这是最干净的状态。爬虫看到 404 会把这个 URL 从索引里移出去,站点不会因此受罚——404 本身不是错误,是正常的互联网状态。

410 比 404 更明确一点:它表示「这个资源曾经存在,现在永久删除了,别再来」。Google 的文档里说过 410 的处理速度可能比 404 快一些,因为不需要再观察。但实践中差别不大,我一般只在明确删除了大批内容时用 410。

第二种:软 404,返回 200 但内容说「没有」

这是最危险的。服务器返回 200 OK,页面上写着「文章不存在」或者干脆重定向到首页却保留 200。爬虫会把它当成一个正常的、内容很少的页面收录进去。

为什么危险?因为它制造了三种伤害:一是浪费抓取预算,一个软 404 要花掉跟正常页面一样的抓取资源;二是制造重复内容(如果我有一万条老链接都返回主页内容,那就是一万个重复页面);三是让「已发现未编入索引」的计数暴涨,你去看 GSC 会看到一大堆深链接卡在那里不动。

判断软 404 有个很实用的技巧:看字节数和内容特征。同一批「不存在」的 URL 返回的内容长度往往完全一致(都是那个默认的「暂无内容」模板),而且多个不同 URL 的响应体完全相同。用 curl 抓几个对比 md5 就能确认。

第三种:重定向链,一跳变五跳

每多一跳重定向,抓取预算就多消耗一份,而且链路上任何一跳出问题,整条链断掉。我见过最夸张的一条:http:// → https:// → http://www. → https://www. → https://www./archives/123/ → https://www./123.html,五次跳转。

规范的站应该做到任一 URL 最多一跳到达最终地址。这需要把 http→https、裸域→www、旧结构→新结构 三条规则合并成一条直接跳转到最终地址,而不是串成链。

怎么把死链批量找出来

靠人工点击这辈子都找不完。我用三套办法,覆盖不同的来源。

办法一:翻访问日志里的 404

这是最快、最真实的一份清单,因为它是真实访客和爬虫刚刚踩过的坑:

awk '$9 == 404 {print $7}' /var/log/nginx/access.log \
  | sort | uniq -c | sort -rn | head -50

注意两个细节:一是 $9 的位置取决于你的 log_format,combined 格式里状态码是第 9 个字段,但如果自己加过字段就变了,先用 head -1 看一眼真实的一行再决定;二是要连轮转过的 access.log.1 一起查,只看当天会漏掉昨天的那批。

如果站上装了 CDN,源站日志里看到的 404 会少很多,因为大部分请求被 CDN 挡了。这时候要么去 CDN 控制台看日志,要么在 CDN 上开一个「未命中回源」的采样。

办法二:抓页面里的每一个链接,逐个验证

日志只覆盖「有人访问过」的链接,页面里那些从来没人点的死链抓不到。所以还需要主动扫:把站内所有文章页抓一遍,抽出所有 <a href>,去重后逐个请求。

import re, urllib.request, urllib.parse
from concurrent.futures import ThreadPoolExecutor

seen, broken = set(), []
def check(url):
    try:
        req = urllib.request.Request(url, method='HEAD',
              headers={'User-Agent': 'Mozilla/5.0 (compatible; LinkCheck/1.0)'})
        code = urllib.request.urlopen(req, timeout=10).status
        if code >= 400:
            broken.append((code, url))
    except urllib.error.HTTPError as e:
        broken.append((e.code, url))
    except Exception as e:
        broken.append(('ERR', url))

with ThreadPoolExecutor(max_workers=8) as pool:
    pool.map(check, sorted(seen))

千万不要用 -c 50 这种高并发去扫自己的站,那跟 DDoS 没区别,云厂商的 WAF 会直接把你 IP 封掉,某些情况下还会触发安全告警。八个并发、每请求间隔几百毫秒,一万个链接大概跑十几分钟,完全可以接受。

另外注意 HEAD 请求的问题:有些老站或者带 WAF 的站对 HEAD 返回 405,看起来像死链其实是误报。我的做法是 HEAD 拿到 4xx 之后再用 GET 复核一次,只有 GET 也失败才算真死链。

办法三:Search Console 的 404 报告

GSC 的「网页」报告里有一个「未找到 (404)」分类,列出的是Google 曾经抓取过、现在 404 的 URL。这份清单的价值在于:它包含外部链接指向的地址,也就是别人的文章里链接到你的、但现在打不开的页面。这类链接是外链资产,处理优先级最高——因为它本来能带来流量和权重。

这份报告导出有上限(一次 1000 行),站大了要分几次导。我一般每月导一次,跟本地日志比对,看有没有新的批次出现。

404 页面本身该怎么设计

找到死链只是第一步,404 页面怎么呈现,其实直接影响到损失多少。

必须返回真正的 404 状态码

这是最容易出错的地方。Nginx 里如果你写了:

location / {
    try_files $uri $uri/ /index.php;
}

那么一个不存在的 /abc.html 会走到 /index.php,PHP 返回 200,页面上显示「文章不存在」——这就是软 404。正确的写法是让 PHP 在判断文章不存在时显式发 header('HTTP/1.1 404 Not Found');,或者对静态站直接写:

location / {
    try_files $uri $uri/ =404;
}
error_page 404 /404.html;

注意 error_page 404 /404.html 这一行,/404.html 本身必须能访问,否则会形成「404 的 404」,Nginx 会返回一个内部错误。而且这一行不会改变返回的状态码,仍然是 404,这是对的。

页面上要放什么

一个好的 404 页面应该做三件事,顺序很重要:

  • 明确告诉用户「找不到」,不要用「页面正在建设中」这种含糊话术,用户会以为是网站坏了。
  • 给出搜索框和最新文章列表——大部分踩到 404 的人其实想找的是某个主题,给他一个入口比让他返回首页更有用。
  • 给出首页和分类页链接,让用户有台阶下。

但有一件事绝对不能做:不要在 404 页面上堆几百个内链,或者把全站文章列表都铺出来。有些 SEO 教程说「404 页面要充分利用权重」,这是过时的思路。大量的、无关的内链会稀释页面权重,而且会让爬虫在 404 页上继续抓取,浪费预算。

noindex 与 nofollow

404 页面本身返回 404 状态码,搜索引擎自然不会收录它,不需要再加 noindex。但如果因为某些原因你的站只能返回 200(比如用了静态托管服务不支持自定义状态码),那就必须加 <meta name="robots" content="noindex">,至少避免被收录成重复内容。

另外 404 页面上的链接建议加 rel="nofollow",避免爬虫顺着这些链接继续在死链区打转。

死链出现了,该怎么处理

找到死链之后有三种处理方式,怎么选取决于这个 URL 有没有价值。

有价值的:做 301 跳到最相关的页面

判断标准是「有没有外链指向它」「有没有历史流量」。如果一条老链接被别的站引用过,或者过去三个月有过点击,那它有价值,应该 301 跳到内容最接近的现存页面。

关键点:跳到「最相关」而不是「首页」。把所有死链都 301 到首页,是 SEO 里最糟糕的做法之一——搜索引擎会把它识别为软 404 的变体,而且用户点进去发现不是他要的内容会立刻离开,跳出率暴涨。

批量做 301 我用 Nginx 的 map,比一条条写 location 干净得多:

map $uri $redirect_to {
    default                "";
    ~^/archives/(\d+)/$    /$1.html;
    /old-category/         /tech/;
    /about-me.html         /about/;
}

server {
    if ($redirect_to != "") {
        return 301 $redirect_to;
    }
}

这里 if 在 server 块里配合 return 是 Nginx 官方文档明确认可的安全用法("if is evil" 那篇文章里专门列出了这个例外),不要因为看到「if is evil」就不敢用。

没价值的:让它 404 或 410

如果一条 URL 从来没有外链、也没有流量,那就让它老老实实 404。不用跳转,不用保留。搜索引擎会在一两个月内把它从索引里清掉,对站点没有任何负面影响。

这里我要纠正一个很流行的说法:「大量 404 会拉低站点质量分」。这是错的。Google 的 John Mueller 多次公开说过,站点有 404 是正常的,不影响排名。真正影响排名的是软 404和死链导致的抓取预算浪费,而不是 404 这个状态码本身。

内容还在但换了地址:301,别删

这是最常见的一种。文章还在,只是 URL 改了。这种情况必须 301,而且要在改 URL 的当天就配好,不要等搜索引擎把老 URL 从索引里踢掉之后才想起来。老 URL 的 301 至少要保留一年,因为外链和用户收藏不会更新得那么快。

一次真实的恢复过程

回到开头那位读者。我们的处理顺序是这样的:

第一步,把九千条返回 200 的「假首页」全部改成返回真正的 404。这一步做完,GSC 的 4xx 计数立刻涨到九千多——看着吓人,但这是正确状态。

第二步,按访问日志里的点击量排序,把有流量的前 300 条老 URL 做了 301,映射到对应的新 URL。规则用上面的 map 写法,正则一条搞定 /archives/(\d+)/ 到 /$1.html。

第三步,剩下的八千多条保持 404,不跳转。

第四步,重新提交 sitemap,把里面所有已删除的 URL 清掉,让 sitemap 只包含 200 状态的有效地址。

恢复的节奏是这样的:改完第二周,抓取异常从 9000 降到 2000 左右;第四周,索引量开始回升;第八周,首页展示量恢复到原来的七成。整个过程大概两个月,比预想的慢,但这已经算快的了——SEO 类问题的修复周期都是以周为单位的,如果有人告诉你三天见效,那是在卖药。

每周十分钟的例行检查

死链不会一次性清完,它是个持续的过程——文章会删、URL 会改、外链会失效。我给自己定了一个十分钟的例行清单:

  • 看当天日志里 404 的 top 10,有没有突然出现的新批次(突发的一批通常意味着某个模板或规则改错了)
  • 看 GSC 的 404 报告有没有新增,特别是有外链的那些
  • 抽查三条自己站的随机内链,确认能打开
  • 确认 404 页面本身返回的是 404 而不是 200

这四件事加起来不到十分钟,但能提前发现大部分问题。最开始那个读者的站之所以能烂半年,就是因为从来没有看过日志里的状态码分布——流量是慢慢流失的,等他发现的时候,损失已经积累了半年。

Last modification:October 25th, 2026 at 01:25 pm

Leave a Comment