问题现象
最近发现重构后的博客在 Vercel 部署后出现了一个奇怪的问题:当直接通过 URL(无 .html
后缀)访问(例如 .../posts/my-post
),页面会先显示出一个空的布局,内容区域一片空白,然后在刷新后,地址栏带上了 .html
后缀,文章内容才被加载出来。
由于我的博客在 Google 的索引也是这样没有 .html
后缀的形式,因此也会出现相同的现象,导致极其糟糕的用户体验。
排查与定位
为了找到根源,我首先排查了 VitePress 主题中的客户端代码,检查是否存在 location.href
或 router.push
之类的手动跳转逻辑,但一无所获。
既然客户端代码没有问题,那么问题很可能出在部署平台的配置上。我的博客托管在 Vercel,因此 vercel.json
文件成为了重点排查对象。果然,我在其中发现了问题的根源:
// vercel.json (旧的错误配置)
{
"rewrites": [
{
"source": "/(.*)",
"destination": "/index.html"
}
]
}
原理分析:错误的 SPA 模式
上述 rewrites
规则的含义是:“无论用户请求什么路径,都返回根目录下的 index.html
文件”。
这是一个典型的 单页应用 (SPA) 配置。但 VitePress 本质上是一个 静态站点生成器 (SSG),它为每一篇文章都预先生成了独立的 HTML 文件(例如 /posts/my-post.html
)。
这个错误的配置导致了如下低效的加载流程:
- 请求:浏览器请求
/posts/my-post
。 - 错误响应:Vercel 服务器根据规则,返回了通用的
index.html
(一个只包含基本布局和 JS 脚本的“空壳”)。 - 客户端接管:浏览器加载完这个“空壳”后,执行 VitePress 的客户端 JS。
- 二次加载:JS 脚本启动后,发现浏览器地址栏的 URL 与当前页面内容不匹配,于是在客户端重新发起请求或通过路由切换来获取真正的文章内容。然而不知为何,并没有真正显示出来。刷新后才由于重定向至
.html
后缀的链接,正常显示了。
解决方案:正确的服务器端重写
解决方案是修改 vercel.json
,让其在服务器端就能智能地处理这种无后缀的 URL 请求,直接返回正确的 HTML 文件。
我将 rewrites
规则修改为:
// vercel.json (新的正确配置)
{
"rewrites": [
{
"source": "/:path((?!.*\\.).*)",
"destination": "/:path.html"
}
]
}
这个新规则的原理是:
source: "/:path((?!.*\\.).*)"
: 匹配所有不包含.
(点) 的 URL 路径。这样它就能匹配到/posts/my-post
这样的文章路径,同时忽略/assets/image.png
这样的静态资源文件。destination": "/:path.html"
: 将匹配到的路径在服务器内部重写,为其添加.html
后缀。
这样,新的高效加载流程变为:
- 请求:浏览器请求
/posts/my-post
。 - 智能重写:Vercel 服务器在内部将路径“翻译”为
/posts/my-post.html
。 - 正确响应:服务器直接返回包含完整文章内容的
posts/my-post.html
文件。 - 瞬时加载:浏览器一次性接收到完整页面,立即渲染,无任何延迟和闪烁。
总结
这个问题提醒我们,在使用现代前端框架时,必须确保部署环境的配置与框架自身的工作模式相匹配。将为 SSG 设计的 VitePress 错误地配置为 SPA 模式,虽然刷新后最终也能显示内容,但是会导致 Google 进入的读者看到的是几乎空白的页面,一头雾水。通过重写vercel的重定向规则,我们就能轻松修复这个问题,提供更流畅的用户体验。