avatar👨‍💻
BestGuo2020BestGuo 的小窝

face-lift

一个人的世界是如此安静

博客升级后样式错乱:最后发现问题不在 Valaxy,而在 Nginx

今天给自己的博客做了一次升级,本以为只是一次常规的依赖更新,没想到最后却在一个看似简单的“样式错乱”问题上折腾了很久。

一开始,我把 Valaxy 框架升级到了最新版本,希望和作者使用的版本保持一致。构建过程没有报错,本地预览也完全正常,但部署到云服务器后,文章页面的布局却明显不对。

更奇怪的是,本地和线上使用的明明是同一套代码。后来我甚至直接把本地构建好的产物上传到服务器,问题依旧存在。

我先怀疑 Valaxy 的新版本存在兼容问题,又参考了云游君使用同款主题的博客。对方的网站显示正常,于是我把 Valaxy 一路降级到了 0.28.11,结果线上页面还是老样子。

最后没想到,这个困扰了我很久的问题,竟然被 AI 帮我定位并解决了。

问题现象

本地使用下面的命令预览构建产物:

bash
pnpm serve

文章页面显示正常:

本地预览页面显示正常

但同一篇文章部署到云服务器后,整个页面向右偏移,右侧导航栏已经被压瘪了:

修复前的线上页面

因为本地正常、线上异常,最容易产生的第一反应就是:

  • 本地和 GitHub Actions 使用的 Node.js 或 pnpm 版本不同;
  • Valaxy 新版本存在构建差异;
  • 服务器没有上传完整的 CSS;
  • 浏览器或 CDN 缓存了旧资源;
  • Nginx 返回了错误的 MIME 类型;
  • 线上加载了旧版本的构建产物。

这些方向都很合理,但这次真正的问题并不在 CSS。

最初怀疑是 Valaxy 版本问题

这次升级首先把 Valaxy 更新到了最新版本。发现线上异常后,我自然把版本升级列为首要嫌疑。

我对比了云游君使用同款主题的博客,发现对方的网站并没有出现相同问题。为了排除最新版带来的影响,我又把 Valaxy 降级到了 0.28.11

然而重新安装依赖、重新构建并发布之后,问题依旧存在。

这一步其实已经说明:问题大概率不由某一个 Valaxy 版本直接导致。但当时页面表现得实在太像样式问题,我仍然在构建产物和依赖版本上反复排查。

确认本地与线上静态资源一致

排查的转折点,是不再只凭“看起来像 CSS 错了”判断问题,而是直接比较浏览器实际加载的资源和页面结构。

关于本地和生产环境各种明细比较可以交给 Codex、Claude Code 这类 Agent 来做,不是单纯的 AI 聊天对话工具。

首先检查文章页面引用的主要资源:

text
assets/style.Bw4NHIdy.css
assets/theme.D1YS81AU.js
assets/index.CoA0Qxzn.js

然后分别计算本地文件与服务器响应内容的 SHA-256。结果显示,CSS、主题脚本和入口脚本的文件长度与哈希全部一致。

这意味着:

线上加载的 CSS 和 JavaScript 与本地构建产物完全相同,样式文件本身没有损坏,也没有少上传。

其实移动端适配页面也有问题,后面接下来就比较移动端中的计算样式,文章正文的字体、字号、行高、宽度和内边距也都相同。真正不同的是文章主体的位置:

text
本地 .yun-main 的顶部位置:约 48px
线上 .yun-main 的顶部位置:约 524px

线上页面的正文被额外向下推了约 476px

进一步检查 DOM 后发现,线上页面顶部残留了一块移动端作者信息卡片,而本地页面没有这块结构。至此可以确定:这不是 CSS 把元素画错了,而是服务器最初返回的 HTML 就不对。

真正原因:Nginx 返回了错误的 HTML

博客文章的访问地址是:

text
/posts/sql-spatial-query-optimization-tutorial

这是一个不带 .html 后缀的 clean URL。

但 Valaxy 静态生成的真实文件是:

text
/posts/sql-spatial-query-optimization-tutorial.html

服务器原来的 Nginx 配置为:

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

这段配置的查找过程是:

  1. 查找 $uri 对应的文件;
  2. 如果找不到,就返回根目录的 /index.html

当浏览器请求下面的地址时:

text
/posts/sql-spatial-query-optimization-tutorial

Nginx 只会查找:

text
/posts/sql-spatial-query-optimization-tutorial

它不会自动继续尝试:

text
/posts/sql-spatial-query-optimization-tutorial.html

由于无扩展名的真实文件不存在,请求最终回退到了博客首页的 /index.html。随后,浏览器中的 Vue Router 又根据当前 URL 加载并渲染了文章内容。结果就变成了:

text
首页的初始 SSR 结构
        +
客户端路由渲染的文章结构
        =
顶部残留首页组件,文章布局错位

所以页面看起来像“样式错乱”,实际却是 Nginx 给文章 URL 返回了错误的 HTML 文件。

为什么 pnpm serve 没有问题

本地的 Valaxy/Vite 预览服务器支持 clean URL。访问:

text
/posts/sql-spatial-query-optimization-tutorial

预览服务器会自动找到:

text
/posts/sql-spatial-query-optimization-tutorial.html

但 Nginx 的 try_files 不会自动补全 .html,必须在配置中明确写出来。

这也解释了为什么同一份构建产物在本地完全正常,放到服务器上却出现问题:差异不在构建文件,而在服务器如何把 URL 映射到文件。

解决办法

把 Nginx 配置修改为:

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

新的查找顺序为:

  1. 查找 $uri 对应的真实文件;
  2. 查找 $uri.html
  3. 查找 $uri/ 对应的目录;
  4. 全部找不到时,最后才回退到 /index.html

这段配置分别是什么意思

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

location / 用于匹配没有被其他更具体规则捕获的普通网站请求。try_files 则按照从左到右的顺序检查文件或目录,找到第一个存在的目标后便停止查找。

  • $uri:当前请求路径,不包含域名和查询参数。它优先匹配真实存在的 CSS、JavaScript、图片、字体或带后缀页面。
  • $uri.html:在请求路径后补上 .html 再查找。例如请求 /posts/article 时,会尝试读取 /posts/article.html。这是 Valaxy 无扩展名文章地址能够命中静态 HTML 的关键。
  • $uri/:检查对应目录是否存在,并可配合 index 指令读取目录下的 index.html,例如 /about/index.html
  • /index.html:前面都没有命中时的最终回退入口,通常用于 Vue Router 的 history 模式,由客户端路由继续处理请求。

推荐的顺序可以概括为:

text
真实文件 → 补全 .html → 真实目录 → SPA 入口

顺序很重要。真实静态页面应当优先于 SPA 回退,否则 Nginx 会过早返回首页;如果省略 $uri.html,Valaxy 生成的无扩展名文章 URL 就无法命中对应的 .html 文件。 修改配置后检查语法并重新加载 Nginx:

bash
nginx -t
nginx -s reload

修复后,服务器能够为无扩展名文章 URL 返回正确的文章 HTML,页面布局也恢复正常:

修复后的线上页面

如何验证是否命中了正确文件

可以分别请求无扩展名地址和显式 .html 地址:

bash
curl -I https://www.bestguo.top/posts/valaxy-nginx-clean-url-style-problem
curl -I https://www.bestguo.top/posts/valaxy-nginx-clean-url-style-problem.html

修复前,无扩展名地址返回的是根目录 index.html,本次排查中响应大小只有约 46KB

显式访问 .html 时,返回的才是真正的文章文件,大小约为 124KB,并且与本地 dist 文件的 SHA-256 完全一致。

加入 $uri.html 后,无扩展名 URL 也会命中这个正确的文章文件。

如果博客完全采用静态生成,不需要让未知路径回退到 SPA,还可以使用更严格的配置:

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

这样不存在的页面会直接返回 404,不会因为统一回退到首页而掩盖静态文件映射问题。

排查小结

这次问题最迷惑的地方,是它在视觉上非常像 CSS 或框架版本故障,但根因却发生在更前面一层:服务器返回了错误的 HTML。

以后再遇到“本地正常、线上异常”,可以按照下面的顺序排查:

  1. 确认请求的 HTML 是否真的是目标页面;
  2. 对比本地和线上 CSS、JavaScript 的文件长度与哈希;
  3. 检查静态资源的状态码、Content-Type 和缓存响应头;
  4. 对比关键元素的计算样式和实际位置;
  5. 检查 Nginx 的 rootaliastry_files 与 clean URL 映射;
  6. 最后再考虑框架版本、依赖和构建环境差异。

还有一个很重要的经验:

“同一份构建产物”只能证明文件内容一致,不能证明服务器一定把正确的文件返回给了当前 URL。

这次我先升级 Valaxy,又降级到 0.28.11,反复构建、上传和刷新,始终没有解决问题。最后通过 AI 对比静态资源哈希、DOM 结构和服务器响应,才发现真正的问题只是 Nginx 少尝试了一次 $uri.html

有时候最难排查的问题,并不是配置有多复杂,而是表面现象把人带到了完全错误的方向。

我终于做出了自己家乡的麻将游戏
从 2.95 秒到 0.44 秒:MySQL 8 空间查询优化实战教程