feat: add without subfolders option for clean urls

pull/929/head
Divyansh Singh 3 years ago
parent 0a6ecea4b8
commit e5856ca65d

@ -8,7 +8,7 @@ export default defineConfig({
description: 'Vite & Vue powered static site generator.', description: 'Vite & Vue powered static site generator.',
lastUpdated: true, lastUpdated: true,
cleanUrls: 'off', cleanUrls: 'without-subfolders',
themeConfig: { themeConfig: {
nav: nav(), nav: nav(),

@ -115,7 +115,7 @@ Below shows the the full option you may define within this object.
interface MarkdownOptions extends MarkdownIt.Options { interface MarkdownOptions extends MarkdownIt.Options {
// Syntax highlight theme for Shiki. // Syntax highlight theme for Shiki.
// See: https://github.com/shikijs/shiki/blob/main/docs/themes.md#all-themes // See: https://github.com/shikijs/shiki/blob/main/docs/themes.md#all-themes
theme?: Shiki.Theme | { light: Shiki.Theme, dark: Shiki.Theme } theme?: Shiki.Theme | { light: Shiki.Theme; dark: Shiki.Theme }
// Enable line numbers in code block. // Enable line numbers in code block.
lineNumbers?: boolean lineNumbers?: boolean
@ -173,19 +173,27 @@ export default {
} }
``` ```
## cleanUrls ## cleanUrls (Experimental)
- Type: `"off" | "with-subfolders"` - Type: `'disabled' | 'without-subfolders' | 'with-subfolders'`
- Default: `"off"` - Default: `'disabled'`
| Option | Page | Generated page | URL | Generated 404 | Allows removing trailing `.html` from URLs and, optionally, generating clean directory structure. Available modes:
|-------------------|---------------|---------------------|--------------|----------------|
| `off` | foo/bar.md | foo/bar.html | foo/bar.html | /404.html |
| `with-subfolders` | foo/bar.md | foo/bar/index.html | foo/bar | /404.html |
| Mode | Page | Generated Page | URL |
| :--------------------: | :-------: | :---------------: | :---------: |
| `'disabled'` | `/foo.md` | `/foo.html` | `/foo.html` |
| `'without-subfolders'` | `/foo.md` | `/foo.html` | `/foo` |
| `'with-subfolders'` | `/foo.md` | `/foo/index.html` | `/foo` |
::: warning
Enabling this may require additional configuration on your hosting platform. For it to work, your server must serve the generated page on requesting the URL (see above table) **without a redirect**.
:::
```ts ```ts
export default { export default {
cleanUrls: "with-subfolders" cleanUrls: 'with-subfolders'
} }
``` ```

@ -12,7 +12,7 @@ If you're coming from VitePress 0.x version, there're several breaking changes d
- `children` key is now named `items`. - `children` key is now named `items`.
- Top level item may not contain `link` at the moment. We're planning to bring it back. - Top level item may not contain `link` at the moment. We're planning to bring it back.
- `repo`, `repoLabel`, `docsDir`, `docsBranch`, `editLinks`, `editLinkText` are removed in favor of more flexible api. - `repo`, `repoLabel`, `docsDir`, `docsBranch`, `editLinks`, `editLinkText` are removed in favor of more flexible api.
- For adding GitHub link with icon to the nav, use [Social Links](./theme-nav.html#navigation-links) feature. - For adding GitHub link with icon to the nav, use [Social Links](./theme-nav#navigation-links) feature.
- For adding "Edit this page" feature, use [Edit Link](./theme-edit-link) feature. - For adding "Edit this page" feature, use [Edit Link](./theme-edit-link) feature.
- `lastUpdated` option is now split into `config.lastUpdated` and `themeConfig.lastUpdatedText`. - `lastUpdated` option is now split into `config.lastUpdated` and `themeConfig.lastUpdatedText`.
- `carbonAds.carbon` is changed to `carbonAds.code`. - `carbonAds.carbon` is changed to `carbonAds.code`.

@ -4,7 +4,7 @@
### Images ### Images
Unlike VuePress, VitePress handles [`base`](/guide/asset-handling.html#base-url) of your config automatically when you use static image. Unlike VuePress, VitePress handles [`base`](./asset-handling#base-url) of your config automatically when you use static image.
Hence, now you can render images without `img` tag. Hence, now you can render images without `img` tag.
@ -14,7 +14,7 @@ Hence, now you can render images without `img` tag.
``` ```
::: warning ::: warning
For dynamic images you still need `withBase` as shown in [Base URL guide](/guide/asset-handling.html#base-url). For dynamic images you still need `withBase` as shown in [Base URL guide](./asset-handling#base-url).
::: :::
Use `<img.*withBase\('(.*)'\).*alt="([^"]*)".*>` regex to find and replace it with `![$2]($1)` to replace all the images with `![](...)` syntax. Use `<img.*withBase\('(.*)'\).*alt="([^"]*)".*>` regex to find and replace it with `![$2]($1)` to replace all the images with `![](...)` syntax.

@ -4,7 +4,7 @@ The Nav is the navigation bar displayed on top of the page. It contains the site
## Site Title and Logo ## Site Title and Logo
By default, nav shows the title of the site refferencing [`config.title`](../config/app-configs.html#title) value. If you would like to change what's displayed on nav, you may define custom text in `themeConfig.siteTitle` option. By default, nav shows the title of the site refferencing [`config.title`](../config/app-configs#title) value. If you would like to change what's displayed on nav, you may define custom text in `themeConfig.siteTitle` option.
```js ```js
export default { export default {

@ -73,12 +73,12 @@
"dependencies": { "dependencies": {
"@docsearch/css": "^3.1.1", "@docsearch/css": "^3.1.1",
"@docsearch/js": "^3.1.1", "@docsearch/js": "^3.1.1",
"@vitejs/plugin-vue": "^3.0.0-beta.0", "@vitejs/plugin-vue": "^3.0.0-beta.1",
"@vue/devtools-api": "^6.2.0", "@vue/devtools-api": "^6.2.0",
"@vueuse/core": "^8.7.5", "@vueuse/core": "^8.7.5",
"body-scroll-lock": "^4.0.0-beta.0", "body-scroll-lock": "^4.0.0-beta.0",
"shiki": "^0.10.1", "shiki": "^0.10.1",
"vite": "3.0.0-beta.5", "vite": "^3.0.0-beta.7",
"vue": "^3.2.37" "vue": "^3.2.37"
}, },
"devDependencies": { "devDependencies": {

@ -24,7 +24,7 @@ importers:
'@types/node': ^18.0.1 '@types/node': ^18.0.1
'@types/polka': ^0.5.4 '@types/polka': ^0.5.4
'@types/prompts': ^2.0.14 '@types/prompts': ^2.0.14
'@vitejs/plugin-vue': ^3.0.0-beta.0 '@vitejs/plugin-vue': ^3.0.0-beta.1
'@vue/devtools-api': ^6.2.0 '@vue/devtools-api': ^6.2.0
'@vueuse/core': ^8.7.5 '@vueuse/core': ^8.7.5
body-scroll-lock: ^4.0.0-beta.0 body-scroll-lock: ^4.0.0-beta.0
@ -67,19 +67,19 @@ importers:
sirv: ^2.0.2 sirv: ^2.0.2
supports-color: ^9.2.2 supports-color: ^9.2.2
typescript: ^4.7.4 typescript: ^4.7.4
vite: 3.0.0-beta.5 vite: ^3.0.0-beta.7
vitest: ^0.17.0 vitest: ^0.17.0
vue: ^3.2.37 vue: ^3.2.37
vue-tsc: ^0.38.2 vue-tsc: ^0.38.2
dependencies: dependencies:
'@docsearch/css': 3.1.1 '@docsearch/css': 3.1.1
'@docsearch/js': 3.1.1 '@docsearch/js': 3.1.1
'@vitejs/plugin-vue': 3.0.0-beta.0_hrw2jqbpnmgiltw2su6c7ud5mq '@vitejs/plugin-vue': 3.0.0-beta.1_gsdjzz3hbcro3set7v2rc2jro4
'@vue/devtools-api': 6.2.0 '@vue/devtools-api': 6.2.0
'@vueuse/core': 8.7.5_vue@3.2.37 '@vueuse/core': 8.8.0_vue@3.2.37
body-scroll-lock: 4.0.0-beta.0 body-scroll-lock: 4.0.0-beta.0
shiki: 0.10.1 shiki: 0.10.1
vite: 3.0.0-beta.5 vite: 3.0.0-beta.7
vue: 3.2.37 vue: 3.2.37
devDependencies: devDependencies:
'@rollup/plugin-alias': 3.1.9_rollup@2.75.7 '@rollup/plugin-alias': 3.1.9_rollup@2.75.7
@ -97,7 +97,7 @@ importers:
'@types/markdown-it': 12.2.3 '@types/markdown-it': 12.2.3
'@types/micromatch': 4.0.2 '@types/micromatch': 4.0.2
'@types/minimist': 1.2.2 '@types/minimist': 1.2.2
'@types/node': 18.0.1 '@types/node': 18.0.3
'@types/polka': 0.5.4 '@types/polka': 0.5.4
'@types/prompts': 2.0.14 '@types/prompts': 2.0.14
chokidar: 3.5.3 chokidar: 3.5.3
@ -459,14 +459,14 @@ packages:
/@types/accepts/1.3.5: /@types/accepts/1.3.5:
resolution: {integrity: sha512-jOdnI/3qTpHABjM5cx1Hc0sKsPoYCp+DP/GJRGtDlPd7fiV9oXGGIcjW/ZOxLIvjGz8MA+uMZI9metHlgqbgwQ==} resolution: {integrity: sha512-jOdnI/3qTpHABjM5cx1Hc0sKsPoYCp+DP/GJRGtDlPd7fiV9oXGGIcjW/ZOxLIvjGz8MA+uMZI9metHlgqbgwQ==}
dependencies: dependencies:
'@types/node': 18.0.1 '@types/node': 18.0.3
dev: true dev: true
/@types/body-parser/1.19.2: /@types/body-parser/1.19.2:
resolution: {integrity: sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==} resolution: {integrity: sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==}
dependencies: dependencies:
'@types/connect': 3.4.35 '@types/connect': 3.4.35
'@types/node': 18.0.1 '@types/node': 18.0.3
dev: true dev: true
/@types/body-scroll-lock/3.1.0: /@types/body-scroll-lock/3.1.0:
@ -496,7 +496,7 @@ packages:
/@types/connect/3.4.35: /@types/connect/3.4.35:
resolution: {integrity: sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==} resolution: {integrity: sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==}
dependencies: dependencies:
'@types/node': 18.0.1 '@types/node': 18.0.3
dev: true dev: true
/@types/content-disposition/0.5.5: /@types/content-disposition/0.5.5:
@ -509,13 +509,13 @@ packages:
'@types/connect': 3.4.35 '@types/connect': 3.4.35
'@types/express': 4.17.13 '@types/express': 4.17.13
'@types/keygrip': 1.0.2 '@types/keygrip': 1.0.2
'@types/node': 18.0.1 '@types/node': 18.0.3
dev: true dev: true
/@types/cross-spawn/6.0.2: /@types/cross-spawn/6.0.2:
resolution: {integrity: sha512-KuwNhp3eza+Rhu8IFI5HUXRP0LIhqH5cAjubUvGXXthh4YYBuP2ntwEX+Cz8GJoZUHlKo247wPWOfA9LYEq4cw==} resolution: {integrity: sha512-KuwNhp3eza+Rhu8IFI5HUXRP0LIhqH5cAjubUvGXXthh4YYBuP2ntwEX+Cz8GJoZUHlKo247wPWOfA9LYEq4cw==}
dependencies: dependencies:
'@types/node': 18.0.1 '@types/node': 18.0.3
dev: true dev: true
/@types/debug/4.1.7: /@types/debug/4.1.7:
@ -535,7 +535,7 @@ packages:
/@types/express-serve-static-core/4.17.29: /@types/express-serve-static-core/4.17.29:
resolution: {integrity: sha512-uMd++6dMKS32EOuw1Uli3e3BPgdLIXmezcfHv7N4c1s3gkhikBplORPpMq3fuWkxncZN1reb16d5n8yhQ80x7Q==} resolution: {integrity: sha512-uMd++6dMKS32EOuw1Uli3e3BPgdLIXmezcfHv7N4c1s3gkhikBplORPpMq3fuWkxncZN1reb16d5n8yhQ80x7Q==}
dependencies: dependencies:
'@types/node': 18.0.1 '@types/node': 18.0.3
'@types/qs': 6.9.7 '@types/qs': 6.9.7
'@types/range-parser': 1.2.4 '@types/range-parser': 1.2.4
dev: true dev: true
@ -552,7 +552,7 @@ packages:
/@types/fs-extra/9.0.13: /@types/fs-extra/9.0.13:
resolution: {integrity: sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==} resolution: {integrity: sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==}
dependencies: dependencies:
'@types/node': 18.0.1 '@types/node': 18.0.3
dev: true dev: true
/@types/http-assert/1.5.3: /@types/http-assert/1.5.3:
@ -596,7 +596,7 @@ packages:
'@types/http-errors': 1.8.2 '@types/http-errors': 1.8.2
'@types/keygrip': 1.0.2 '@types/keygrip': 1.0.2
'@types/koa-compose': 3.2.5 '@types/koa-compose': 3.2.5
'@types/node': 18.0.1 '@types/node': 18.0.3
dev: true dev: true
/@types/linkify-it/3.0.2: /@types/linkify-it/3.0.2:
@ -632,8 +632,8 @@ packages:
resolution: {integrity: sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==} resolution: {integrity: sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==}
dev: true dev: true
/@types/node/18.0.1: /@types/node/18.0.3:
resolution: {integrity: sha512-CmR8+Tsy95hhwtZBKJBs0/FFq4XX7sDZHlGGf+0q+BRZfMbOTkzkj0AFAuTyXbObDIoanaBBW0+KEW+m3N16Wg==} resolution: {integrity: sha512-HzNRZtp4eepNitP+BD6k2L6DROIDG4Q0fm4x+dwfsr6LGmROENnok75VGw40628xf+iR24WeMFcHuuBDUAzzsQ==}
dev: true dev: true
/@types/normalize-package-data/2.4.1: /@types/normalize-package-data/2.4.1:
@ -645,14 +645,14 @@ packages:
dependencies: dependencies:
'@types/express': 4.17.13 '@types/express': 4.17.13
'@types/express-serve-static-core': 4.17.29 '@types/express-serve-static-core': 4.17.29
'@types/node': 18.0.1 '@types/node': 18.0.3
'@types/trouter': 3.1.1 '@types/trouter': 3.1.1
dev: true dev: true
/@types/prompts/2.0.14: /@types/prompts/2.0.14:
resolution: {integrity: sha512-HZBd99fKxRWpYCErtm2/yxUZv6/PBI9J7N4TNFffl5JbrYMHBwF25DjQGTW3b3jmXq+9P6/8fCIb2ee57BFfYA==} resolution: {integrity: sha512-HZBd99fKxRWpYCErtm2/yxUZv6/PBI9J7N4TNFffl5JbrYMHBwF25DjQGTW3b3jmXq+9P6/8fCIb2ee57BFfYA==}
dependencies: dependencies:
'@types/node': 18.0.1 '@types/node': 18.0.3
dev: true dev: true
/@types/qs/6.9.7: /@types/qs/6.9.7:
@ -666,14 +666,14 @@ packages:
/@types/resolve/1.17.1: /@types/resolve/1.17.1:
resolution: {integrity: sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==} resolution: {integrity: sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==}
dependencies: dependencies:
'@types/node': 18.0.1 '@types/node': 18.0.3
dev: true dev: true
/@types/serve-static/1.13.10: /@types/serve-static/1.13.10:
resolution: {integrity: sha512-nCkHGI4w7ZgAdNkrEu0bv+4xNV/XDqW+DydknebMOQwkpDGx8G+HTlj7R7ABI8i8nKxVw0wtKPi1D+lPOkh4YQ==} resolution: {integrity: sha512-nCkHGI4w7ZgAdNkrEu0bv+4xNV/XDqW+DydknebMOQwkpDGx8G+HTlj7R7ABI8i8nKxVw0wtKPi1D+lPOkh4YQ==}
dependencies: dependencies:
'@types/mime': 1.3.2 '@types/mime': 1.3.2
'@types/node': 18.0.1 '@types/node': 18.0.3
dev: true dev: true
/@types/trouter/3.1.1: /@types/trouter/3.1.1:
@ -684,14 +684,14 @@ packages:
resolution: {integrity: sha512-5d2RhCard1nQUC3aHcq/gHzWYO6K0WJmAbjO7mQJgCQKtZpgXxv1rOM6O/dBDhDYYVutk1sciOgNSe+5YyfM8A==} resolution: {integrity: sha512-5d2RhCard1nQUC3aHcq/gHzWYO6K0WJmAbjO7mQJgCQKtZpgXxv1rOM6O/dBDhDYYVutk1sciOgNSe+5YyfM8A==}
dev: false dev: false
/@vitejs/plugin-vue/3.0.0-beta.0_hrw2jqbpnmgiltw2su6c7ud5mq: /@vitejs/plugin-vue/3.0.0-beta.1_gsdjzz3hbcro3set7v2rc2jro4:
resolution: {integrity: sha512-t8os1QK1qpovpgYAJSOWYEu+Doy/DZRW1cNwMvUl0qo+Yv7D9a3cxo24oL01lbojcc9ABQhyvUP3BsvFNtriqg==} resolution: {integrity: sha512-cPVQHIKZkVEQ8qW7+BlbTrGJXNpP2aMKzVhQdTnWK9u6cSDmVdZOXHmKPO2KVvrNpFXXS8R7hHBXMsSApA+XOA==}
engines: {node: '>=14.18.0'} engines: {node: '>=14.18.0'}
peerDependencies: peerDependencies:
vite: ^3.0.0-alpha vite: ^3.0.0-alpha
vue: ^3.2.25 vue: ^3.2.25
dependencies: dependencies:
vite: 3.0.0-beta.5 vite: 3.0.0-beta.7
vue: 3.2.37 vue: 3.2.37
dev: false dev: false
@ -805,8 +805,8 @@ packages:
/@vue/shared/3.2.37: /@vue/shared/3.2.37:
resolution: {integrity: sha512-4rSJemR2NQIo9Klm1vabqWjD8rs/ZaJSzMxkMNeJS6lHiUjjUeYFbooN19NgFjztubEKh3WlZUeOLVdbbUWHsw==} resolution: {integrity: sha512-4rSJemR2NQIo9Klm1vabqWjD8rs/ZaJSzMxkMNeJS6lHiUjjUeYFbooN19NgFjztubEKh3WlZUeOLVdbbUWHsw==}
/@vueuse/core/8.7.5_vue@3.2.37: /@vueuse/core/8.8.0_vue@3.2.37:
resolution: {integrity: sha512-tqgzeZGoZcXzoit4kOGLWJibDMLp0vdm6ZO41SSUQhkhtrPhAg6dbIEPiahhUu6sZAmSYvVrZgEr5aKD51nrLA==} resolution: {integrity: sha512-TyvcNuA6O9WGkT8oQB4ERt8aBxe/e0fUs3SnibaxtLOr4eVXq42m3sLZgwgWOrJi4s9/8pTsMaJNn/6BUefwpQ==}
peerDependencies: peerDependencies:
'@vue/composition-api': ^1.1.0 '@vue/composition-api': ^1.1.0
vue: ^2.6.0 || ^3.2.0 vue: ^2.6.0 || ^3.2.0
@ -817,18 +817,18 @@ packages:
optional: true optional: true
dependencies: dependencies:
'@types/web-bluetooth': 0.0.14 '@types/web-bluetooth': 0.0.14
'@vueuse/metadata': 8.7.5 '@vueuse/metadata': 8.8.0
'@vueuse/shared': 8.7.5_vue@3.2.37 '@vueuse/shared': 8.8.0_vue@3.2.37
vue: 3.2.37 vue: 3.2.37
vue-demi: 0.13.2_vue@3.2.37 vue-demi: 0.13.2_vue@3.2.37
dev: false dev: false
/@vueuse/metadata/8.7.5: /@vueuse/metadata/8.8.0:
resolution: {integrity: sha512-emJZKRQSaEnVqmlu39NpNp8iaW+bPC2kWykWoWOZMSlO/0QVEmO/rt8A5VhOEJTKLX3vwTevqbiRy9WJRwVOQg==} resolution: {integrity: sha512-bRF+QPrw/RtP0al3nT/DtJ7CN0a6y6tEEO6hQ4CuJcGuUqd15eCOF6WKqQnC5DRaGFhsq/YwnQYsLTdJsW8f1A==}
dev: false dev: false
/@vueuse/shared/8.7.5_vue@3.2.37: /@vueuse/shared/8.8.0_vue@3.2.37:
resolution: {integrity: sha512-THXPvMBFmg6Gf6AwRn/EdTh2mhqwjGsB2Yfp374LNQSQVKRHtnJ0I42bsZTn7nuEliBxqUrGQm/lN6qUHmhJLw==} resolution: {integrity: sha512-DNZEs5Wy8hxxjAyWni6UK4BX/OGa8R7g0GX1tid5+AvmRbUwvUXL+0lVmGEuWPSQY4OZdYef1lvuFCi4Bfd59A==}
peerDependencies: peerDependencies:
'@vue/composition-api': ^1.1.0 '@vue/composition-api': ^1.1.0
vue: ^2.6.0 || ^3.2.0 vue: ^2.6.0 || ^3.2.0
@ -3430,8 +3430,8 @@ packages:
readable-stream: 3.6.0 readable-stream: 3.6.0
dev: true dev: true
/tinypool/0.2.1: /tinypool/0.2.2:
resolution: {integrity: sha512-HFU5ZYVq3wBfhSaf8qdqGsneaqXm0FgJQpoUlJbVdHpRLzm77IneKAD3RjzJWZvIv0YpPB9S7LUW53f6BE6ZSg==} resolution: {integrity: sha512-tp4n5OARNL3v8ntdJUyo5NsDfwvUtu8isB43USjrsQxQrADDKY6UGBkmFaw/2vNmEt8S/uSm2U5FhkiK1eAFGw==}
engines: {node: '>=14.0.0'} engines: {node: '>=14.0.0'}
dev: true dev: true
@ -3551,8 +3551,8 @@ packages:
engines: {node: '>= 0.8'} engines: {node: '>= 0.8'}
dev: true dev: true
/vite/3.0.0-beta.5: /vite/3.0.0-beta.7:
resolution: {integrity: sha512-SfesZuCME4fEmLy4hgsJAg55HRiTgDhH3oPM44XePrdKP5FqYvDkzpSWl6ldDOJYTskKWafGyyuYfXoxodv40Q==} resolution: {integrity: sha512-yjw154hB229qq5Bl6+/CJSTxC/yIDmDJbaAjE/pdracz3jytNEd2ovk5BvxgZT6+qPiUc2rRH3FgGqiZnweIFw==}
engines: {node: '>=14.18.0'} engines: {node: '>=14.18.0'}
hasBin: true hasBin: true
peerDependencies: peerDependencies:
@ -3576,34 +3576,6 @@ packages:
rollup: 2.75.7 rollup: 2.75.7
optionalDependencies: optionalDependencies:
fsevents: 2.3.2 fsevents: 2.3.2
dev: false
/vite/3.0.0-beta.6:
resolution: {integrity: sha512-jAxxCGXs6oIO3dFh7gwDEP9RqFzYY+ULDWawS1dd3HfM4FCr8rkOnLljDoBBIDdTNM8M7pDzdoYSmpPEOJqyZQ==}
engines: {node: '>=14.18.0'}
hasBin: true
peerDependencies:
less: '*'
sass: '*'
stylus: '*'
terser: ^5.4.0
peerDependenciesMeta:
less:
optional: true
sass:
optional: true
stylus:
optional: true
terser:
optional: true
dependencies:
esbuild: 0.14.48
postcss: 8.4.14
resolve: 1.22.1
rollup: 2.75.7
optionalDependencies:
fsevents: 2.3.2
dev: true
/vitest/0.17.0_supports-color@9.2.2: /vitest/0.17.0_supports-color@9.2.2:
resolution: {integrity: sha512-5YO9ubHo0Zg35mea3+zZAr4sCku32C3usvIH5COeJB48TZV/R0J9aGNtGOOqEWZYfOKP0pGZUvTokne3x/QEFg==} resolution: {integrity: sha512-5YO9ubHo0Zg35mea3+zZAr4sCku32C3usvIH5COeJB48TZV/R0J9aGNtGOOqEWZYfOKP0pGZUvTokne3x/QEFg==}
@ -3629,13 +3601,13 @@ packages:
dependencies: dependencies:
'@types/chai': 4.3.1 '@types/chai': 4.3.1
'@types/chai-subset': 1.3.3 '@types/chai-subset': 1.3.3
'@types/node': 18.0.1 '@types/node': 18.0.3
chai: 4.3.6 chai: 4.3.6
debug: 4.3.4_supports-color@9.2.2 debug: 4.3.4_supports-color@9.2.2
local-pkg: 0.4.1 local-pkg: 0.4.1
tinypool: 0.2.1 tinypool: 0.2.2
tinyspy: 0.3.3 tinyspy: 0.3.3
vite: 3.0.0-beta.6 vite: 3.0.0-beta.7
transitivePeerDependencies: transitivePeerDependencies:
- less - less
- sass - sass

@ -40,17 +40,15 @@ export function createRouter(
const route = reactive(getDefaultRoute()) const route = reactive(getDefaultRoute())
function go(href: string = inBrowser ? location.href : '/') { function go(href: string = inBrowser ? location.href : '/') {
// ensure correct deep link so page refresh lands on correct files.
const url = new URL(href, fakeHost) const url = new URL(href, fakeHost)
if (siteDataRef.value.cleanUrls === 'off') { if (siteDataRef.value.cleanUrls === 'disabled') {
// No clean URLs // ensure correct deep link so page refresh lands on correct files.
// Let's add ".html" if missing // if cleanUrls is enabled, the server should handle this
if (!url.pathname.endsWith('/') && !url.pathname.endsWith('.html')) { if (!url.pathname.endsWith('/') && !url.pathname.endsWith('.html')) {
url.pathname += '.html' url.pathname += '.html'
href = url.pathname + url.search + url.hash href = url.pathname + url.search + url.hash
} }
} }
if (inBrowser) { if (inBrowser) {
// save scroll position before changing url // save scroll position before changing url
history.replaceState({ scrollPosition: window.scrollY }, document.title) history.replaceState({ scrollPosition: window.scrollY }, document.title)
@ -101,10 +99,7 @@ export function createRouter(
} }
} }
} catch (err: any) { } catch (err: any) {
if ( if (!/fetch/.test(err.message) && !/^\/404(\.html|\/)?$/.test(href)) {
!err.message.match(/fetch/) &&
!/^[\\/]404[\\/]?(\.html)?$/.test(href)
) {
console.error(err) console.error(err)
} }

@ -8,7 +8,7 @@ import VPIconEdit from './icons/VPIconEdit.vue'
import VPLink from './VPLink.vue' import VPLink from './VPLink.vue'
import VPDocFooterLastUpdated from './VPDocFooterLastUpdated.vue' import VPDocFooterLastUpdated from './VPDocFooterLastUpdated.vue'
const { site, theme, page, frontmatter } = useData() const { theme, page, frontmatter } = useData()
const editLink = useEditLink() const editLink = useEditLink()
const control = usePrevNext() const control = usePrevNext()
@ -35,13 +35,13 @@ const hasLastUpdated = computed(() => {
<div v-if="control.prev || control.next" class="prev-next"> <div v-if="control.prev || control.next" class="prev-next">
<div class="pager"> <div class="pager">
<a v-if="control.prev" class="pager-link prev" :href="normalizeLink(control.prev.link, site.cleanUrls)"> <a v-if="control.prev" class="pager-link prev" :href="normalizeLink(control.prev.link)">
<span class="desc">{{ theme.docFooter?.prev ?? 'Previous page' }}</span> <span class="desc">{{ theme.docFooter?.prev ?? 'Previous page' }}</span>
<span class="title">{{ control.prev.text }} </span> <span class="title">{{ control.prev.text }} </span>
</a> </a>
</div> </div>
<div class="pager" :class="{ 'has-prev': control.prev }"> <div class="pager" :class="{ 'has-prev': control.prev }">
<a v-if="control.next" class="pager-link next" :href="normalizeLink(control.next.link, site.cleanUrls)"> <a v-if="control.next" class="pager-link next" :href="normalizeLink(control.next.link)">
<span class="desc">{{ theme.docFooter?.next ?? 'Next page' }}</span> <span class="desc">{{ theme.docFooter?.next ?? 'Next page' }}</span>
<span class="title">{{ control.next.text }}</span> <span class="title">{{ control.next.text }}</span>
</a> </a>

@ -1,11 +1,8 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed } from 'vue' import { computed } from 'vue'
import { useData } from 'vitepress'
import { normalizeLink } from '../support/utils' import { normalizeLink } from '../support/utils'
import VPIconExternalLink from './icons/VPIconExternalLink.vue' import VPIconExternalLink from './icons/VPIconExternalLink.vue'
const { site } = useData()
const props = defineProps<{ const props = defineProps<{
href?: string href?: string
noIcon?: boolean noIcon?: boolean
@ -19,7 +16,7 @@ const isExternal = computed(() => props.href && /^[a-z]+:/i.test(props.href))
:is="href ? 'a' : 'span'" :is="href ? 'a' : 'span'"
class="VPLink" class="VPLink"
:class="{ link: href }" :class="{ link: href }"
:href="href ? normalizeLink(href, site.cleanUrls) : undefined" :href="href ? normalizeLink(href) : undefined"
:target="isExternal ? '_blank' : undefined" :target="isExternal ? '_blank' : undefined"
:rel="isExternal ? 'noopener noreferrer' : undefined" :rel="isExternal ? 'noopener noreferrer' : undefined"
> >

@ -1,6 +1,5 @@
import { ref } from 'vue' import { ref } from 'vue'
import { withBase } from 'vitepress' import { withBase, useData } from 'vitepress'
import { cleanUrlsOptions } from '../../../../types/shared'
export const HASH_RE = /#.*$/ export const HASH_RE = /#.*$/
export const EXT_RE = /(index)?\.(md|html)$/ export const EXT_RE = /(index)?\.(md|html)$/
@ -70,14 +69,12 @@ export function normalize(path: string): string {
return decodeURI(path).replace(HASH_RE, '').replace(EXT_RE, '') return decodeURI(path).replace(HASH_RE, '').replace(EXT_RE, '')
} }
export function normalizeLink( export function normalizeLink(url: string): string {
url: string,
cleanUrls: cleanUrlsOptions = 'off'
): string {
if (isExternal(url)) { if (isExternal(url)) {
return url return url
} }
const { site } = useData()
const { pathname, search, hash } = new URL(url, 'http://example.com') const { pathname, search, hash } = new URL(url, 'http://example.com')
const normalizedPath = const normalizedPath =
@ -85,7 +82,7 @@ export function normalizeLink(
? url ? url
: `${pathname.replace( : `${pathname.replace(
/(\.md)?$/, /(\.md)?$/,
cleanUrls === 'off' ? '.html' : '' site.value.cleanUrls === 'disabled' ? '.html' : ''
)}${search}${hash}` )}${search}${hash}`
return withBase(normalizedPath) return withBase(normalizedPath)

@ -7,7 +7,6 @@ import { RollupOutput, OutputChunk, OutputAsset } from 'rollup'
import { HeadConfig, PageData, createTitle, notFoundPageData } from '../shared' import { HeadConfig, PageData, createTitle, notFoundPageData } from '../shared'
import { slash } from '../utils/slash' import { slash } from '../utils/slash'
import { SiteConfig, resolveSiteDataByRoute } from '../config' import { SiteConfig, resolveSiteDataByRoute } from '../config'
import { cleanUrlsOptions } from '../../../types/shared'
export async function renderPage( export async function renderPage(
config: SiteConfig, config: SiteConfig,
@ -151,28 +150,19 @@ export async function renderPage(
${inlinedScript} ${inlinedScript}
</body> </body>
</html>`.trim() </html>`.trim()
const createSubDirectory =
config.cleanUrls === 'with-subfolders' &&
!/(^|\/)(index|404).md$/.test(page)
const htmlFileName = path.join( const htmlFileName = path.join(
config.outDir, config.outDir,
transformHTMLFileName(page, config.cleanUrls) page.replace(/\.md$/, createSubDirectory ? '/index.html' : '.html')
) )
await fs.ensureDir(path.dirname(htmlFileName)) await fs.ensureDir(path.dirname(htmlFileName))
await fs.writeFile(htmlFileName, html) await fs.writeFile(htmlFileName, html)
} }
function transformHTMLFileName(
page: string,
shouldCleanUrls: cleanUrlsOptions
): string {
if (page === 'index.md' || page.endsWith('/index.md') || page === '404.md') {
return page.replace(/\.md$/, '.html')
}
return page.replace(
/\.md$/,
shouldCleanUrls !== 'off' ? '/index.html' : '.html'
)
}
function resolvePageImports( function resolvePageImports(
config: SiteConfig, config: SiteConfig,
page: string, page: string,

@ -16,12 +16,12 @@ import {
LocaleConfig, LocaleConfig,
DefaultTheme, DefaultTheme,
APPEARANCE_KEY, APPEARANCE_KEY,
createLangDictionary createLangDictionary,
CleanUrlsMode
} from './shared' } from './shared'
import { resolveAliases, DEFAULT_THEME_PATH } from './alias' import { resolveAliases, DEFAULT_THEME_PATH } from './alias'
import { MarkdownOptions } from './markdown/markdown' import { MarkdownOptions } from './markdown/markdown'
import _debug from 'debug' import _debug from 'debug'
import { cleanUrlsOptions } from '../../types/shared'
export { resolveSiteDataByRoute } from './shared' export { resolveSiteDataByRoute } from './shared'
@ -61,7 +61,7 @@ export interface UserConfig<ThemeConfig = any> {
scrollOffset?: number | string scrollOffset?: number | string
/** /**
* Enable MPA / zero-JS mode * Enable MPA / zero-JS mode.
* @experimental * @experimental
*/ */
mpa?: boolean mpa?: boolean
@ -74,11 +74,17 @@ export interface UserConfig<ThemeConfig = any> {
ignoreDeadLinks?: boolean ignoreDeadLinks?: boolean
/** /**
* Always use "clean URLs" without the `.html`. * @experimental
* Also generate static files as `foo/index.html` insted of `foo.html`. * Remove '.html' from URLs and generate clean directory structure.
* @default false *
* Available Modes:
* - `disabled`: generates `/foo.html` for every `/foo.md` and shows `/foo.html` in browser
* - `without-subfolders`: generates `/foo.html` for every `/foo.md` but shows `/foo` in browser
* - `with-subfolders`: generates `/foo/index.html` for every `/foo.md` and shows `/foo` in browser
*
* @default 'disabled'
*/ */
cleanUrls?: cleanUrlsOptions cleanUrls?: CleanUrlsMode
} }
export type RawConfigExports<ThemeConfig = any> = export type RawConfigExports<ThemeConfig = any> =
@ -96,6 +102,7 @@ export interface SiteConfig<ThemeConfig = any>
| 'mpa' | 'mpa'
| 'lastUpdated' | 'lastUpdated'
| 'ignoreDeadLinks' | 'ignoreDeadLinks'
| 'cleanUrls'
> { > {
root: string root: string
srcDir: string srcDir: string
@ -106,7 +113,6 @@ export interface SiteConfig<ThemeConfig = any>
tempDir: string tempDir: string
alias: AliasOptions alias: AliasOptions
pages: string[] pages: string[]
cleanUrls: cleanUrlsOptions
} }
const resolve = (root: string, file: string) => const resolve = (root: string, file: string) =>
@ -176,7 +182,7 @@ export async function resolveConfig(
shouldPreload: userConfig.shouldPreload, shouldPreload: userConfig.shouldPreload,
mpa: !!userConfig.mpa, mpa: !!userConfig.mpa,
ignoreDeadLinks: userConfig.ignoreDeadLinks, ignoreDeadLinks: userConfig.ignoreDeadLinks,
cleanUrls: userConfig.cleanUrls || 'off' cleanUrls: userConfig.cleanUrls || 'disabled'
} }
return config return config
@ -281,7 +287,7 @@ export async function resolveSiteData(
locales: userConfig.locales || {}, locales: userConfig.locales || {},
langs: createLangDictionary(userConfig), langs: createLangDictionary(userConfig),
scrollOffset: userConfig.scrollOffset || 90, scrollOffset: userConfig.scrollOffset || 90,
cleanUrls: userConfig.cleanUrls || 'off' cleanUrls: userConfig.cleanUrls || 'disabled'
} }
} }

@ -18,7 +18,6 @@ import anchor from 'markdown-it-anchor'
import attrs from 'markdown-it-attrs' import attrs from 'markdown-it-attrs'
import emoji from 'markdown-it-emoji' import emoji from 'markdown-it-emoji'
import toc from 'markdown-it-toc-done-right' import toc from 'markdown-it-toc-done-right'
import { cleanUrlsOptions } from '../../../types/shared'
export type ThemeOptions = Theme | { light: Theme; dark: Theme } export type ThemeOptions = Theme | { light: Theme; dark: Theme }
@ -57,8 +56,7 @@ export type { Header }
export const createMarkdownRenderer = async ( export const createMarkdownRenderer = async (
srcDir: string, srcDir: string,
options: MarkdownOptions = {}, options: MarkdownOptions = {},
base = '/', base = '/'
cleanUrls: cleanUrlsOptions = 'off'
): Promise<MarkdownRenderer> => { ): Promise<MarkdownRenderer> => {
const md = MarkdownIt({ const md = MarkdownIt({
html: true, html: true,
@ -83,8 +81,7 @@ export const createMarkdownRenderer = async (
rel: 'noopener noreferrer', rel: 'noopener noreferrer',
...options.externalLinks ...options.externalLinks
}, },
base, base
cleanUrls
) )
// 3rd party plugins // 3rd party plugins

@ -5,16 +5,14 @@
import MarkdownIt from 'markdown-it' import MarkdownIt from 'markdown-it'
import { MarkdownRenderer } from '../markdown' import { MarkdownRenderer } from '../markdown'
import { URL } from 'url' import { URL } from 'url'
import { EXTERNAL_URL_RE } from '../../shared' import { EXTERNAL_URL_RE, CleanUrlsMode } from '../../shared'
import { cleanUrlsOptions } from '../../../../types/shared'
const indexRE = /(^|.*\/)index.md(#?.*)$/i const indexRE = /(^|.*\/)index.md(#?.*)$/i
export const linkPlugin = ( export const linkPlugin = (
md: MarkdownIt, md: MarkdownIt,
externalAttrs: Record<string, string>, externalAttrs: Record<string, string>,
base: string, base: string
shouldCleanUrls: cleanUrlsOptions
) => { ) => {
md.renderer.rules.link_open = (tokens, idx, options, env, self) => { md.renderer.rules.link_open = (tokens, idx, options, env, self) => {
const token = tokens[idx] const token = tokens[idx]
@ -39,7 +37,7 @@ export const linkPlugin = (
// links to files (other than html/md) // links to files (other than html/md)
!/\.(?!html|md)\w+($|\?)/i.test(url) !/\.(?!html|md)\w+($|\?)/i.test(url)
) { ) {
normalizeHref(hrefAttr, shouldCleanUrls) normalizeHref(hrefAttr, env.cleanUrl)
} }
// encode vite-specific replace strings in case they appear in URLs // encode vite-specific replace strings in case they appear in URLs
@ -54,7 +52,7 @@ export const linkPlugin = (
function normalizeHref( function normalizeHref(
hrefAttr: [string, string], hrefAttr: [string, string],
shouldCleanUrls: cleanUrlsOptions shouldCleanUrls: CleanUrlsMode
) { ) {
let url = hrefAttr[1] let url = hrefAttr[1]
@ -63,17 +61,17 @@ export const linkPlugin = (
const [, path, hash] = indexMatch const [, path, hash] = indexMatch
url = path + hash url = path + hash
} else { } else {
let cleanUrl = url.replace(/[?#].*$/, '').replace(/\?.*$/, '') let cleanUrl = url.replace(/[?#].*$/, '')
// transform foo.md -> foo[.html] // transform foo.md -> foo[.html]
if (cleanUrl.endsWith('.md')) { if (cleanUrl.endsWith('.md')) {
cleanUrl = cleanUrl.replace( cleanUrl = cleanUrl.replace(
/\.md$/, /\.md$/,
shouldCleanUrls === 'off' ? '.html' : '' shouldCleanUrls === 'disabled' ? '.html' : ''
) )
} }
// transform ./foo -> ./foo[.html] // transform ./foo -> ./foo[.html]
if ( if (
shouldCleanUrls === 'off' && shouldCleanUrls === 'disabled' &&
!cleanUrl.endsWith('.html') && !cleanUrl.endsWith('.html') &&
!cleanUrl.endsWith('/') !cleanUrl.endsWith('/')
) { ) {

@ -3,13 +3,12 @@ import path from 'path'
import c from 'picocolors' import c from 'picocolors'
import matter from 'gray-matter' import matter from 'gray-matter'
import LRUCache from 'lru-cache' import LRUCache from 'lru-cache'
import { PageData, HeadConfig, EXTERNAL_URL_RE } from './shared' import { PageData, HeadConfig, EXTERNAL_URL_RE, CleanUrlsMode } from './shared'
import { slash } from './utils/slash' import { slash } from './utils/slash'
import { deeplyParseHeader } from './utils/parseHeader' import { deeplyParseHeader } from './utils/parseHeader'
import { getGitTimestamp } from './utils/getGitTimestamp' import { getGitTimestamp } from './utils/getGitTimestamp'
import { createMarkdownRenderer, MarkdownOptions } from './markdown/markdown' import { createMarkdownRenderer, MarkdownOptions } from './markdown/markdown'
import _debug from 'debug' import _debug from 'debug'
import { cleanUrlsOptions } from '../../types/shared'
const debug = _debug('vitepress:md') const debug = _debug('vitepress:md')
const cache = new LRUCache<string, MarkdownCompileResult>({ max: 1024 }) const cache = new LRUCache<string, MarkdownCompileResult>({ max: 1024 })
@ -30,9 +29,9 @@ export async function createMarkdownToVueRenderFn(
isBuild = false, isBuild = false,
base = '/', base = '/',
includeLastUpdatedData = false, includeLastUpdatedData = false,
cleanUrls: cleanUrlsOptions = 'off' cleanUrls: CleanUrlsMode = 'disabled'
) { ) {
const md = await createMarkdownRenderer(srcDir, options, base, cleanUrls) const md = await createMarkdownRenderer(srcDir, options, base)
pages = pages.map((p) => slash(p.replace(/\.md$/, ''))) pages = pages.map((p) => slash(p.replace(/\.md$/, '')))
@ -69,7 +68,7 @@ export async function createMarkdownToVueRenderFn(
md.__path = file md.__path = file
md.__relativePath = relativePath md.__relativePath = relativePath
const html = md.render(content) const html = md.render(content, { path: file, relativePath, cleanUrls })
const data = md.__data const data = md.__data
// validate data.links // validate data.links

@ -46,6 +46,7 @@ export async function createVitePressPlugin(
vite: userViteConfig, vite: userViteConfig,
pages, pages,
ignoreDeadLinks, ignoreDeadLinks,
lastUpdated,
cleanUrls cleanUrls
} = siteConfig } = siteConfig
@ -84,7 +85,7 @@ export async function createVitePressPlugin(
config.define, config.define,
config.command === 'build', config.command === 'build',
config.base, config.base,
siteConfig.lastUpdated, lastUpdated,
cleanUrls cleanUrls
) )
}, },

@ -7,7 +7,8 @@ export type {
LocaleConfig, LocaleConfig,
Header, Header,
DefaultTheme, DefaultTheme,
PageDataPayload PageDataPayload,
CleanUrlsMode
} from '../../types/shared' } from '../../types/shared'
export const EXTERNAL_URL_RE = /^https?:/i export const EXTERNAL_URL_RE = /^https?:/i

7
types/shared.d.ts vendored

@ -18,10 +18,14 @@ export interface Header {
slug: string slug: string
} }
export type cleanUrlsOptions = 'off' | 'with-subfolders' export type CleanUrlsMode =
| 'disabled'
| 'without-subfolders'
| 'with-subfolders'
export interface SiteData<ThemeConfig = any> { export interface SiteData<ThemeConfig = any> {
base: string base: string
cleanUrls?: CleanUrlsMode
/** /**
* Language of the site as it should be set on the `html` element. * Language of the site as it should be set on the `html` element.
@ -59,7 +63,6 @@ export interface SiteData<ThemeConfig = any> {
label: string label: string
} }
> >
cleanUrls: cleanUrlsOptions
} }
export type HeadConfig = export type HeadConfig =

Loading…
Cancel
Save