diff --git a/e2e/docs/components/route-link.md b/e2e/docs/components/route-link.md
index 48dd4e0010..fbe08217e3 100644
--- a/e2e/docs/components/route-link.md
+++ b/e2e/docs/components/route-link.md
@@ -60,26 +60,23 @@
- text
- text text2
-### Hash and query
+### Query and hash
- text
- text
- text
- text
- text
-- text
- text
- text
- text
- text
- text
-- text
- text
- text
- text
- text
- text
-- text
### Relative
diff --git a/e2e/docs/router/navigate-by-link.md b/e2e/docs/router/navigate-by-link.md
index e496113275..9eb6e96187 100644
--- a/e2e/docs/router/navigate-by-link.md
+++ b/e2e/docs/router/navigate-by-link.md
@@ -5,7 +5,6 @@
- [Home with query](/README.md?home=true)
- [Home with query and hash](/README.md?home=true#home)
- [404 with hash](/404.md#404)
-- [404 with hash and query](/404.md#404?notFound=true)
## HTML Links
@@ -14,15 +13,31 @@
Home
Home
404
-404
+
+## HTML Clean Links
+
+Home
+404
+Home
+Home
+404
+
+## Markdown Clean Links
+
+> Non-recommended usage. HTML paths could not be prepended with `base` correctly.
+
+- [Home](/)
+- [404](/404)
+- [Home with query](/?home=true)
+- [Home with query and hash](/?home=true#home)
+- [404 with hash](/404#404)
## Markdown Links with html paths
+> Non-recommended usage. HTML paths could not be prepended with `base` correctly.
+
- [Home](/)
- [404](/404.html)
- [Home with query](/?home=true)
- [Home with query and hash](/?home=true#home)
- [404 with hash](/404.html#404)
-- [404 with hash and query](/404.html#404?notFound=true)
-
-> Non-recommended usage. HTML paths could not be prepended with `base` correctly.
diff --git a/e2e/docs/router/navigate-by-router.md b/e2e/docs/router/navigate-by-router.md
index b28397dbf7..b791ed7ea5 100644
--- a/e2e/docs/router/navigate-by-router.md
+++ b/e2e/docs/router/navigate-by-router.md
@@ -1,37 +1,61 @@
-Home
-404
-
-Home
-Home
-404
-404
+
+ Home
+ 404
+ Home
+ Home
+ 404
+
+
+
+ Home
+ 404
+ Home
+ Home
+ 404
+
diff --git a/e2e/tests/components/route-link.spec.ts b/e2e/tests/components/route-link.spec.ts
index e555744cac..d577ca953e 100644
--- a/e2e/tests/components/route-link.spec.ts
+++ b/e2e/tests/components/route-link.spec.ts
@@ -142,31 +142,28 @@ test('should render slots correctly', async ({ page }) => {
}
})
-test('should render hash and query correctly', async ({ page }) => {
+test('should render query and hash correctly', async ({ page }) => {
const CONFIGS = [
`${BASE}#hash`,
`${BASE}?query`,
`${BASE}?query#hash`,
`${BASE}?query=1#hash`,
`${BASE}?query=1&query=2#hash`,
- `${BASE}#hash?query=1&query=2`,
`${BASE}#hash`,
`${BASE}?query`,
`${BASE}?query#hash`,
`${BASE}?query=1#hash`,
`${BASE}?query=1&query=2#hash`,
- `${BASE}#hash?query=1&query=2`,
`#hash`,
`?query`,
`?query#hash`,
`?query=1#hash`,
`?query=1&query=2#hash`,
- `#hash?query=1&query=2`,
]
for (const [index, href] of CONFIGS.entries()) {
await expect(
- page.locator('.e2e-theme-content #hash-and-query + ul > li a').nth(index),
+ page.locator('.e2e-theme-content #query-and-hash + ul > li a').nth(index),
).toHaveAttribute('href', href)
}
})
diff --git a/e2e/tests/router/navigate-by-link.spec.ts b/e2e/tests/router/navigate-by-link.spec.ts
index 7c6f974992..9fbea5c6a6 100644
--- a/e2e/tests/router/navigate-by-link.spec.ts
+++ b/e2e/tests/router/navigate-by-link.spec.ts
@@ -6,86 +6,182 @@ test.beforeEach(async ({ page }) => {
})
test.describe('markdown links', () => {
+ const selector = '#markdown-links + ul > li > a'
+
test('should navigate to home correctly', async ({ page }) => {
- await page.locator('#markdown-links + ul > li > a').nth(0).click()
+ await page.locator(selector).nth(0).click()
await expect(page).toHaveURL(BASE)
await expect(page.locator('#home-h2')).toHaveText('Home H2')
})
test('should navigate to 404 page correctly', async ({ page }) => {
- await page.locator('#markdown-links + ul > li > a').nth(1).click()
+ await page.locator(selector).nth(1).click()
await expect(page).toHaveURL(`${BASE}404.html`)
await expect(page.locator('#notfound-h2')).toHaveText('NotFound H2')
})
test('should preserve query', async ({ page }) => {
- await page.locator('#markdown-links + ul > li > a').nth(2).click()
+ await page.locator(selector).nth(2).click()
await expect(page).toHaveURL(`${BASE}?home=true`)
await expect(page.locator('#home-h2')).toHaveText('Home H2')
})
test('should preserve query and hash', async ({ page }) => {
- await page.locator('#markdown-links + ul > li > a').nth(3).click()
+ await page.locator(selector).nth(3).click()
await expect(page).toHaveURL(`${BASE}?home=true#home`)
await expect(page.locator('#home-h2')).toHaveText('Home H2')
})
test('should preserve hash', async ({ page }) => {
- await page.locator('#markdown-links + ul > li > a').nth(4).click()
+ await page.locator(selector).nth(4).click()
await expect(page).toHaveURL(`${BASE}404.html#404`)
await expect(page.locator('#notfound-h2')).toHaveText('NotFound H2')
})
+})
+
+test.describe('html links', () => {
+ const selector = '#html-links + p > a'
+
+ test('should navigate to home correctly', async ({ page }) => {
+ await page.locator(selector).nth(0).click()
+ await expect(page).toHaveURL(BASE)
+ await expect(page.locator('#home-h2')).toHaveText('Home H2')
+ })
+
+ test('should navigate to 404 page correctly', async ({ page }) => {
+ await page.locator(selector).nth(1).click()
+ await expect(page).toHaveURL(`${BASE}404.html`)
+ await expect(page.locator('#notfound-h2')).toHaveText('NotFound H2')
+ })
+
+ test('should preserve query', async ({ page }) => {
+ await page.locator(selector).nth(2).click()
+ await expect(page).toHaveURL(`${BASE}?home=true`)
+ await expect(page.locator('#home-h2')).toHaveText('Home H2')
+ })
- test('should preserve hash and query', async ({ page }) => {
- await page.locator('#markdown-links + ul > li > a').nth(5).click()
- await expect(page).toHaveURL(`${BASE}404.html#404?notFound=true`)
+ test('should preserve query and hash', async ({ page }) => {
+ await page.locator(selector).nth(3).click()
+ await expect(page).toHaveURL(`${BASE}?home=true#home`)
+ await expect(page.locator('#home-h2')).toHaveText('Home H2')
+ })
+
+ test('should preserve hash', async ({ page }) => {
+ await page.locator(selector).nth(4).click()
+ await expect(page).toHaveURL(`${BASE}404.html#404`)
await expect(page.locator('#notfound-h2')).toHaveText('NotFound H2')
})
})
-test.describe('html links', () => {
+test.describe('html clean links', () => {
+ const selector = '#html-clean-links + p > a'
+
test('should navigate to home correctly', async ({ page }) => {
- await page.locator('#html-links + p > a').nth(0).click()
+ await page.locator(selector).nth(0).click()
await expect(page).toHaveURL(BASE)
await expect(page.locator('#home-h2')).toHaveText('Home H2')
})
test('should navigate to 404 page correctly', async ({ page }) => {
- await page.locator('#html-links + p > a').nth(1).click()
+ await page.locator('#html-clean-links + p> a').nth(1).click()
await expect(page).toHaveURL(`${BASE}404.html`)
await expect(page.locator('#notfound-h2')).toHaveText('NotFound H2')
})
test('should preserve query', async ({ page }) => {
- await page.locator('#html-links + p > a').nth(2).click()
+ await page.locator(selector).nth(2).click()
await expect(page).toHaveURL(`${BASE}?home=true`)
await expect(page.locator('#home-h2')).toHaveText('Home H2')
})
test('should preserve query and hash', async ({ page }) => {
- await page.locator('#html-links + p > a').nth(3).click()
+ await page.locator(selector).nth(3).click()
await expect(page).toHaveURL(`${BASE}?home=true#home`)
await expect(page.locator('#home-h2')).toHaveText('Home H2')
})
test('should preserve hash', async ({ page }) => {
- await page.locator('#html-links + p > a').nth(4).click()
+ await page.locator(selector).nth(4).click()
await expect(page).toHaveURL(`${BASE}404.html#404`)
await expect(page.locator('#notfound-h2')).toHaveText('NotFound H2')
})
+})
- test('should preserve hash and query', async ({ page }) => {
- await page.locator('#html-links + p > a').nth(5).click()
- await expect(page).toHaveURL(`${BASE}404.html#404?notFound=true`)
- await expect(page.locator('#notfound-h2')).toHaveText('NotFound H2')
+test.describe('markdown clean links', () => {
+ const selector = '#markdown-clean-links + blockquote + ul > li > a'
+
+ test('should navigate to home correctly', async ({ page }) => {
+ const locator = page.locator(selector).nth(0)
+
+ if (BASE === '/') {
+ await locator.click()
+ await expect(page).toHaveURL('/')
+ await expect(page.locator('#home-h2')).toHaveText('Home H2')
+ } else {
+ await expect(locator).toHaveAttribute('href', '/')
+ await expect(locator).toHaveAttribute('target', '_blank')
+ }
+ })
+
+ test('should navigate to 404 page correctly', async ({ page }) => {
+ const locator = page.locator(selector).nth(1)
+
+ if (BASE === '/') {
+ await locator.click()
+ await expect(page).toHaveURL(`${BASE}404.html`)
+ await expect(page.locator('#notfound-h2')).toHaveText('NotFound H2')
+ } else {
+ await expect(locator).toHaveAttribute('href', '/404')
+ await expect(locator).toHaveAttribute('target', '_blank')
+ }
+ })
+
+ test('should preserve query', async ({ page }) => {
+ const locator = page.locator(selector).nth(2)
+
+ if (BASE === '/') {
+ await locator.click()
+ await expect(page).toHaveURL(`${BASE}?home=true`)
+ await expect(page.locator('#home-h2')).toHaveText('Home H2')
+ } else {
+ await expect(locator).toHaveAttribute('href', '/?home=true')
+ await expect(locator).toHaveAttribute('target', '_blank')
+ }
+ })
+
+ test('should preserve query and hash', async ({ page }) => {
+ const locator = page.locator(selector).nth(3)
+
+ if (BASE === '/') {
+ await locator.click()
+ await expect(page).toHaveURL(`${BASE}?home=true#home`)
+ await expect(page.locator('#home-h2')).toHaveText('Home H2')
+ } else {
+ await expect(locator).toHaveAttribute('href', '/?home=true#home')
+ await expect(locator).toHaveAttribute('target', '_blank')
+ }
+ })
+
+ test('should preserve hash', async ({ page }) => {
+ const locator = page.locator(selector).nth(4)
+
+ if (BASE === '/') {
+ await locator.click()
+ await expect(page).toHaveURL(`${BASE}404.html#404`)
+ await expect(page.locator('#notfound-h2')).toHaveText('NotFound H2')
+ } else {
+ await expect(locator).toHaveAttribute('href', '/404#404')
+ await expect(locator).toHaveAttribute('target', '_blank')
+ }
})
})
test.describe('markdown links with html paths', () => {
+ const selector = '#markdown-links-with-html-paths + blockquote + ul > li > a'
+
test('should navigate to home correctly', async ({ page }) => {
- const locator = page
- .locator('#markdown-links-with-html-paths + ul > li > a')
- .nth(0)
+ const locator = page.locator(selector).nth(0)
+
if (BASE === '/') {
await locator.click()
await expect(page).toHaveURL('/')
@@ -95,4 +191,56 @@ test.describe('markdown links with html paths', () => {
await expect(locator).toHaveAttribute('target', '_blank')
}
})
+
+ test('should navigate to 404 page correctly', async ({ page }) => {
+ const locator = page.locator(selector).nth(1)
+
+ if (BASE === '/') {
+ await locator.click()
+ await expect(page).toHaveURL(`${BASE}404.html`)
+ await expect(page.locator('#notfound-h2')).toHaveText('NotFound H2')
+ } else {
+ await expect(locator).toHaveAttribute('href', '/404.html')
+ await expect(locator).toHaveAttribute('target', '_blank')
+ }
+ })
+
+ test('should preserve query', async ({ page }) => {
+ const locator = page.locator(selector).nth(2)
+
+ if (BASE === '/') {
+ await locator.click()
+ await expect(page).toHaveURL(`${BASE}?home=true`)
+ await expect(page.locator('#home-h2')).toHaveText('Home H2')
+ } else {
+ await expect(locator).toHaveAttribute('href', '/?home=true')
+ await expect(locator).toHaveAttribute('target', '_blank')
+ }
+ })
+
+ test('should preserve query and hash', async ({ page }) => {
+ const locator = page.locator(selector).nth(3)
+
+ if (BASE === '/') {
+ await locator.click()
+ await expect(page).toHaveURL(`${BASE}?home=true#home`)
+ await expect(page.locator('#home-h2')).toHaveText('Home H2')
+ } else {
+ await expect(locator).toHaveAttribute('href', '/?home=true#home')
+ await expect(locator).toHaveAttribute('target', '_blank')
+ }
+ })
+
+ test('should preserve hash', async ({ page }) => {
+ const locator = page.locator(selector).nth(4)
+
+ if (BASE === '/') {
+ await locator.click()
+ await expect(page).toHaveURL(`${BASE}404.html#404`)
+ await expect(page.locator('#notfound-h2')).toHaveText('NotFound H2')
+ } else {
+ await expect(locator).toHaveAttribute('href', '/404.html#404')
+ await expect(locator).toHaveAttribute('target', '_blank')
+ }
+ })
})
diff --git a/e2e/tests/router/navigate-by-router.spec.ts b/e2e/tests/router/navigate-by-router.spec.ts
index b847a839a2..6af7fba0d6 100644
--- a/e2e/tests/router/navigate-by-router.spec.ts
+++ b/e2e/tests/router/navigate-by-router.spec.ts
@@ -5,38 +5,72 @@ test.beforeEach(async ({ page }) => {
await page.goto('router/navigate-by-router.html')
})
-test('should navigate to home correctly', async ({ page }) => {
- await page.locator('#home').click()
- await expect(page).toHaveURL(BASE)
- await expect(page.locator('#home-h2')).toHaveText('Home H2')
-})
+test.describe('should navigate to home correctly', () => {
+ test('full', async ({ page }) => {
+ await page.locator('#full .home').click()
+ await expect(page).toHaveURL(BASE)
+ await expect(page.locator('#home-h2')).toHaveText('Home H2')
+ })
-test('should navigate to 404 page correctly', async ({ page }) => {
- await page.locator('#not-found').click()
- await expect(page).toHaveURL(`${BASE}404.html`)
- await expect(page.locator('#notfound-h2')).toHaveText('NotFound H2')
+ test('clean', async ({ page }) => {
+ await page.locator('#clean .home').click()
+ await expect(page).toHaveURL(BASE)
+ await expect(page.locator('#home-h2')).toHaveText('Home H2')
+ })
})
-test('should preserve query', async ({ page }) => {
- await page.locator('#home-with-query').click()
- await expect(page).toHaveURL(`${BASE}?home=true`)
- await expect(page.locator('#home-h2')).toHaveText('Home H2')
+test.describe('should navigate to 404 page correctly', () => {
+ test('full', async ({ page }) => {
+ await page.locator('#full .not-found').click()
+ await expect(page).toHaveURL(`${BASE}404.html`)
+ await expect(page.locator('#notfound-h2')).toHaveText('NotFound H2')
+ })
+
+ test('clean', async ({ page }) => {
+ await page.locator('#clean .not-found').click()
+ await expect(page).toHaveURL(`${BASE}404.html`)
+ await expect(page.locator('#notfound-h2')).toHaveText('NotFound H2')
+ })
})
-test('should preserve query and hash', async ({ page }) => {
- await page.locator('#home-with-query-and-hash').click()
- await expect(page).toHaveURL(`${BASE}?home=true#home`)
- await expect(page.locator('#home-h2')).toHaveText('Home H2')
+test.describe('should preserve query', () => {
+ test('full', async ({ page }) => {
+ await page.locator('#full .home-with-query').click()
+ await expect(page).toHaveURL(`${BASE}?home=true`)
+ await expect(page.locator('#home-h2')).toHaveText('Home H2')
+ })
+
+ test('clean', async ({ page }) => {
+ await page.locator('#clean .home-with-query').click()
+ await expect(page).toHaveURL(`${BASE}?home=true`)
+ await expect(page.locator('#home-h2')).toHaveText('Home H2')
+ })
})
-test('should preserve hash', async ({ page }) => {
- await page.locator('#not-found-with-hash').click()
- await expect(page).toHaveURL(`${BASE}404.html#404`)
- await expect(page.locator('#notfound-h2')).toHaveText('NotFound H2')
+test.describe('should preserve query and hash', () => {
+ test('full', async ({ page }) => {
+ await page.locator('#full .home-with-query-and-hash').click()
+ await expect(page).toHaveURL(`${BASE}?home=true#home`)
+ await expect(page.locator('#home-h2')).toHaveText('Home H2')
+ })
+
+ test('clean', async ({ page }) => {
+ await page.locator('#clean .home-with-query-and-hash').click()
+ await expect(page).toHaveURL(`${BASE}?home=true#home`)
+ await expect(page.locator('#home-h2')).toHaveText('Home H2')
+ })
})
-test('should preserve hash and query', async ({ page }) => {
- await page.locator('#not-found-with-hash-and-query').click()
- await expect(page).toHaveURL(`${BASE}404.html#404?notFound=true`)
- await expect(page.locator('#notfound-h2')).toHaveText('NotFound H2')
+test.describe('should preserve hash', () => {
+ test('full', async ({ page }) => {
+ await page.locator('#full .not-found-with-hash').click()
+ await expect(page).toHaveURL(`${BASE}404.html#404`)
+ await expect(page.locator('#notfound-h2')).toHaveText('NotFound H2')
+ })
+
+ test('clean', async ({ page }) => {
+ await page.locator('#clean .not-found-with-hash').click()
+ await expect(page).toHaveURL(`${BASE}404.html#404`)
+ await expect(page.locator('#notfound-h2')).toHaveText('NotFound H2')
+ })
})
diff --git a/e2e/tests/router/resolve-route.spec.ts b/e2e/tests/router/resolve-route.spec.ts
index bad4350843..d9b9b12898 100644
--- a/e2e/tests/router/resolve-route.spec.ts
+++ b/e2e/tests/router/resolve-route.spec.ts
@@ -45,7 +45,7 @@ const TEST_CASES = [
selector: '#route-meta',
expected: {
path: '/page-data/route-meta.html',
- meta: { a: 0, b: 2, c: 3 },
+ meta: { a: 0, c: 3 },
notFound: false,
},
},
diff --git a/packages/bundler-vite/src/plugins/vuepressConfigPlugin.ts b/packages/bundler-vite/src/plugins/vuepressConfigPlugin.ts
index b1187019f0..ba33da2c64 100644
--- a/packages/bundler-vite/src/plugins/vuepressConfigPlugin.ts
+++ b/packages/bundler-vite/src/plugins/vuepressConfigPlugin.ts
@@ -65,6 +65,7 @@ const resolveDefine = async ({
const define: UserConfig['define'] = {
__VUEPRESS_VERSION__: JSON.stringify(app.version),
__VUEPRESS_BASE__: JSON.stringify(app.options.base),
+ __VUEPRESS_CLEAN_URL__: JSON.stringify(app.options.route.cleanUrl),
__VUEPRESS_DEV__: JSON.stringify(!isBuild),
__VUEPRESS_SSR__: JSON.stringify(isServer),
// @see http://link.vuejs.org/feature-flags
diff --git a/packages/bundler-webpack/src/config/handlePluginDefine.ts b/packages/bundler-webpack/src/config/handlePluginDefine.ts
index 8fdf06b1ac..ada678a3ab 100644
--- a/packages/bundler-webpack/src/config/handlePluginDefine.ts
+++ b/packages/bundler-webpack/src/config/handlePluginDefine.ts
@@ -21,6 +21,7 @@ export const handlePluginDefine = async ({
{
__VUEPRESS_VERSION__: JSON.stringify(app.version),
__VUEPRESS_BASE__: JSON.stringify(app.options.base),
+ __VUEPRESS_CLEAN_URL__: JSON.stringify(app.options.route.cleanUrl),
__VUEPRESS_DEV__: JSON.stringify(!isBuild),
__VUEPRESS_SSR__: JSON.stringify(isServer),
// @see http://link.vuejs.org/feature-flags
diff --git a/packages/cli/src/commands/dev/watchPageFiles.ts b/packages/cli/src/commands/dev/watchPageFiles.ts
index d9b884a2ee..b89dff9847 100644
--- a/packages/cli/src/commands/dev/watchPageFiles.ts
+++ b/packages/cli/src/commands/dev/watchPageFiles.ts
@@ -43,7 +43,7 @@ export const watchPageFiles = (app: App): FSWatcher[] => {
})
// watch page files
- const pagesWatcher = chokidar.watch(app.options.pagePatterns, {
+ const pagesWatcher = chokidar.watch(app.options.route.pagePatterns, {
cwd: app.dir.source(),
ignoreInitial: true,
})
diff --git a/packages/client/src/router/index.ts b/packages/client/src/router/index.ts
index 206f6f2f2b..3fba49b350 100644
--- a/packages/client/src/router/index.ts
+++ b/packages/client/src/router/index.ts
@@ -3,4 +3,4 @@ export { useRoute, useRouter } from 'vue-router'
export * from './resolveRoute.js'
export * from './resolveRouteFullPath.js'
-export * from './resolveRoutePath.js'
+export * from './resolveRouteCleanPath.js'
diff --git a/packages/client/src/router/resolveRoute.ts b/packages/client/src/router/resolveRoute.ts
index 8f848a56a3..15e8acd07c 100644
--- a/packages/client/src/router/resolveRoute.ts
+++ b/packages/client/src/router/resolveRoute.ts
@@ -1,7 +1,7 @@
-import { splitPath } from '@vuepress/shared'
+import { resolveRoutePathWithExt, splitPath } from '@vuepress/shared'
import { routes } from '../internal/routes.js'
import type { Route, RouteMeta } from '../types/index.js'
-import { resolveRoutePath } from './resolveRoutePath.js'
+import { resolveRouteCleanPath } from './resolveRouteCleanPath.js'
export interface ResolvedRoute
extends Route {
@@ -20,21 +20,23 @@ export const resolveRoute = (
const { pathname, hashAndQueries } = splitPath(path)
// resolve the route path
- const routePath = resolveRoutePath(pathname, currentPath)
- const routeFullPath = routePath + hashAndQueries
+ const cleanRoutePath = resolveRouteCleanPath(pathname, currentPath)
+ const routeFullPath = __VUEPRESS_CLEAN_URL__
+ ? cleanRoutePath
+ : resolveRoutePathWithExt(cleanRoutePath) + hashAndQueries
// the route not found
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- unsafe indexed access
- if (!routes.value[routePath]) {
+ if (!routes.value[cleanRoutePath]) {
return {
- ...routes.value['/404.html'],
+ ...routes.value['/404'],
path: routeFullPath,
notFound: true,
} as ResolvedRoute
}
return {
- ...routes.value[routePath],
+ ...routes.value[cleanRoutePath],
path: routeFullPath,
notFound: false,
} as ResolvedRoute
diff --git a/packages/client/src/router/resolveRoutePath.ts b/packages/client/src/router/resolveRouteCleanPath.ts
similarity index 96%
rename from packages/client/src/router/resolveRoutePath.ts
rename to packages/client/src/router/resolveRouteCleanPath.ts
index 74a2620955..58cb4846e4 100644
--- a/packages/client/src/router/resolveRoutePath.ts
+++ b/packages/client/src/router/resolveRouteCleanPath.ts
@@ -4,7 +4,7 @@ import { redirects, routes } from '../internal/routes.js'
/**
* Resolve route path with given raw path
*/
-export const resolveRoutePath = (
+export const resolveRouteCleanPath = (
pathname: string,
currentPath?: string,
): string => {
diff --git a/packages/client/src/router/resolveRouteFullPath.ts b/packages/client/src/router/resolveRouteFullPath.ts
index 694386d74b..8fd184f076 100644
--- a/packages/client/src/router/resolveRouteFullPath.ts
+++ b/packages/client/src/router/resolveRouteFullPath.ts
@@ -1,5 +1,5 @@
-import { splitPath } from '@vuepress/shared'
-import { resolveRoutePath } from './resolveRoutePath.js'
+import { resolveRoutePathWithExt, splitPath } from '@vuepress/shared'
+import { resolveRouteCleanPath } from './resolveRouteCleanPath.js'
/**
* Resolve route full path with given raw path
@@ -9,5 +9,11 @@ export const resolveRouteFullPath = (
currentPath?: string,
): string => {
const { pathname, hashAndQueries } = splitPath(path)
- return resolveRoutePath(pathname, currentPath) + hashAndQueries
+ const routeCleanPath = resolveRouteCleanPath(pathname, currentPath)
+
+ return (
+ (__VUEPRESS_CLEAN_URL__
+ ? routeCleanPath
+ : resolveRoutePathWithExt(routeCleanPath)) + hashAndQueries
+ )
}
diff --git a/packages/client/types.d.ts b/packages/client/types.d.ts
index 032e27fcfd..89087a11fb 100644
--- a/packages/client/types.d.ts
+++ b/packages/client/types.d.ts
@@ -2,6 +2,7 @@
declare const __VUEPRESS_VERSION__: string
declare const __VUEPRESS_BASE__: string
declare const __VUEPRESS_DEV__: boolean
+declare const __VUEPRESS_CLEAN_URL__: boolean
declare const __VUEPRESS_SSR__: boolean
declare const __VUE_HMR_RUNTIME__: Record
declare const __VUE_PROD_DEVTOOLS__: boolean
diff --git a/packages/core/src/app/resolveAppMarkdown.ts b/packages/core/src/app/resolveAppMarkdown.ts
index d3b35b4ef2..c2d86edd67 100644
--- a/packages/core/src/app/resolveAppMarkdown.ts
+++ b/packages/core/src/app/resolveAppMarkdown.ts
@@ -8,6 +8,14 @@ import type { App } from '../types/index.js'
* @internal
*/
export const resolveAppMarkdown = async (app: App): Promise => {
+ app.options.markdown.links ??= {}
+
+ // links plugin is not disabled
+ if (app.options.markdown.links !== false) {
+ // set the cleanUrl option
+ app.options.markdown.links.cleanUrl = app.options.route.cleanUrl
+ }
+
// plugin hook: extendsMarkdownOptions
await app.pluginApi.hooks.extendsMarkdownOptions.process(
app.options.markdown,
diff --git a/packages/core/src/app/resolveAppOptions.ts b/packages/core/src/app/resolveAppOptions.ts
index 756227ffe5..6d6282db93 100644
--- a/packages/core/src/app/resolveAppOptions.ts
+++ b/packages/core/src/app/resolveAppOptions.ts
@@ -41,8 +41,18 @@ export const resolveAppOptions = ({
bundler,
debug = false,
markdown = {},
- pagePatterns = ['**/*.md', '!.vuepress', '!node_modules'],
- permalinkPattern = null,
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
+ pagePatterns: _pagePatterns,
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
+ permalinkPattern: _permalinkPattern,
+ route: {
+ cleanUrl = false,
+ pagePatterns = ['**/*.md', '!.vuepress', '!node_modules'],
+ permalinkPattern = null,
+ } = {
+ pagePatterns: _pagePatterns,
+ permalinkPattern: _permalinkPattern,
+ },
plugins = [],
theme,
}: AppConfig): AppOptions => ({
@@ -68,8 +78,11 @@ export const resolveAppOptions = ({
bundler,
debug,
markdown,
- pagePatterns,
- permalinkPattern,
+ route: {
+ cleanUrl,
+ pagePatterns,
+ permalinkPattern,
+ },
plugins,
theme,
})
diff --git a/packages/core/src/app/resolveAppPages.ts b/packages/core/src/app/resolveAppPages.ts
index 51ed100308..6c6af1b979 100644
--- a/packages/core/src/app/resolveAppPages.ts
+++ b/packages/core/src/app/resolveAppPages.ts
@@ -13,7 +13,7 @@ export const resolveAppPages = async (app: App): Promise => {
log('resolveAppPages start')
// resolve page absolute file paths according to the page patterns
- const pageFilePaths = await globby(app.options.pagePatterns, {
+ const pageFilePaths = await globby(app.options.route.pagePatterns, {
absolute: true,
cwd: app.dir.source(),
})
@@ -25,7 +25,7 @@ export const resolveAppPages = async (app: App): Promise => {
pageFilePaths.map(async (filePath) => {
const page = await createPage(app, { filePath })
// if there is a 404 page, set the default layout to NotFound
- if (page.path === '/404.html') {
+ if (page.path === '/404') {
page.frontmatter.layout ??= 'NotFound'
hasNotFoundPage = true
}
@@ -37,7 +37,7 @@ export const resolveAppPages = async (app: App): Promise => {
if (!hasNotFoundPage) {
pages.push(
await createPage(app, {
- path: '/404.html',
+ path: '/404',
frontmatter: { layout: 'NotFound' },
content: '404 Not Found',
}),
diff --git a/packages/core/src/page/resolvePagePath.ts b/packages/core/src/page/resolvePagePath.ts
index 0edac439de..480533bd04 100644
--- a/packages/core/src/page/resolvePagePath.ts
+++ b/packages/core/src/page/resolvePagePath.ts
@@ -23,5 +23,9 @@ export const resolvePagePath = ({
)
}
- return encodeURI(pagePath.split('/').map(sanitizeFileName).join('/'))
+ return (
+ encodeURI(pagePath.split('/').map(sanitizeFileName).join('/'))
+ // get clean format
+ .replace(/\.html$/, '')
+ )
}
diff --git a/packages/core/src/page/resolvePagePermalink.ts b/packages/core/src/page/resolvePagePermalink.ts
index 4b62c069f7..9e59f56977 100644
--- a/packages/core/src/page/resolvePagePermalink.ts
+++ b/packages/core/src/page/resolvePagePermalink.ts
@@ -36,7 +36,7 @@ export const resolvePagePermalink = ({
}
const permalinkPattern =
- frontmatter.permalinkPattern || app.options.permalinkPattern
+ frontmatter.permalinkPattern || app.options.route.permalinkPattern
if (!isString(permalinkPattern)) {
return null
diff --git a/packages/core/src/types/app/options.ts b/packages/core/src/types/app/options.ts
index 74a11478f6..8681e61ccf 100644
--- a/packages/core/src/types/app/options.ts
+++ b/packages/core/src/types/app/options.ts
@@ -5,6 +5,27 @@ import type { Bundler } from '../bundler.js'
import type { PluginConfig } from '../plugin.js'
import type { Theme } from '../theme.js'
+export interface RouteOptions {
+ /**
+ * Whether to use "clean url"
+ */
+ cleanUrl?: boolean
+
+ /**
+ * Patterns to match the markdown files as pages
+ *
+ * @default ['**\/*.md', '!.vuepress', '!node_modules']
+ */
+ pagePatterns?: string[]
+
+ /**
+ * Pattern to generate permalink for pages
+ *
+ * @default null
+ */
+ permalinkPattern?: string | null
+}
+
/**
* Vuepress app common config that shared between dev and build
*/
@@ -69,18 +90,9 @@ export interface AppConfigCommon extends Partial {
markdown?: MarkdownOptions
/**
- * Patterns to match the markdown files as pages
- *
- * @default ['**\/*.md', '!.vuepress', '!node_modules']
+ * Vuepress route options
*/
- pagePatterns?: string[]
-
- /**
- * Pattern to generate permalink for pages
- *
- * @default null
- */
- permalinkPattern?: string | null
+ route?: RouteOptions
/**
* Vuepress bundler
@@ -177,11 +189,27 @@ export interface AppConfigBuild {
*
* It would be provided by user, typically via a config file.
*/
-export type AppConfig = AppConfigBuild & AppConfigCommon & AppConfigDev
+export type AppConfig = AppConfigBuild &
+ AppConfigCommon &
+ AppConfigDev & {
+ /**
+ * @deprecated use `route.pagePatterns` instead
+ */
+ pagePatterns?: string[]
+
+ /**
+ * @deprecated use `route.permalinkPattern` instead
+ */
+ permalinkPattern?: string | null
+ }
/**
* Vuepress app options that resolved from user config.
*
* It fills all optional fields with a default value.
*/
-export type AppOptions = Required
+export type AppOptions = Required<
+ AppConfigBuild & AppConfigCommon & AppConfigDev
+> & {
+ route: Required
+}
diff --git a/packages/core/tests/app/resolveAppOptions.spec.ts b/packages/core/tests/app/resolveAppOptions.spec.ts
index 29b2d07d2d..9ed1ea01f4 100644
--- a/packages/core/tests/app/resolveAppOptions.spec.ts
+++ b/packages/core/tests/app/resolveAppOptions.spec.ts
@@ -30,8 +30,11 @@ it('should create app options with default values', () => {
host: '0.0.0.0',
port: 8080,
open: false,
- pagePatterns: ['**/*.md', '!.vuepress', '!node_modules'],
- permalinkPattern: null,
+ route: {
+ cleanUrl: false,
+ pagePatterns: ['**/*.md', '!.vuepress', '!node_modules'],
+ permalinkPattern: null,
+ },
templateDev: path.normalize(
require.resolve('@vuepress/client/templates/dev.html'),
),
diff --git a/packages/core/tests/app/resolveAppPages.spec.ts b/packages/core/tests/app/resolveAppPages.spec.ts
index 73abc40368..5f83b8bb7f 100644
--- a/packages/core/tests/app/resolveAppPages.spec.ts
+++ b/packages/core/tests/app/resolveAppPages.spec.ts
@@ -13,9 +13,9 @@ it('should create two pages with default 404 page', async () => {
app.markdown = createMarkdown()
const pages = await resolveAppPages(app)
- const fooPage = pages.find((page) => page.path === '/foo.html')
- const barPage = pages.find((page) => page.path === '/bar.html')
- const notFoundPage = pages.find((page) => page.path === '/404.html')
+ const fooPage = pages.find((page) => page.path === '/foo')
+ const barPage = pages.find((page) => page.path === '/bar')
+ const notFoundPage = pages.find((page) => page.path === '/404')
expect(pages).toHaveLength(3)
expect(fooPage?.filePathRelative).toEqual('foo.md')
@@ -33,9 +33,9 @@ it('should create two pages with custom 404 page', async () => {
app.markdown = createMarkdown()
const pages = await resolveAppPages(app)
- const fooPage = pages.find((page) => page.path === '/foo.html')
- const barPage = pages.find((page) => page.path === '/bar.html')
- const notFoundPage = pages.find((page) => page.path === '/404.html')
+ const fooPage = pages.find((page) => page.path === '/foo')
+ const barPage = pages.find((page) => page.path === '/bar')
+ const notFoundPage = pages.find((page) => page.path === '/404')
expect(pages).toHaveLength(3)
expect(fooPage?.filePathRelative).toEqual('foo.md')
diff --git a/packages/core/tests/page/inferPagePath.spec.ts b/packages/core/tests/page/inferPagePath.spec.ts
index faac1b12b3..13419e2419 100644
--- a/packages/core/tests/page/inferPagePath.spec.ts
+++ b/packages/core/tests/page/inferPagePath.spec.ts
@@ -31,7 +31,7 @@ const TEST_CASES: [string, ReturnType][] = [
[
'foo.md',
{
- pathInferred: '/foo.html',
+ pathInferred: '/foo',
pathLocale: '/',
},
],
@@ -45,7 +45,7 @@ const TEST_CASES: [string, ReturnType][] = [
[
'en/foo.md',
{
- pathInferred: '/en/foo.html',
+ pathInferred: '/en/foo',
pathLocale: '/en/',
},
],
@@ -59,7 +59,7 @@ const TEST_CASES: [string, ReturnType][] = [
[
'zh/foo.md',
{
- pathInferred: '/zh/foo.html',
+ pathInferred: '/zh/foo',
pathLocale: '/zh/',
},
],
@@ -73,7 +73,7 @@ const TEST_CASES: [string, ReturnType][] = [
[
'中文/foo.md',
{
- pathInferred: '/中文/foo.html',
+ pathInferred: '/中文/foo',
pathLocale: '/中文/',
},
],
@@ -99,7 +99,7 @@ it('should use `/` as the default locale path', () => {
filePathRelative: 'en/foo/bar.md',
}),
).toEqual({
- pathInferred: '/en/foo/bar.html',
+ pathInferred: '/en/foo/bar',
pathLocale: '/',
})
})
diff --git a/packages/core/tests/page/resolvePagePath.spec.ts b/packages/core/tests/page/resolvePagePath.spec.ts
index 316d8c04e1..9ffb436e00 100644
--- a/packages/core/tests/page/resolvePagePath.spec.ts
+++ b/packages/core/tests/page/resolvePagePath.spec.ts
@@ -40,7 +40,7 @@ const TEST_CASES: [
},
},
],
- '/options.html',
+ '/options',
],
// use permalink
[
@@ -92,7 +92,7 @@ const TEST_CASES: [
options: {},
},
],
- '/inferred.html',
+ '/inferred',
],
]
diff --git a/packages/markdown/src/plugins/linksPlugin/isLinkInternal.ts b/packages/markdown/src/plugins/linksPlugin/isLinkInternal.ts
new file mode 100644
index 0000000000..5a420c186b
--- /dev/null
+++ b/packages/markdown/src/plugins/linksPlugin/isLinkInternal.ts
@@ -0,0 +1,34 @@
+const INTERNAL_WITH_EXT_REGEXP = /^([^#?]*?(?:\/|\.md|\.html))([#?].*)?$/
+const CLEAN_INTERNAL_REGEXP = /^([^#?]*?(?:[^.#?]))([#?].*)?$/
+
+export interface LinkInfo {
+ pathname: string
+ hashAndQuery: string
+}
+
+export const isLinkInternal = (
+ href: string,
+ cleanUrl: boolean,
+): LinkInfo | false => {
+ const extMatched = INTERNAL_WITH_EXT_REGEXP.exec(href)
+
+ if (extMatched) {
+ return {
+ pathname: extMatched[1],
+ hashAndQuery: extMatched[2] || '',
+ }
+ }
+
+ if (cleanUrl) {
+ const cleanMatched = CLEAN_INTERNAL_REGEXP.exec(href)
+
+ if (cleanMatched) {
+ return {
+ pathname: cleanMatched[1],
+ hashAndQuery: cleanMatched[2] || '',
+ }
+ }
+ }
+
+ return false
+}
diff --git a/packages/markdown/src/plugins/linksPlugin/linksPlugin.ts b/packages/markdown/src/plugins/linksPlugin/linksPlugin.ts
index 14c04eac46..a684f5e26f 100644
--- a/packages/markdown/src/plugins/linksPlugin/linksPlugin.ts
+++ b/packages/markdown/src/plugins/linksPlugin/linksPlugin.ts
@@ -1,10 +1,23 @@
-import { inferRoutePath, isLinkExternal } from '@vuepress/shared'
+import {
+ inferRoutePath,
+ isLinkExternal,
+ resolveRoutePathWithExt,
+} from '@vuepress/shared'
import type { PluginWithOptions } from 'markdown-it'
import type Token from 'markdown-it/lib/token.mjs'
import type { MarkdownEnv } from '../../types.js'
+import type { LinkInfo } from './isLinkInternal.js'
+import { isLinkInternal } from './isLinkInternal.js'
import { resolvePaths } from './resolvePaths.js'
export interface LinksPluginOptions {
+ /**
+ * Whether use "clean url"
+ *
+ * @default false
+ */
+ cleanUrl?: boolean
+
/**
* Additional attributes for external links
*
@@ -25,10 +38,21 @@ export interface LinksPluginOptions {
*/
internalTag?: 'a' | 'RouteLink' | 'RouterLink'
+ /**
+ * Method to check if a link is internal
+ *
+ * @default isLinkExternal from '@vuepress/shared'
+ */
+ isInternal?: (
+ href: string,
+ env: MarkdownEnv,
+ cleanUrl: boolean,
+ ) => LinkInfo | false
+
/**
* Method to check if a link is external
*
- * @default import { isLinkExternal } from '@vuepress/shared'
+ * @default isLinkExternal fom '@vuepress/shared'
*/
isExternal?: (href: string, env: MarkdownEnv) => boolean
}
@@ -45,6 +69,8 @@ export const linksPlugin: PluginWithOptions = (
): void => {
// tag of internal links
const internalTag = options.internalTag || 'RouteLink'
+ const cleanUrl = options.cleanUrl ?? false
+ const isInternal = options.isInternal ?? isLinkInternal
const isExternal =
options.isExternal ?? ((href, env) => isLinkExternal(href, env.base))
@@ -92,7 +118,7 @@ export const linksPlugin: PluginWithOptions = (
// check if a link is an internal link
const internalLinkMatch = hrefLink.match(
- /^([^#?]*?(?:\/|\.md|\.html))([#?].*)?$/,
+ /^([^#?]*?(?:\/|\.md|\.html|\/[A-z]+))([#?].*)?$/,
)
if (!internalLinkMatch) {
@@ -128,14 +154,17 @@ export const linksPlugin: PluginWithOptions = (
? absolutePath.replace(new RegExp(`^${base}`), '/')
: relativePath,
)
+
// replace the original href link with the normalized path
- hrefAttr[1] = `${normalizedPath}${rawHashAndQueries}`
+ hrefAttr[1] = `${cleanUrl ? normalizedPath : resolveRoutePathWithExt(normalizedPath)}${rawHashAndQueries}`
// set `hasOpenInternalLink` to modify the ending tag
hasOpenInternalLink = true
} else {
+ // ext is added here
const normalizedPath = inferRoutePath(absolutePath ?? relativePath)
+
// replace the original href link with the normalized path
- hrefAttr[1] = `${normalizedPath}${rawHashAndQueries}`
+ hrefAttr[1] = `${cleanUrl ? normalizedPath : resolveRoutePathWithExt(normalizedPath)}${rawHashAndQueries}`
}
// extract internal links for file / page existence check
diff --git a/packages/markdown/src/plugins/linksPlugin/resolvePaths.ts b/packages/markdown/src/plugins/linksPlugin/resolvePaths.ts
index d63b3a98e6..ed1d15686d 100644
--- a/packages/markdown/src/plugins/linksPlugin/resolvePaths.ts
+++ b/packages/markdown/src/plugins/linksPlugin/resolvePaths.ts
@@ -8,6 +8,7 @@ export const resolvePaths = (
rawPath: string,
base: string,
filePathRelative: string | null,
+ cleanUrl: boolean,
): {
absolutePath: string | null
relativePath: string
diff --git a/packages/markdown/tests/plugins/linksPlugin.spec.ts b/packages/markdown/tests/plugins/linksPlugin.spec.ts
index 17911a6f48..71d95b1e47 100644
--- a/packages/markdown/tests/plugins/linksPlugin.spec.ts
+++ b/packages/markdown/tests/plugins/linksPlugin.spec.ts
@@ -157,7 +157,6 @@ describe('internal links', () => {
item,
item.replace(link, `${link}#hash`),
item.replace(link, `${link}?a=1&b=2`),
- item.replace(link, `${link}#hash?a=1&b=2`),
item.replace(link, `${link}?a=1&b=2#hash`),
]
})
@@ -228,7 +227,6 @@ describe('internal links', () => {
{ raw, ...rest },
{ raw: `${raw}#hash`, ...rest },
{ raw: `${raw}?a=1&b=2`, ...rest },
- { raw: `${raw}#hash?a=1&b=2`, ...rest },
{ raw: `${raw}?a=1&b=2#hash`, ...rest },
])
@@ -262,7 +260,6 @@ describe('internal links', () => {
item,
item.replace(link, `${link}#hash`),
item.replace(link, `${link}?a=1&b=2`),
- item.replace(link, `${link}#hash?a=1&b=2`),
item.replace(link, `${link}?a=1&b=2#hash`),
]
})
@@ -301,7 +298,6 @@ describe('internal links', () => {
item,
item.replace(link, `${link}#hash`),
item.replace(link, `${link}?a=1&b=2`),
- item.replace(link, `${link}#hash?a=1&b=2`),
item.replace(link, `${link}?a=1&b=2#hash`),
]
})
@@ -342,7 +338,6 @@ describe('internal links', () => {
item,
item.replace(link, `${link}#hash`),
item.replace(link, `${link}?a=1&b=2`),
- item.replace(link, `${link}#hash?a=1&b=2`),
item.replace(link, `${link}?a=1&b=2#hash`),
]
})
@@ -416,7 +411,6 @@ describe('internal links', () => {
{ raw, ...rest },
{ raw: `${raw}#hash`, ...rest },
{ raw: `${raw}?a=1&b=2`, ...rest },
- { raw: `${raw}#hash?a=1&b=2`, ...rest },
{ raw: `${raw}?a=1&b=2#hash`, ...rest },
]),
)
@@ -454,7 +448,6 @@ describe('internal links', () => {
item,
item.replace(link, `${link}#hash`),
item.replace(link, `${link}?a=1&b=2`),
- item.replace(link, `${link}#hash?a=1&b=2`),
item.replace(link, `${link}?a=1&b=2#hash`),
]
})
@@ -528,7 +521,6 @@ describe('internal links', () => {
{ raw, ...rest },
{ raw: `${raw}#hash`, ...rest },
{ raw: `${raw}?a=1&b=2`, ...rest },
- { raw: `${raw}#hash?a=1&b=2`, ...rest },
{ raw: `${raw}?a=1&b=2#hash`, ...rest },
]),
)
@@ -565,7 +557,6 @@ describe('internal links', () => {
item,
item.replace(link, `${link}#hash`),
item.replace(link, `${link}?a=1&b=2`),
- item.replace(link, `${link}#hash?a=1&b=2`),
item.replace(link, `${link}?a=1&b=2#hash`),
]
})
@@ -639,7 +630,6 @@ describe('internal links', () => {
{ raw, ...rest },
{ raw: `${raw}#hash`, ...rest },
{ raw: `${raw}?a=1&b=2`, ...rest },
- { raw: `${raw}#hash?a=1&b=2`, ...rest },
{ raw: `${raw}?a=1&b=2#hash`, ...rest },
]),
)
diff --git a/packages/shared/src/utils/routes/index.ts b/packages/shared/src/utils/routes/index.ts
index 5e67f4b001..e0558e4a4c 100644
--- a/packages/shared/src/utils/routes/index.ts
+++ b/packages/shared/src/utils/routes/index.ts
@@ -1,5 +1,6 @@
export * from './inferRoutePath'
export * from './normalizeRoutePath.js'
+export * from './resolveRoutePathWithExt.js'
export * from './resolveLocalePath.js'
export * from './resolveRoutePathFromUrl.js'
export * from './splitPath.js'
diff --git a/packages/shared/src/utils/routes/inferRoutePath.ts b/packages/shared/src/utils/routes/inferRoutePath.ts
index 803cef50bf..775c174bcf 100644
--- a/packages/shared/src/utils/routes/inferRoutePath.ts
+++ b/packages/shared/src/utils/routes/inferRoutePath.ts
@@ -6,20 +6,20 @@ export const inferRoutePath = (rawPath: string): string => {
if (!rawPath || rawPath.endsWith('/')) return rawPath
// convert README.md to index.html
- let routePath = rawPath.replace(/(^|\/)README.md$/i, '$1index.html')
+ let routePath = rawPath.replace(/(^|\/)README.md$/i, '$1index')
- // convert /foo/bar.md to /foo/bar.html
+ // convert /foo/bar.md to /foo/bar
if (routePath.endsWith('.md')) {
- routePath = `${routePath.substring(0, routePath.length - 3)}.html`
+ routePath = routePath.substring(0, routePath.length - 3)
}
- // convert /foo/bar to /foo/bar.html
- else if (!routePath.endsWith('.html')) {
- routePath = `${routePath}.html`
+ // convert /foo/bar.html to /foo/bar
+ else if (routePath.endsWith('.html')) {
+ routePath = routePath.substring(0, routePath.length - 5)
}
- // convert /foo/index.html to /foo/
- if (routePath.endsWith('/index.html')) {
- routePath = routePath.substring(0, routePath.length - 10)
+ // convert /foo/index to /foo/
+ if (routePath.endsWith('/index')) {
+ routePath = routePath.substring(0, routePath.length - 5)
}
return routePath
diff --git a/packages/shared/src/utils/routes/resolveRoutePathWithExt.ts b/packages/shared/src/utils/routes/resolveRoutePathWithExt.ts
new file mode 100644
index 0000000000..cd48fb29b1
--- /dev/null
+++ b/packages/shared/src/utils/routes/resolveRoutePathWithExt.ts
@@ -0,0 +1,2 @@
+export const resolveRoutePathWithExt = (routePath: string): string =>
+ routePath.endsWith('/') ? routePath : `${routePath}.html`
diff --git a/packages/shared/tests/routes/inferRoutePath.spec.ts b/packages/shared/tests/routes/inferRoutePath.spec.ts
index 8869932c13..4accf3b63e 100644
--- a/packages/shared/tests/routes/inferRoutePath.spec.ts
+++ b/packages/shared/tests/routes/inferRoutePath.spec.ts
@@ -15,19 +15,19 @@ const TEST_CASES = [
['/foo/index.md', '/foo/'],
['/foo/index.html', '/foo/'],
['/foo/index', '/foo/'],
- ['README.md', 'index.html'],
- ['readme.md', 'index.html'],
- ['index.md', 'index.html'],
- ['index.html', 'index.html'],
- ['index', 'index.html'],
+ ['README.md', 'index'],
+ ['readme.md', 'index'],
+ ['index.md', 'index'],
+ ['index.html', 'index'],
+ ['index', 'index'],
// absolute non-index
- ['/foo', '/foo.html'],
- ['/foo.md', '/foo.html'],
- ['/foo.html', '/foo.html'],
- ['/foo/bar', '/foo/bar.html'],
- ['/foo/bar.md', '/foo/bar.html'],
- ['/foo/bar.html', '/foo/bar.html'],
+ ['/foo', '/foo'],
+ ['/foo.md', '/foo'],
+ ['/foo.html', '/foo'],
+ ['/foo/bar', '/foo/bar'],
+ ['/foo/bar.md', '/foo/bar'],
+ ['/foo/bar.html', '/foo/bar'],
// relative index without current
['foo/', 'foo/'],
@@ -38,19 +38,19 @@ const TEST_CASES = [
['foo/index', 'foo/'],
// relative non index without current
- ['foo', 'foo.html'],
- ['foo.md', 'foo.html'],
- ['foo.html', 'foo.html'],
- ['foo/bar', 'foo/bar.html'],
- ['foo/bar.md', 'foo/bar.html'],
- ['foo/bar.html', 'foo/bar.html'],
+ ['foo', 'foo'],
+ ['foo.md', 'foo'],
+ ['foo.html', 'foo'],
+ ['foo/bar', 'foo/bar'],
+ ['foo/bar.md', 'foo/bar'],
+ ['foo/bar.html', 'foo/bar'],
// unexpected corner cases
['', ''],
- ['.md', '.html'],
- ['foo/.md', 'foo/.html'],
- ['/.md', '/.html'],
- ['/foo/.md', '/foo/.html'],
+ ['.md', ''],
+ ['foo/.md', 'foo/'],
+ ['/.md', '/'],
+ ['/foo/.md', '/foo/'],
]
describe('should normalize clean paths correctly', () => {
diff --git a/packages/shared/tests/routes/normalizeRoutePath.spec.ts b/packages/shared/tests/routes/normalizeRoutePath.spec.ts
index 267b5c2731..d20e9b4fad 100644
--- a/packages/shared/tests/routes/normalizeRoutePath.spec.ts
+++ b/packages/shared/tests/routes/normalizeRoutePath.spec.ts
@@ -1,7 +1,9 @@
import { describe, expect, it } from 'vitest'
import { normalizeRoutePath } from '../../src/index.js'
-const TEST_CASES = [
+type TestCase = [[path: string, current?: string], expected: string]
+
+const TEST_CASES: TestCase[] = [
// absolute index
[['/'], '/'],
[['/README.md'], '/'],
@@ -15,19 +17,19 @@ const TEST_CASES = [
[['/foo/index.md'], '/foo/'],
[['/foo/index.html'], '/foo/'],
[['/foo/index'], '/foo/'],
- [['README.md'], 'index.html'],
- [['readme.md'], 'index.html'],
- [['index.md'], 'index.html'],
- [['index.html'], 'index.html'],
- [['index'], 'index.html'],
+ [['README.md'], 'index'],
+ [['readme.md'], 'index'],
+ [['index.md'], 'index'],
+ [['index.html'], 'index'],
+ [['index'], 'index'],
// absolute non-index
- [['/foo'], '/foo.html'],
- [['/foo.md'], '/foo.html'],
- [['/foo.html'], '/foo.html'],
- [['/foo/bar'], '/foo/bar.html'],
- [['/foo/bar.md'], '/foo/bar.html'],
- [['/foo/bar.html'], '/foo/bar.html'],
+ [['/foo'], '/foo'],
+ [['/foo.md'], '/foo'],
+ [['/foo.html'], '/foo'],
+ [['/foo/bar'], '/foo/bar'],
+ [['/foo/bar.md'], '/foo/bar'],
+ [['/foo/bar.html'], '/foo/bar'],
// relative index without current
[['foo/'], 'foo/'],
@@ -38,164 +40,164 @@ const TEST_CASES = [
[['foo/index'], 'foo/'],
// relative non index without current
- [['foo'], 'foo.html'],
- [['foo.md'], 'foo.html'],
- [['foo.html'], 'foo.html'],
- [['foo/bar'], 'foo/bar.html'],
- [['foo/bar.md'], 'foo/bar.html'],
- [['foo/bar.html'], 'foo/bar.html'],
+ [['foo'], 'foo'],
+ [['foo.md'], 'foo'],
+ [['foo.html'], 'foo'],
+ [['foo/bar'], 'foo/bar'],
+ [['foo/bar.md'], 'foo/bar'],
+ [['foo/bar.html'], 'foo/bar'],
// relative non index with current
- [['foo', '/'], '/foo.html'],
- [['foo', '/a.html'], '/foo.html'],
- [['foo', '/index.html'], '/foo.html'],
- [['foo', '/a/'], '/a/foo.html'],
- [['foo', '/a/index.html'], '/a/foo.html'],
- [['foo', '/a/b.html'], '/a/foo.html'],
- [['foo.md', '/'], '/foo.html'],
- [['foo.md', '/a.html'], '/foo.html'],
- [['foo.md', '/index.html'], '/foo.html'],
- [['foo.md', '/a/'], '/a/foo.html'],
- [['foo.md', '/a/index.html'], '/a/foo.html'],
- [['foo.md', '/a/b.html'], '/a/foo.html'],
- [['foo.html', '/'], '/foo.html'],
- [['foo.html', '/a.html'], '/foo.html'],
- [['foo.html', '/index.html'], '/foo.html'],
- [['foo.html', '/a/'], '/a/foo.html'],
- [['foo.html', '/a/index.html'], '/a/foo.html'],
- [['foo.html', '/a/b.html'], '/a/foo.html'],
- [['foo/bar', '/'], '/foo/bar.html'],
- [['foo/bar', '/a.html'], '/foo/bar.html'],
- [['foo/bar', '/index.html'], '/foo/bar.html'],
- [['foo/bar', '/a/'], '/a/foo/bar.html'],
- [['foo/bar', '/a/index.html'], '/a/foo/bar.html'],
- [['foo/bar', '/a/b.html'], '/a/foo/bar.html'],
- [['foo/bar.md', '/'], '/foo/bar.html'],
- [['foo/bar.md', '/a.html'], '/foo/bar.html'],
- [['foo/bar.md', '/index.html'], '/foo/bar.html'],
- [['foo/bar.md', '/a/'], '/a/foo/bar.html'],
- [['foo/bar.md', '/a/index.html'], '/a/foo/bar.html'],
- [['foo/bar.md', '/a/b.html'], '/a/foo/bar.html'],
- [['foo/bar.html', '/'], '/foo/bar.html'],
- [['foo/bar.html', '/a.html'], '/foo/bar.html'],
- [['foo/bar.html', '/index.html'], '/foo/bar.html'],
- [['foo/bar.html', '/a/'], '/a/foo/bar.html'],
- [['foo/bar.html', '/a/index.html'], '/a/foo/bar.html'],
- [['foo/bar.html', '/a/b.html'], '/a/foo/bar.html'],
- [['./foo', '/'], '/foo.html'],
- [['./foo', '/a.html'], '/foo.html'],
- [['./foo', '/index.html'], '/foo.html'],
- [['./foo', '/a/'], '/a/foo.html'],
- [['./foo', '/a/index.html'], '/a/foo.html'],
- [['./foo', '/a/b.html'], '/a/foo.html'],
- [['./foo.md', '/'], '/foo.html'],
- [['./foo.md', '/a.html'], '/foo.html'],
- [['./foo.md', '/index.html'], '/foo.html'],
- [['./foo.md', '/a/'], '/a/foo.html'],
- [['./foo.md', '/a/index.html'], '/a/foo.html'],
- [['./foo.md', '/a/b.html'], '/a/foo.html'],
- [['./foo.html', '/'], '/foo.html'],
- [['./foo.html', '/a.html'], '/foo.html'],
- [['./foo.html', '/index.html'], '/foo.html'],
- [['./foo.html', '/a/'], '/a/foo.html'],
- [['./foo.html', '/a/index.html'], '/a/foo.html'],
- [['./foo.html', '/a/b.html'], '/a/foo.html'],
- [['./foo/bar', '/'], '/foo/bar.html'],
- [['./foo/bar', '/a.html'], '/foo/bar.html'],
- [['./foo/bar', '/index.html'], '/foo/bar.html'],
- [['./foo/bar', '/a/'], '/a/foo/bar.html'],
- [['./foo/bar', '/a/index.html'], '/a/foo/bar.html'],
- [['./foo/bar', '/a/b.html'], '/a/foo/bar.html'],
- [['./foo/bar.md', '/'], '/foo/bar.html'],
- [['./foo/bar.md', '/a.html'], '/foo/bar.html'],
- [['./foo/bar.md', '/index.html'], '/foo/bar.html'],
- [['./foo/bar.md', '/a/'], '/a/foo/bar.html'],
- [['./foo/bar.md', '/a/index.html'], '/a/foo/bar.html'],
- [['./foo/bar.md', '/a/b.html'], '/a/foo/bar.html'],
- [['./foo/bar.html', '/'], '/foo/bar.html'],
- [['./foo/bar.html', '/a.html'], '/foo/bar.html'],
- [['./foo/bar.html', '/index.html'], '/foo/bar.html'],
- [['./foo/bar.html', '/a/'], '/a/foo/bar.html'],
- [['./foo/bar.html', '/a/index.html'], '/a/foo/bar.html'],
- [['./foo/bar.html', '/a/b.html'], '/a/foo/bar.html'],
- [['../foo', '/a/'], '/foo.html'],
- [['../foo', '/a/index.html'], '/foo.html'],
- [['../foo', '/a/b.html'], '/foo.html'],
- [['../foo.md', '/a/'], '/foo.html'],
- [['../foo.md', '/a/index.html'], '/foo.html'],
- [['../foo.md', '/a/b.html'], '/foo.html'],
- [['../foo.html', '/a/'], '/foo.html'],
- [['../foo.html', '/a/index.html'], '/foo.html'],
- [['../foo.html', '/a/b.html'], '/foo.html'],
- [['../foo/bar', '/a/'], '/foo/bar.html'],
- [['../foo/bar', '/a/index.html'], '/foo/bar.html'],
- [['../foo/bar', '/a/b.html'], '/foo/bar.html'],
- [['../foo/bar.md', '/a/'], '/foo/bar.html'],
- [['../foo/bar.md', '/a/index.html'], '/foo/bar.html'],
- [['../foo/bar.md', '/a/b.html'], '/foo/bar.html'],
- [['../foo/bar.html', '/a/'], '/foo/bar.html'],
- [['../foo/bar.html', '/a/index.html'], '/foo/bar.html'],
- [['../foo/bar.html', '/a/b.html'], '/foo/bar.html'],
+ [['foo', '/'], '/foo'],
+ [['foo', '/a.html'], '/foo'],
+ [['foo', '/index.html'], '/foo'],
+ [['foo', '/a/'], '/a/foo'],
+ [['foo', '/a/index.html'], '/a/foo'],
+ [['foo', '/a/b.html'], '/a/foo'],
+ [['foo.md', '/'], '/foo'],
+ [['foo.md', '/a.html'], '/foo'],
+ [['foo.md', '/index.html'], '/foo'],
+ [['foo.md', '/a/'], '/a/foo'],
+ [['foo.md', '/a/index.html'], '/a/foo'],
+ [['foo.md', '/a/b.html'], '/a/foo'],
+ [['foo.html', '/'], '/foo'],
+ [['foo.html', '/a.html'], '/foo'],
+ [['foo.html', '/index.html'], '/foo'],
+ [['foo.html', '/a/'], '/a/foo'],
+ [['foo.html', '/a/index.html'], '/a/foo'],
+ [['foo.html', '/a/b.html'], '/a/foo'],
+ [['foo/bar', '/'], '/foo/bar'],
+ [['foo/bar', '/a.html'], '/foo/bar'],
+ [['foo/bar', '/index.html'], '/foo/bar'],
+ [['foo/bar', '/a/'], '/a/foo/bar'],
+ [['foo/bar', '/a/index.html'], '/a/foo/bar'],
+ [['foo/bar', '/a/b.html'], '/a/foo/bar'],
+ [['foo/bar.md', '/'], '/foo/bar'],
+ [['foo/bar.md', '/a.html'], '/foo/bar'],
+ [['foo/bar.md', '/index.html'], '/foo/bar'],
+ [['foo/bar.md', '/a/'], '/a/foo/bar'],
+ [['foo/bar.md', '/a/index.html'], '/a/foo/bar'],
+ [['foo/bar.md', '/a/b.html'], '/a/foo/bar'],
+ [['foo/bar.html', '/'], '/foo/bar'],
+ [['foo/bar.html', '/a.html'], '/foo/bar'],
+ [['foo/bar.html', '/index.html'], '/foo/bar'],
+ [['foo/bar.html', '/a/'], '/a/foo/bar'],
+ [['foo/bar.html', '/a/index.html'], '/a/foo/bar'],
+ [['foo/bar.html', '/a/b.html'], '/a/foo/bar'],
+ [['./foo', '/'], '/foo'],
+ [['./foo', '/a.html'], '/foo'],
+ [['./foo', '/index.html'], '/foo'],
+ [['./foo', '/a/'], '/a/foo'],
+ [['./foo', '/a/index.html'], '/a/foo'],
+ [['./foo', '/a/b.html'], '/a/foo'],
+ [['./foo.md', '/'], '/foo'],
+ [['./foo.md', '/a.html'], '/foo'],
+ [['./foo.md', '/index.html'], '/foo'],
+ [['./foo.md', '/a/'], '/a/foo'],
+ [['./foo.md', '/a/index.html'], '/a/foo'],
+ [['./foo.md', '/a/b.html'], '/a/foo'],
+ [['./foo.html', '/'], '/foo'],
+ [['./foo.html', '/a.html'], '/foo'],
+ [['./foo.html', '/index.html'], '/foo'],
+ [['./foo.html', '/a/'], '/a/foo'],
+ [['./foo.html', '/a/index.html'], '/a/foo'],
+ [['./foo.html', '/a/b.html'], '/a/foo'],
+ [['./foo/bar', '/'], '/foo/bar'],
+ [['./foo/bar', '/a.html'], '/foo/bar'],
+ [['./foo/bar', '/index.html'], '/foo/bar'],
+ [['./foo/bar', '/a/'], '/a/foo/bar'],
+ [['./foo/bar', '/a/index.html'], '/a/foo/bar'],
+ [['./foo/bar', '/a/b.html'], '/a/foo/bar'],
+ [['./foo/bar.md', '/'], '/foo/bar'],
+ [['./foo/bar.md', '/a.html'], '/foo/bar'],
+ [['./foo/bar.md', '/index.html'], '/foo/bar'],
+ [['./foo/bar.md', '/a/'], '/a/foo/bar'],
+ [['./foo/bar.md', '/a/index.html'], '/a/foo/bar'],
+ [['./foo/bar.md', '/a/b.html'], '/a/foo/bar'],
+ [['./foo/bar.html', '/'], '/foo/bar'],
+ [['./foo/bar.html', '/a.html'], '/foo/bar'],
+ [['./foo/bar.html', '/index.html'], '/foo/bar'],
+ [['./foo/bar.html', '/a/'], '/a/foo/bar'],
+ [['./foo/bar.html', '/a/index.html'], '/a/foo/bar'],
+ [['./foo/bar.html', '/a/b.html'], '/a/foo/bar'],
+ [['../foo', '/a/'], '/foo'],
+ [['../foo', '/a/index.html'], '/foo'],
+ [['../foo', '/a/b.html'], '/foo'],
+ [['../foo.md', '/a/'], '/foo'],
+ [['../foo.md', '/a/index.html'], '/foo'],
+ [['../foo.md', '/a/b.html'], '/foo'],
+ [['../foo.html', '/a/'], '/foo'],
+ [['../foo.html', '/a/index.html'], '/foo'],
+ [['../foo.html', '/a/b.html'], '/foo'],
+ [['../foo/bar', '/a/'], '/foo/bar'],
+ [['../foo/bar', '/a/index.html'], '/foo/bar'],
+ [['../foo/bar', '/a/b.html'], '/foo/bar'],
+ [['../foo/bar.md', '/a/'], '/foo/bar'],
+ [['../foo/bar.md', '/a/index.html'], '/foo/bar'],
+ [['../foo/bar.md', '/a/b.html'], '/foo/bar'],
+ [['../foo/bar.html', '/a/'], '/foo/bar'],
+ [['../foo/bar.html', '/a/index.html'], '/foo/bar'],
+ [['../foo/bar.html', '/a/b.html'], '/foo/bar'],
// absolute non index with current
- [['/foo', '/'], '/foo.html'],
- [['/foo', '/a.html'], '/foo.html'],
- [['/foo', '/index.html'], '/foo.html'],
- [['/foo', '/a/'], '/foo.html'],
- [['/foo', '/a/index.html'], '/foo.html'],
- [['/foo', '/a/b.html'], '/foo.html'],
- [['/foo.md', '/'], '/foo.html'],
- [['/foo.md', '/a.html'], '/foo.html'],
- [['/foo.md', '/index.html'], '/foo.html'],
- [['/foo.md', '/a/'], '/foo.html'],
- [['/foo.md', '/a/index.html'], '/foo.html'],
- [['/foo.md', '/a/b.html'], '/foo.html'],
- [['/foo.html', '/'], '/foo.html'],
- [['/foo.html', '/a.html'], '/foo.html'],
- [['/foo.html', '/index.html'], '/foo.html'],
- [['/foo.html', '/a/'], '/foo.html'],
- [['/foo.html', '/a/index.html'], '/foo.html'],
- [['/foo.html', '/a/b.html'], '/foo.html'],
- [['/foo/bar', '/'], '/foo/bar.html'],
- [['/foo/bar', '/a.html'], '/foo/bar.html'],
- [['/foo/bar', '/index.html'], '/foo/bar.html'],
- [['/foo/bar', '/a/'], '/foo/bar.html'],
- [['/foo/bar', '/a/index.html'], '/foo/bar.html'],
- [['/foo/bar', '/a/b.html'], '/foo/bar.html'],
- [['/foo/bar.md', '/'], '/foo/bar.html'],
- [['/foo/bar.md', '/a.html'], '/foo/bar.html'],
- [['/foo/bar.md', '/index.html'], '/foo/bar.html'],
- [['/foo/bar.md', '/a/'], '/foo/bar.html'],
- [['/foo/bar.md', '/a/index.html'], '/foo/bar.html'],
- [['/foo/bar.md', '/a/b.html'], '/foo/bar.html'],
- [['/foo/bar.html', '/'], '/foo/bar.html'],
- [['/foo/bar.html', '/a.html'], '/foo/bar.html'],
- [['/foo/bar.html', '/index.html'], '/foo/bar.html'],
- [['/foo/bar.html', '/a/'], '/foo/bar.html'],
- [['/foo/bar.html', '/a/index.html'], '/foo/bar.html'],
- [['/foo/bar.html', '/a/b.html'], '/foo/bar.html'],
+ [['/foo', '/'], '/foo'],
+ [['/foo', '/a.html'], '/foo'],
+ [['/foo', '/index.html'], '/foo'],
+ [['/foo', '/a/'], '/foo'],
+ [['/foo', '/a/index.html'], '/foo'],
+ [['/foo', '/a/b.html'], '/foo'],
+ [['/foo.md', '/'], '/foo'],
+ [['/foo.md', '/a.html'], '/foo'],
+ [['/foo.md', '/index.html'], '/foo'],
+ [['/foo.md', '/a/'], '/foo'],
+ [['/foo.md', '/a/index.html'], '/foo'],
+ [['/foo.md', '/a/b.html'], '/foo'],
+ [['/foo.html', '/'], '/foo'],
+ [['/foo.html', '/a.html'], '/foo'],
+ [['/foo.html', '/index.html'], '/foo'],
+ [['/foo.html', '/a/'], '/foo'],
+ [['/foo.html', '/a/index.html'], '/foo'],
+ [['/foo.html', '/a/b.html'], '/foo'],
+ [['/foo/bar', '/'], '/foo/bar'],
+ [['/foo/bar', '/a.html'], '/foo/bar'],
+ [['/foo/bar', '/index.html'], '/foo/bar'],
+ [['/foo/bar', '/a/'], '/foo/bar'],
+ [['/foo/bar', '/a/index.html'], '/foo/bar'],
+ [['/foo/bar', '/a/b.html'], '/foo/bar'],
+ [['/foo/bar.md', '/'], '/foo/bar'],
+ [['/foo/bar.md', '/a.html'], '/foo/bar'],
+ [['/foo/bar.md', '/index.html'], '/foo/bar'],
+ [['/foo/bar.md', '/a/'], '/foo/bar'],
+ [['/foo/bar.md', '/a/index.html'], '/foo/bar'],
+ [['/foo/bar.md', '/a/b.html'], '/foo/bar'],
+ [['/foo/bar.html', '/'], '/foo/bar'],
+ [['/foo/bar.html', '/a.html'], '/foo/bar'],
+ [['/foo/bar.html', '/index.html'], '/foo/bar'],
+ [['/foo/bar.html', '/a/'], '/foo/bar'],
+ [['/foo/bar.html', '/a/index.html'], '/foo/bar'],
+ [['/foo/bar.html', '/a/b.html'], '/foo/bar'],
- // only hash and query
+ // empty
[[''], ''],
// unexpected corner cases
- [['.md'], '.html'],
- [['foo/.md'], 'foo/.html'],
- [['/.md'], '/.html'],
- [['/foo/.md'], '/foo/.html'],
- [['.md', '/a/'], '/a/.html'],
- [['foo/.md', '/a/'], '/a/foo/.html'],
- [['/.md', '/a/'], '/.html'],
- [['/foo/.md', '/a/'], '/foo/.html'],
- [['.md', '/a/index.html'], '/a/.html'],
- [['foo/.md', '/a/index.html'], '/a/foo/.html'],
- [['/.md', '/a/index.html'], '/.html'],
- [['/foo/.md', '/a/index.html'], '/foo/.html'],
- [['.md', '/a/b.html'], '/a/.html'],
- [['foo/.md', '/a/b.html'], '/a/foo/.html'],
- [['/.md', '/a/b.html'], '/.html'],
- [['/foo/.md', '/a/b.html'], '/foo/.html'],
-] as const
+ [['.md'], ''],
+ [['foo/.md'], 'foo/'],
+ [['/.md'], '/'],
+ [['/foo/.md'], '/foo/'],
+ [['.md', '/a/'], '/a/'],
+ [['foo/.md', '/a/'], '/a/foo/'],
+ [['/.md', '/a/'], '/'],
+ [['/foo/.md', '/a/'], '/foo/'],
+ [['.md', '/a/index.html'], '/a/'],
+ [['foo/.md', '/a/index.html'], '/a/foo/'],
+ [['/.md', '/a/index.html'], '/'],
+ [['/foo/.md', '/a/index.html'], '/foo/'],
+ [['.md', '/a/b.html'], '/a/'],
+ [['foo/.md', '/a/b.html'], '/a/foo/'],
+ [['/.md', '/a/b.html'], '/'],
+ [['/foo/.md', '/a/b.html'], '/foo/'],
+]
describe('should normalize clean paths correctly', () => {
TEST_CASES.forEach(([[path, current], expected]) => {