## Bug report ### Description 使用 **目录式内容**(`foo/README.md` → 路由 `/foo/`,产物 `foo/index.html`)时,客户端访问**无后缀 clean URL**(如 `/foo`)会 404,部署在会自动规范化 URL 的静态托管(如 Vercel)时还会形成**无限重定向循环**。 客户端 `resolveRoutePath` 流程(`packages/client/src/router/resolveRoutePath.ts`): ``` /foo → normalizeRoutePath → /foo.html(inferRoutePath 有意把无后缀补 .html) → routes 里没有 /foo.html(实际路由是目录式 /foo/) → redirects 里也没有 /foo.html → 返回 /foo.html → resolveRoute 判定 404 ❌ ``` 路由表里只有目录路由 `/foo/`,但客户端把 clean URL 转成了**不存在的 `.html`**,且没有回退到目录路由。 ### 复现 - 配置 `foo/README.md`(目录页) - 浏览器访问 `/foo`(无后缀,即 clean URL) - 观察:客户端导航到不存在的 `/foo.html` → 404 - 部署在 Vercel 等静态托管时:托管层把 `/foo.html` 301 回目录 → 浏览器再把目录去斜杠 → **无限重定向循环**(`ERR_TOO_MANY_REDIRECTS`) > 访问 `/foo/`(带斜杠)正常。问题仅在 clean URL 形态(无后缀)下触发。 ### Expected behavior 访问 `/foo`(无后缀)应正确解析到目录路由 `/foo/`,而不是转成不存在的 `/foo.html`。 ### 环境 - vuepress 2.0.0-rc.31(`vuepress@next`) - 部署:Vercel / 任意会做 URL 规范化的静态托管 ### 相关 - 已在 https://github.com/vuejs/vuepress/issues/3239 关闭(该仓库是 v1 历史仓库) - 修复 PR:#(待创建)
Activity
MEMZ-CHROER commentedon Aug 17, 2026
@Mister-Hope 你好!感谢你的 #1538 提供完整的 clean URL 方案。想知会一下:我在 #1720 提交了一个针对性的快速修复(
resolveRoutePath在.html路由不存在时回退目录路由),用于解决 #1538 落地前用户实际踩到的 404 / 无限重定向 bug(部署在 Vercel 等会规范化 URL 的托管时)。它与 #1538 不冲突:仅在规范化的
.html路由不存在时回退目录;#1538 合入后inferRoutePath返回 clean 格式,此回退自然失效。供你判断取舍——如果 #1538 很快合入可以关闭 #1720,我完全理解;如果 #1538 尚需时日,这个小修复可以先保线上。无论如何都感谢你的 clean URL 方案。🙏
Mister-Hope commentedon Aug 17, 2026
简短版本;我根本就不同意你的expected behavior。而且我那个搁置的pr合并了也不会解决你这个问题。
长:
/foo 和 /foo/ 本来就应该是不同的东西(/foo.md 和 /foo/README.md)。我不认为文档类框架应当默认进行这类危险的回退。如果你有需求,你可以自己自定义一下resolver,也就是你所谓的pr是可以在你项目里直接自定义的。
cc @meteorlxy
MEMZ-CHROER commentedon Aug 17, 2026
理解了,感谢你指出正确方向!/foo 与 /foo/ 确实是不同页面(foo.md vs foo/README.md),默认回退确实危险。我试试在项目里自定义 resolver 的方式,不打扰 core。
MEMZ-CHROER commentedon Aug 17, 2026
@Mister-Hope 按你说的方向做了一轮实验,汇报结果(实验在本地 dev 环境完成,未改动 core):
方案:在项目侧
clientConfig.enhance里用router.beforeEach+resolveRoute实现"同名文件优先,目录兜底":实验数据(
resolveRoute实测):/tanks→ notFound(无同名文件)→/tanks/存在 → 重定向到/tanks/✅/tanks/design→/tanks/design.html存在 → 放行,不重定向 ✅结论:完全认同你的模型(
/foo与/foo/是不同页面)。这个 fallback 只在同名文件不存在时才触发,不会混淆foo.md与foo/README.md。这样既满足目录式站点的 UX 需求,也不违背 VuePress 的 URL 语义。如果 core 未来能给路由解析暴露一个正式的自定义入口(比如 resolvers 里加
resolveRoutePath),会比 beforeEach hack 更干净——但当前版本下项目侧 hack 完全够用。再次感谢你的指导。PR #1720 我这边先保持 open,等你判断。
MEMZ-CHROER commentedon Aug 17, 2026
补充:实验用的可运行实现(项目侧
docs/.vuepress/client.js):关键点:
resolveRoute(client 导出,不依赖 Vue inject,可在 enhance 里直接用)useRoutes()——它依赖inject,在 enhance 阶段会报inject() can only be used inside setup()resolveRoute('/tanks')→notFound: true,resolveRoute('/tanks/')→notFound: false,据此判断实测(本地 dev):
/tanks→/tanks/✅/tanks/design→/tanks/design.html(同名文件优先,不重定向)✅MEMZ-CHROER commentedon Aug 17, 2026
@meteorlxy 你好,想请你帮忙看一个 issue。
问题:目录式站点(
foo/README.md→ 路由/foo/)部署在会自动规范化 URL 的托管(如 Vercel)时,访问无后缀/foo会触发 404 / 无限重定向循环。客户端resolveRoutePath把/foo规范化为不存在的/foo.html(inferRoutePath的有意行为),而 routes 表里只有/foo/。已做:
clientConfig.enhance+router.beforeEach+resolveRoute实现"同名文件优先,目录兜底"(/foo.html存在走文件,仅目录存在时回退/foo/),两个场景实测通过resolveRoutePath(仅当原始路径无扩展名时触发),并带测试完全理解
/foo与/foo/语义不同(foo.mdvsfoo/README.md),所以回退仅在同名文件不存在时触发,不会混淆。想听听你对"安全回退是否值得内嵌 core"的看法。如果 core 不打算做,项目侧方案也完全够用。
github-actions commentedon Sep 2, 2026
This issue is marked as
stalebecause it has not had recent activity. Issues marked withstalewill be closed if they have no activity within 7 days.MEMZ-CHROER commentedon Sep 12, 2026
感谢@Mister-Hope删除标签
github-actions commentedon Sep 28, 2026
This issue is marked as
stalebecause it has not had recent activity. Issues marked withstalewill be closed if they have no activity within 7 days.