2026 年 8 月 3 日
静态站点中的 Trailing slash 问题
发现问题
故事始于一位 Rspress 用户遇到的真实问题。
这位用户将站点部署在 /next/ 路径下,并在首页中使用了相对链接:
访问站点的域名根地址时,无论 URL 是否以 / 结尾,链接都能正常跳转。访问 /next/ 时,链接也会正确跳转到:
但访问 /next 时,链接却跳转到了:
页面明明能够正常打开,为什么相对链接少了一层 /next?
这个问题让我开始思考:带不带 trailing slash 的区别到底是什么?
根地址是一个特例
RFC 3986 规定,对于 HTTP(S) URL,空 path 与 / 等价。因此,即使输入的是 https://example.com,浏览器发出的请求仍然是 GET /。
Trailing slash 会改变相对链接的解析结果
浏览器会以当前页面的 URL 为基准解析相对链接。对它来说,/next 的最后一段是当前资源名,而 /next/ 表示当前资源位于一个目录下:
在客户端路由这一层,URL 匹配通常比 Web server 的文件查找更宽松。以 React Router 为例,它默认不区分 path 末尾是否带 /,因此下面两个 URL 都能匹配 path 为 /guide 的同一个路由:
/guide/guide/
不过,这种宽松只适用于路由匹配,并不意味着客户端会自动规范化 URL。React Router 不会主动移除 .html,也不会将 /index.html 视为目录首页。/guide.html 和 /guide/index.html 能否访问,仍取决于框架或 Web server 是否提供了额外处理。
这也解释了为什么“页面能打开”和“页面中的相对链接正确”是两个问题。宽松匹配只能保证客户端找到路由,不会修改地址栏中的 URL;只要地址栏仍是 /next,浏览器就会按照第一种方式解析相对路径。
这种差异延续自早期 Web 将 URL path 映射到文件系统的设计。下面两个地址可能对应两份不同的静态产物:
当 URL 命中目录时,Web server 通常会先补上 trailing slash,再查找 index.html。现代托管平台可以通过 rewrite 让多个 URL 返回同一个页面,但这不会改变浏览器解析相对链接的规则。
解决问题
解决这个问题的关键,是让地址栏中的当前 URL 与站点生成的首选 URL 保持一致。
Rspress 的 route.cleanUrls 用于控制生成的 URL 是否带有 .html 后缀。但它只能控制 Rspress 生成的链接,无法阻止用户从书签、搜索结果或外部网站访问 /guide.html、/guide/ 等其他写法。
因此,Rspress 在 2.1.0 中新增了 route.cleanUrlsRedirect。客户端启动时,它会先匹配真实路由,再按照 cleanUrls 选择的格式,通过 history.replaceState 修正地址栏。例如,当 cleanUrls: true 时:
/guide.html、/guide/、/guide/index.html→/guide/reference/index.html→/reference/
规范化发生在页面渲染前,不会刷新页面,并会保留站点 base、query、hash 和已有的 history state。对于开头的例子,Rspress 会先将 /next 修正为 /next/,再渲染页面,相对链接也就不会跳出 /next/。
从 Rspress 2.1.0 开始,cleanUrls 和 cleanUrlsRedirect 都会默认开启。虽然客户端规范化不能完全替代服务端的 301/308,也不能单独解决重复 URL 的 SEO 问题,但仍能避免 URL 格式不一致引发的代码逻辑错误。
完整的 URL 转换规则和配置说明,请参阅 route.cleanUrlsRedirect API 文档。
最佳实践
今天,URL 不再需要对应磁盘文件;选择带不带 trailing slash 是站点策略,而不是对错。但浏览器和搜索引擎仍将 /about 与 /about/ 视为两个 URL。
为了避免代码逻辑错误,也避免同一页面被多个 URL 收录,需要做到:
- 服务端:使用 301/308 将
/about和/about/合并到同一个首选 URL;/about.html和/about/index.html等变体也可以一并处理。例如,Cloudflare Workers Static Assets 默认支持auto-trailing-slash:普通 HTML 文件使用/file,目录首页使用/folder/,其他写法会以 307 跳转到对应的规范 URL。 - 客户端:开启
cleanUrls,并使用客户端重定向兜底。托管平台无法配置重定向时,它可以在页面最早入口写回首选 URL;但必须等 HTML 和 JavaScript 加载后才能执行,不能完全替代服务端重定向。
所有 URL 保持一致:站内链接、canonical、sitemap 和分享地址都输出首选 URL,其他写法通过 301/308 跳转过去。
最后检查:
/file与/file/不应同时以两个独立 URL 返回同一内容;- 公开 URL、静态产物、Rspress 配置与 CDN 规则使用同一种 trailing slash 策略。