自定义错误页:把 404 变成一个有用的页面

默认的 Nginx 错误页很简陋,而且会把用户直接赶走。花十分钟做个像样的 404,能把「流失」变成「继续浏览」。

一、Nginx 最小配置

nginx
server {
    listen 443 ssl http2;
    server_name www.bzii.cn;

    root /var/www/baize;
    index index.html;

    error_page 404 /404.html;
    error_page 500 502 503 504 /50x.html;

    location = /404.html {
        internal;        # 只允许内部跳转,不允许直接访问
        root /var/www/baize;
    }

    location = /50x.html {
        internal;
        root /var/www/baize;
    }
}
bash
sudo nginx -t && sudo systemctl reload nginx

internal 很重要:没有它的话,用户可以直接访问 /404.html,看到一个「正常页面返回 200 状态」的怪现象,对 SEO 不利。

二、易错点:error_page 与 proxy

反代场景下,后端返回的 404 默认会被 Nginx 直接透传,不会用你的错误页。要加一行:

nginx
location / {
    proxy_pass http://127.0.0.1:3000;
    proxy_intercept_errors on;     # 关键:拦截后端错误码
    error_page 404 /404.html;
    error_page 500 502 503 504 /50x.html;
}

如果只想拦截部分状态码:

nginx
proxy_intercept_errors on;
error_page 500 502 503 504 /50x.html;
# 404 交给后端自己处理(API 常这么做)

三、保持状态码正确

nginx
# 默认 error_page 会保持原状态码(404 还是 404)
error_page 404 /404.html;

# 如果写成这样,会返回 200(错误!对 SEO 有害)
error_page 404 =200 /404.html;

验证:

bash
curl -o /dev/null -s -w "%{http_code}\n" https://www.bzii.cn/no-such-page
# 应该是 404,不是 200

四、SPA 的回退问题

单页应用用 try_files 回退时,所有路径都会返回 200

nginx
location / {
    try_files $uri $uri/ /index.html;
}

这对 SEO 是个问题:不存在的 URL 也返回 200 和首页内容(软 404)。

两种解法:

1. 前端路由兜底:JS 里匹配不到路由时,渲染一个 404 视图。

js
default:
  app.innerHTML = "<div class=\"empty\"><h1>404</h1><p>页面不存在</p></div>";

2. 服务端区分静态资源与页面

nginx
location / {
    try_files $uri $uri/ @fallback;
}

location @fallback {
    # 静态资源不存在就真返回 404
    if ($uri ~* \.(js|css|png|jpg|webp|woff2)$) {
        return 404;
    }
    rewrite ^ /index.html last;
}

五、错误页该有什么

一个有用的 404 页面:

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <meta name="robots" content="noindex">   <!-- 别被收录 -->
  <title>页面不存在 · 白泽网络</title>
  <link rel="stylesheet" href="/style.css">
</head>
<body>
  <main>
    <h1>404</h1>
    <p>这个页面不存在,可能是链接失效或地址输错了。</p>
    <nav>
      <a href="/">回到首页</a>
      <a href="/archive/">浏览归档</a>
      <a href="/tags/">按标签找</a>
    </nav>
    <form action="/search" method="get">
      <input type="search" name="q" placeholder="搜索笔记…">
      <button type="submit">搜索</button>
    </form>
  </main>
</body>
</html>

关键要素:

  • 说清楚发生了什么(不要只写「Error」);
  • 给出下一步(回首页、搜索、看归档);
  • 保持站点风格一致(别让用户以为到了别的站);
  • noindex,别让搜索引擎收录。

六、50x 页面不同

服务端错误是临时故障,用户可能会刷新重试:

html
<h1>服务暂时不可用</h1>
<p>我们正在处理,请稍后重试。</p>
<button onclick="location.reload()">刷新试试</button>
<p class="muted">如果持续出现,请联系 user@example.com</p>

还可以加自动重试:

html
<script>
setTimeout(() => location.reload(), 10000);
</script>

自动刷新要谨慎:如果是死循环式的故障,会让用户陷入无限刷新。建议只重试一次。

七、其他状态码

nginx
error_page 403 /403.html;      # 禁止访问
error_page 429 /429.html;      # 请求过于频繁

location = /403.html { internal; }
location = /429.html { internal; }

八、验证

bash
# 404
curl -i https://www.bzii.cn/no-such-page | head -3

# 确认内容是自己的错误页
curl -s https://www.bzii.cn/no-such-page | grep "<title>"

# 确认 404 页面不能被直接访问(或访问时是 404)
curl -o /dev/null -s -w "%{http_code}\n" https://www.bzii.cn/404.html

# 50x:临时停掉后端测试
sudo systemctl stop myapp
curl -o /dev/null -s -w "%{http_code}\n" https://www.bzii.cn/api/

九、Cloudflare 用户的注意点

Cloudflare 有自己的错误页。想显示你自己的:

  • 源站确实返回对应状态码(不要一律返回 200);
  • 在 CF 的 Custom Pages 里配置,或确保它不拦截(免费版对 5xx 会显示自己的页面)。
bash
# 看错误页到底来自谁
curl -sI https://www.bzii.cn/no-such-page | grep -i "server\|cf-ray"

错误页是「体验的最后一道防线」。十分钟的配置,换来的是用户不直接关掉页面。