这个毛病我今天才在自己的服务器上修掉。它不报错、不影响访问,甚至看起来「很贴心」—— 但它同时在坑你的 SEO、坑你的排错效率、坑你的日志可信度。
它叫 软 404(soft 404)。
先看现场。我随手请求几个明显不存在的地址:
$ curl -o /dev/null -w "%{http_code}\n" https://example.com/xxx-not-exist
200
$ curl -o /dev/null -w "%{http_code}\n" https://example.com/.env
200
$ curl -o /dev/null -w "%{http_code}\n" https://example.com/.git/config
200
三个都返回 200。再对比一下内容 —— 它们返回的都是首页的 HTML。
原因就一行配置:
location / {
root /var/www/html;
index index.html;
try_files $uri $uri/ /index.html; # ← 罪魁祸首
}
try_files 的最后一项是「兜底」:文件找不到时,内部重写到 /index.html。因为这是内部重写而不是重定向,HTTP 状态码保持 200,客户端和搜索引擎看到的就是「这个页面存在,内容就是首页」。
因为它是单页应用(SPA)的标准做法。Vue/React 项目用客户端路由,/user/123 这种路径磁盘上根本没有对应文件,必须靠兜底把首页发出去,JS 才能接管路由。
但我的站是纯静态多页站(每个页面一个真实 HTML 文件),根本没有客户端路由 —— 却抄了 SPA 的兜底配置。这就是典型的「配置从别处抄来,没想过自己是不是那个场景」。
history.pushState / 路由库?| 代价 | 具体后果 |
|---|---|
| ① SEO 稀释 | 搜索引擎抓到一个不存在的 URL,拿到 200 + 首页内容 → 判为「重复内容」。站内每一条坏链、每个外部错误引用,都会变成一份首页副本被尝试收录,权重被摊薄。 |
| ② 坏链永远查不出来 | 链接检查工具靠状态码判断死链。全站 200 的站,链接检查器会告诉你「一切正常」—— 哪怕一半链接是坏的。你失去了最基础的健康度信号。 |
| ③ 日志失去可信度 | 访问日志里扫描器探测 /.env、/.git/config、/wp-admin 全是 200。你没法用 grep " 404 " 快速筛出扫描行为,也没法统计真实错误率。 |
/.env 拿不到任何东西。但这属于「歪打正着」,不能当成安全策略。真正的安全边界应该是「文件不存在就明确 404」,而不是「什么都说自己存在」。
如果你也是纯静态多页站,把兜底改成真 404,并指定 404 页面:
error_page 404 /404.html;
location / {
root /var/www/html;
index index.html;
try_files $uri $uri/ =404; # ← 找不到就真 404
}
改完先测语法再重载,别直接 restart:
nginx -t && systemctl reload nginx
# 1. 不存在的路径 → 应该 404
curl -o /dev/null -w "%{http_code}\n" https://你的域名/not-exist-xyz
# 2. 正常页面 → 应该仍然 200(漏测这一步最容易出事)
for p in / /about.html /articles/ /sitemap.xml /robots.txt; do
echo "$(curl -o /dev/null -w '%{http_code}' https://你的域名$p) $p"
done
/index.html 兜底改成 =404 之后,如果目录索引($uri/)的规则没写对,/articles/ 这类目录路径会直接挂掉。改配置的风险从来不在「新功能不生效」,而在「旧功能被顺手关掉」。
修复后我的站:
/.env / /.git/config → 全部 404 ✅# 如果这个命令输出 200,你的站就有软 404
curl -s -o /dev/null -w "%{http_code}\n" https://你的域名/definitely-not-a-real-page-$(date +%s)
另外,顺手把这条也跑一下 —— 如果你的站连 /.env 都返回 200,那更要立刻改:
for p in /.env /.git/config /wp-admin/ /admin.php; do
echo "$(curl -s -o /dev/null -w '%{http_code}' https://你的域名$p) $p"
done
正常的答案应该是清一色 404(或 403)。如果混着 200 和 301,回去看看你的 try_files 最后一项是什么。👻