自定义错误页分级:从404到503怎么配

Nginx 的默认错误页是 nginx 自己的白底黑字页面,写着 404 Not Found、nginx 版本号,下面一行小字。这个页面在个人站长眼里往往不算问题——反正用户看到 404 就走了。但实际上,错误页是站点里少数几个「100% 会被看到」的页面:过期链接、被转载走的外链、用户手抖打错的路径、搜索引擎抓到的失效 URL,最后都落在这里。

这篇文章讲的是把错误页做成一套分级的体系:哪些状态码需要自定义、每个级别该给用户什么、给搜索引擎什么、技术上怎么落地,以及做完之后怎么验证它真的生效。不讲美化,讲的是行为和策略。

一、错误页的价值不在好看,在三件事

很多站长做错误页的思路是「设计一个可爱的 404 图」,这基本没用。错误页真正能带来价值的地方只有三个:

  • 把人留住。用户点进来的 URL 失效了,但他要找的东西大概率还在站内。错误页给两三个明确的出口(搜索框、栏目页、最新文章),比一句「页面不存在」有用得多。
  • 把错误说清楚。404(不存在,永久)、410(不存在,曾经存在)、403(无权限,别再试)、429(请求太多,稍后再试)、5xx(服务端故障,不是你的问题)—— 这些对用户和爬虫的含义完全不同,统一给一个「出错了」既误导人也误导搜索。尤其是 5xx,一定要明确告诉用户「这是我们的问题,稍后重试」,而不是让他以为链接永久失效。
  • 避免把权重浪费在错误页上。软 404(返回 200 的「页面不存在」页)是 SEO 上最典型的自伤:搜索引擎会把它当成有效页面去抓、去索引,站内大量这种页面会稀释抓取预算,也让人看到一堆重复的低质页面。

换句话说,错误页的设计目标可以压缩成一句话:给人一个出口,给机器一个准确的状态码,给自己留一条能统计的日志。

二、先决定哪些状态码要自定义

不是所有状态码都值得自定义。我按「用户会不会停下来看」分成三档:

级别状态码处理方式
一级(必须自定义)404、410完整版错误页:说明 + 搜索框 + 推荐入口 + 上报埋点
二级(建议自定义)403、429、500、502、503、504轻量版错误页:一句人话 + 重试建议 + 回首页/回上一页
三级(可以不碰)400、405、413、414、416保留默认或极简,这些多半是扫描器/爬虫触发的,用户几乎不会遇到

这里有个容易踩的点:410 不要当成 404 用。站点改版、文章合并、栏目下线时,被主动删除且确定不再提供的内容,用 410 比 404 更准确——它明确告诉搜索引擎「别再来抓了」,通常失效更快。反过来,如果只是临时下线或路径可能恢复,就老老实实 404。

还有一类特殊状态码要单独考虑:503。维护模式下,503 是唯一正确的状态码(不是 502、不是 200 的「维护中」页)。同时它必须带 Retry-After 响应头,否则搜索引擎会按默认节奏继续试探,有的爬虫还会把连续 503 当成站点永久不可用而降权。

三、Nginx 侧的落地配置

先看最基础的配置。错误页文件放在站点根目录下的 /404.html、/error/50x.html 这类位置,不要让它们被当成普通文章页抓走(后面讲 noindex)。

server {
    listen 443 ssl;
    server_name example.com;

    # 一级:404 / 410 各自独立页面
    error_page 404 /error/404.html;
    error_page 410 /error/410.html;

    # 二级:客户端错误与 5xx 合并成一个轻量页
    error_page 403 /error/403.html;
    error_page 429 /error/429.html;
    error_page 500 502 503 504 /error/50x.html;

    # 关键:这些页面必须能真实访问到,否则 nginx 会退化为内置页面
    location ^~ /error/ {
        internal;
        root /data/www/example.com;
    }
}

这里的 internal 指令非常关键。不加的话,任何人直接访问 /error/404.html 都能拿到一个 200 的页面,搜索引擎也可能把它当成正常内容收录,变成一份「合法的重复页」。加了 internal 之后,这个 location 只能由 nginx 内部重定向进入,外部直接访问会返回 404 —— 这正是我们要的。

注意 internal 和 root 的组合:用了 internal 的 location 仍然需要能定位到文件,所以 root 要指对目录。如果你把错误页放在站点根目录而不是子目录,就要写成 location = /404.html { internal; } 这种精确匹配,避免把整站路径都变成 internal。

四、状态码不能丢:最常见的三个事故

自定义错误页最容易出的事故,是「页面看着对了,状态码错了」。这三种情况我都见过:

  • 用 PHP 输出错误页而忘了设状态码。比如 include '404.php'; exit;,PHP 默认返回 200,等于全站软 404。正确写法是 http_response_code(404); 之后再输出,或者 PHP 5.4 以下用 header('HTTP/1.1 404 Not Found');。
  • error_page 404 =200 /404.html; 这种写法会把状态码改成 200。有些人为了「避免搜索引擎看到 404」这么干,完全是反的。等号后面带状态码是给「希望改写状态」的场景用的,比如 error_page 404 =301 /old-path/,那是有意做跳转。
  • 前端路由站点全站 200。单页应用如果没有服务端兜底,任何不存在的路径都会返回 index.html 和 200。至少要判断路径是否命中已知路由,未命中的返回真 404。

验证状态码只有一个办法:curl -I 看首行,别用浏览器看页面内容。浏览器不显示状态码,肉眼看页面长得像 404 完全说明不了任何问题。

五、错误页本身要 noindex

即使状态码是对的,错误页仍可能被索引 —— 尤其是 403、429 这类页面,搜索引擎偶尔会把它们当成「暂时抓不到但页面存在」而保留 URL。稳妥做法是在错误页的 <head> 里加上:

<meta name="robots" content="noindex, follow">

用 follow 而不是 nofollow,是因为错误页里的推荐链接对用户有价值,也没必要阻止爬虫顺着走出去。

同时注意 X-Robots-Tag 响应头这个更硬的手段,它比 meta 标签优先,而且对非 HTML 资源也生效:

location ^~ /error/ {
    internal;
    add_header X-Robots-Tag "noindex, follow" always;
}

always 参数别漏。不带 always 时,add_header 只在 2xx/3xx 响应上生效,而错误页恰恰是 4xx/5xx,等于白加。这个细节坑过很多人。

六、页面上该放什么,不该放什么

按我自己的经验,一个能留住人的 404 页有这样的结构:

  1. 一句人话说明。「这个页面找不到了,可能是链接过期或者地址打错了」—— 不要用「404 Not Found」开头,那是给机器看的。
  2. 搜索框。这是转化率最高的元素。用户既然点进来,说明他有明确目标,搜索框让他自己找。
  3. 三到五个出口。首页、栏目页、最新文章、热门文章。不要放十个,选择太多等于没有选择。
  4. 一个反馈口。「你是在哪里点到这个链接的?」—— 这条信息能帮你找到失效外链或者写错的站内链接,价值比想象中大。

明确不要放的东西:自动跳转。很多老站点做 meta refresh 三秒跳首页,体验极差 —— 用户还没看清发生了什么就被带走,而且对 SEO 来说,带 refresh 的 404 页有时会被当成软跳转处理。如果要做引导,用可见的按钮,让用户自己点。

另一个细节:错误页不要依赖外部资源。CSS、字体、统计脚本全部内联或者走本站。理由很实在——错误页经常出现在服务端有问题的时候(5xx),这时候外链资源大概率也加载不出来,页面会退化成一堆无样式的文字,比不做还难看。

七、把 404 变成数据:日志与统计

错误页真正的长期价值在这:它是发现站内问题的最便宜的手段。做法是在错误页里上报当前 URL 和 referer,落到自己的日志表里,然后每周看一次 Top 20。

# nginx 侧把 404 单独记一个文件,便于统计
map $status $loggable {
    ~^[23]  0;
    default 1;
}
access_log /var/log/nginx/error-only.log main if=$loggable;

然后一条命令出报表:

awk '{print $7}' /var/log/nginx/error-only.log \
  | sort | uniq -c | sort -rn | head -20

看这份报表能发现三类问题:一是站内链接写错(同一个错误路径被大量站内页面引用);二是被外站引用的失效 URL(referer 是外部域名);三是扫描器探测(路径明显是 /wp-admin、/.env 这类)。前两类要修,第三类直接扔进 WAF 就好,不必纠结。

这里有个实操上的小技巧:把 404 报表和内容清单做交叉。如果某个 404 路径的前缀和已删除文章高度相似(比如 /2023/old-title.html),那基本就是改版时漏做了 301,补一条跳转规则比放一个 404 页划算得多。错误页是兜底,不是解决方案 —— 能 301 的失效 URL,永远优先 301。

八、维护模式与 503 的正确做法

站点要升级、迁库、切 CDN 时,最稳的做法是挂维护页而不是直接停服务。配置大致这样:

# 在 server 块最前面加一个开关文件判断
if (-f /data/www/example.com/maintenance.on) {
    return 503;
}
error_page 503 /error/maintenance.html;
location = /error/maintenance.html {
    internal;
    add_header Retry-After 3600 always;
    add_header X-Robots-Tag "noindex" always;
}

要点有三个:状态码 503 而不是 200;Retry-After 给出预计恢复时间(秒数或 HTTP 日期都行);if 只做文件存在判断是安全的(nginx 官方唯一认可 if 的用法就是 return 和 rewrite ... last)。

如果想放行一部分人(自己、监控、搜索引擎),就在 return 503 之前加白名单:

set $maint 1;
if ($remote_addr = 203.0.113.7)        { set $maint 0; }
if ($http_user_agent ~* "Googlebot")   { set $maint 0; }
if ($maint = 1) { return 503; }

注意放行搜索引擎这件事要谨慎:搜索爬虫抓到的如果是「维护中但有内容」的中间态,可能造成索引波动。我自己的做法是只放行自己 IP 和监控,维护窗口尽量控制在三十分钟以内,宁可短时间全站 503,也不要让爬虫看到半成品。

九、上线之后的验证清单

配置改完,一定要跑完整的验证,不能只看浏览器。下面这组命令一次执行完,五项都过才算完:

BASE=https://example.com
for p in /this-page-does-not-exist /error/404.html /error/50x.html; do
  echo "== $p"
  curl -s -o /dev/null -w "code=%{http_code} type=%{content_type}\n" "$BASE$p"
done
# 期望:第一个 404,后两个 404(被 internal 挡住,这正是正确结果)
curl -sI "$BASE/this-page-does-not-exist" | grep -i 'x-robots-tag'
# 期望:noindex
curl -s "$BASE/this-page-does-not-exist" | grep -c 'meta name="robots"'
# 期望:1

再加两条容易被忘的:一是确认错误页没把状态码改成 200(-I 看首行);二是确认错误页里的 CSS 是从本站加载的(看页面里的 href 域名)。

还有一条:改完配置先 nginx -t,再 nginx -s reload,顺序不能反。reload 是平滑的,不会断开现有连接,但前提是新配置能通过语法检查。用 error_page 指向不存在的文件不会让 nginx -t 报错——它只检查语法不检查文件存在,所以文件路径必须自己 curl 验一遍。

十、小结

错误页看起来是小事,但它同时牵涉到用户体验、SEO 正确性和运维可观测性。分级设计是这套体系的核心:404/410 用完整页,把人接回内容里;403/429/5xx 用轻量页,明确告知性质;400 之类的机器错误保留默认。落地时守住四条底线——状态码不能被改成 200、错误页要 internal 且 noindex、503 要带 Retry-After、错误日志要能出报表。

最后一句经验:能 301 的就别做 404。错误页是最后的兜底,衡量它做得好不好的指标不是设计多漂亮,而是「有多少 404 被我们修成了 301」和「有多少 404 从报表里消失了」。

Last modification:October 30th, 2026 at 08:24 pm

Leave a Comment