Skip to content

[Bug report] clean URLs (无后缀访问目录页) 在客户端被转成不存在的 .html,导致 404 / 无限重定向循环 #1719

Description

@MEMZ-CHROER

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 规范化的静态托管

相关

Activity

  1. MEMZ-CHROER commented on Aug 17, 2026

    @MEMZ-CHROER
    Author

    @Mister-Hope 你好!感谢你的 #1538 提供完整的 clean URL 方案。想知会一下:我在 #1720 提交了一个针对性的快速修复(resolveRoutePath 在 .html 路由不存在时回退目录路由),用于解决 #1538 落地前用户实际踩到的 404 / 无限重定向 bug(部署在 Vercel 等会规范化 URL 的托管时)。

    它与 #1538 不冲突:仅在规范化的 .html 路由不存在时回退目录;#1538 合入后 inferRoutePath 返回 clean 格式,此回退自然失效。

    供你判断取舍——如果 #1538 很快合入可以关闭 #1720,我完全理解;如果 #1538 尚需时日,这个小修复可以先保线上。无论如何都感谢你的 clean URL 方案。🙏

  2. Mister-Hope commented on Aug 17, 2026

    @Mister-Hope
    Member

    简短版本;我根本就不同意你的expected behavior。而且我那个搁置的pr合并了也不会解决你这个问题。

    长:

    /foo 和 /foo/ 本来就应该是不同的东西(/foo.md 和 /foo/README.md)。我不认为文档类框架应当默认进行这类危险的回退。如果你有需求,你可以自己自定义一下resolver,也就是你所谓的pr是可以在你项目里直接自定义的。

    cc @meteorlxy

  3. MEMZ-CHROER commented on Aug 17, 2026

    @MEMZ-CHROER
    Author

    理解了,感谢你指出正确方向!/foo 与 /foo/ 确实是不同页面(foo.md vs foo/README.md),默认回退确实危险。我试试在项目里自定义 resolver 的方式,不打扰 core。

  4. MEMZ-CHROER commented on Aug 17, 2026

    @MEMZ-CHROER
    Author

    @Mister-Hope 按你说的方向做了一轮实验,汇报结果(实验在本地 dev 环境完成,未改动 core):

    方案:在项目侧 clientConfig.enhance 里用 router.beforeEach + resolveRoute 实现"同名文件优先,目录兜底":

    访问无后缀 /foo(非根、非目录、无扩展名)时:
    1. 若 /foo.html(foo.md)可访问 → 放行(同名文件优先)
    2. 若 /foo/(目录 index)可访问 → 重定向到 /foo/(目录兜底)
    3. 都没有 → 保持 404
    

    实验数据(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,等你判断。

  5. MEMZ-CHROER commented on Aug 17, 2026

    @MEMZ-CHROER
    Author

    补充:实验用的可运行实现(项目侧 docs/.vuepress/client.js):

    import { defineClientConfig, resolveRoute } from 'vuepress/client'
    
    export default defineClientConfig({
      // ... 你原有的 layouts / 组件注册 ...
      enhance: ({ router }) => {
        // 无后缀路径 → 同名文件优先,目录兜底
        // 1. /foo.html(foo.md)可访问 → 放行
        // 2. /foo/(目录 index)可访问 → 重定向到 /foo/
        // 3. 都没有 → 404
        router.beforeEach((to) => {
          const { path } = to
          if (
            path !== '/' &&
            !path.endsWith('/') &&
            !/^\/.+\.[a-zA-Z0-9]+$/.test(path)
          ) {
            const fileRoute = resolveRoute(`${path}.html`)
            const dirRoute = resolveRoute(`${path}/`)
            if (fileRoute.notFound && !dirRoute.notFound) {
              return `${path}/`
            }
          }
          return true
        })
      },
    })

    关键点:

    • 用 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(同名文件优先,不重定向)✅
  6. MEMZ-CHROER commented on Aug 17, 2026

    @MEMZ-CHROER
    Author

    @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/),两个场景实测通过
    • PR #1720:将同样的安全回退逻辑放入 resolveRoutePath(仅当原始路径无扩展名时触发),并带测试

    完全理解 /foo 与 /foo/ 语义不同(foo.md vs foo/README.md),所以回退仅在同名文件不存在时触发,不会混淆。

    想听听你对"安全回退是否值得内嵌 core"的看法。如果 core 不打算做,项目侧方案也完全够用。

  7. github-actions commented on Sep 2, 2026

    @github-actions

    This issue is marked as stale because it has not had recent activity. Issues marked with stale will be closed if they have no activity within 7 days.

  8. MEMZ-CHROER commented on Sep 12, 2026

    @MEMZ-CHROER
    Author

    感谢@Mister-Hope删除标签

  9. github-actions commented on Sep 28, 2026

    @github-actions

    This issue is marked as stale because it has not had recent activity. Issues marked with stale will be closed if they have no activity within 7 days.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions