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.',
lastUpdated: true,
cleanUrls: 'off',
cleanUrls: 'without-subfolders',
themeConfig: {
nav: nav(),

@ -115,7 +115,7 @@ Below shows the the full option you may define within this object.
interface MarkdownOptions extends MarkdownIt.Options {
// Syntax highlight theme for Shiki.
// 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.
lineNumbers?: boolean
@ -173,19 +173,27 @@ export default {
}
```
## cleanUrls
## cleanUrls (Experimental)
- Type: `"off" | "with-subfolders"`
- Default: `"off"`
- Type: `'disabled' | 'without-subfolders' | 'with-subfolders'`
- Default: `'disabled'`
| Option | Page | Generated page | URL | Generated 404 |
|-------------------|---------------|---------------------|--------------|----------------|
| `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 |
Allows removing trailing `.html` from URLs and, optionally, generating clean directory structure. Available modes:
| 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
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`.
- 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.
- 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.
- `lastUpdated` option is now split into `config.lastUpdated` and `themeConfig.lastUpdatedText`.
- `carbonAds.carbon` is changed to `carbonAds.code`.

@ -4,7 +4,7 @@
### 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.
@ -14,7 +14,7 @@ Hence, now you can render images without `img` tag.
```
::: 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.

@ -4,7 +4,7 @@ The Nav is the navigation bar displayed on top of the page. It contains the site
## 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
export default {

@ -73,12 +73,12 @@
"dependencies": {
"@docsearch/css": "^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",
"@vueuse/core": "^8.7.5",
"body-scroll-lock": "^4.0.0-beta.0",
"shiki": "^0.10.1",
"vite": "3.0.0-beta.5",
"vite": "^3.0.0-beta.7",
"vue": "^3.2.37"
},
"devDependencies": {

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

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

@ -8,7 +8,7 @@ import VPIconEdit from './icons/VPIconEdit.vue'
import VPLink from './VPLink.vue'
import VPDocFooterLastUpdated from './VPDocFooterLastUpdated.vue'
const { site, theme, page, frontmatter } = useData()
const { theme, page, frontmatter } = useData()
const editLink = useEditLink()
const control = usePrevNext()
@ -35,13 +35,13 @@ const hasLastUpdated = computed(() => {
<div v-if="control.prev || control.next" class="prev-next">
<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="title">{{ control.prev.text }} </span>
</a>
</div>
<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="title">{{ control.next.text }}</span>
</a>

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

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

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

@ -16,12 +16,12 @@ import {
LocaleConfig,
DefaultTheme,
APPEARANCE_KEY,
createLangDictionary
createLangDictionary,
CleanUrlsMode
} from './shared'
import { resolveAliases, DEFAULT_THEME_PATH } from './alias'
import { MarkdownOptions } from './markdown/markdown'
import _debug from 'debug'
import { cleanUrlsOptions } from '../../types/shared'
export { resolveSiteDataByRoute } from './shared'
@ -61,7 +61,7 @@ export interface UserConfig<ThemeConfig = any> {
scrollOffset?: number | string
/**
* Enable MPA / zero-JS mode
* Enable MPA / zero-JS mode.
* @experimental
*/
mpa?: boolean
@ -74,11 +74,17 @@ export interface UserConfig<ThemeConfig = any> {
ignoreDeadLinks?: boolean
/**
* Always use "clean URLs" without the `.html`.
* Also generate static files as `foo/index.html` insted of `foo.html`.
* @default false
* @experimental
* Remove '.html' from URLs and generate clean directory structure.
*
* 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> =
@ -96,6 +102,7 @@ export interface SiteConfig<ThemeConfig = any>
| 'mpa'
| 'lastUpdated'
| 'ignoreDeadLinks'
| 'cleanUrls'
> {
root: string
srcDir: string
@ -106,7 +113,6 @@ export interface SiteConfig<ThemeConfig = any>
tempDir: string
alias: AliasOptions
pages: string[]
cleanUrls: cleanUrlsOptions
}
const resolve = (root: string, file: string) =>
@ -176,7 +182,7 @@ export async function resolveConfig(
shouldPreload: userConfig.shouldPreload,
mpa: !!userConfig.mpa,
ignoreDeadLinks: userConfig.ignoreDeadLinks,
cleanUrls: userConfig.cleanUrls || 'off'
cleanUrls: userConfig.cleanUrls || 'disabled'
}
return config
@ -281,7 +287,7 @@ export async function resolveSiteData(
locales: userConfig.locales || {},
langs: createLangDictionary(userConfig),
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 emoji from 'markdown-it-emoji'
import toc from 'markdown-it-toc-done-right'
import { cleanUrlsOptions } from '../../../types/shared'
export type ThemeOptions = Theme | { light: Theme; dark: Theme }
@ -57,8 +56,7 @@ export type { Header }
export const createMarkdownRenderer = async (
srcDir: string,
options: MarkdownOptions = {},
base = '/',
cleanUrls: cleanUrlsOptions = 'off'
base = '/'
): Promise<MarkdownRenderer> => {
const md = MarkdownIt({
html: true,
@ -83,8 +81,7 @@ export const createMarkdownRenderer = async (
rel: 'noopener noreferrer',
...options.externalLinks
},
base,
cleanUrls
base
)
// 3rd party plugins

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

@ -3,13 +3,12 @@ import path from 'path'
import c from 'picocolors'
import matter from 'gray-matter'
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 { deeplyParseHeader } from './utils/parseHeader'
import { getGitTimestamp } from './utils/getGitTimestamp'
import { createMarkdownRenderer, MarkdownOptions } from './markdown/markdown'
import _debug from 'debug'
import { cleanUrlsOptions } from '../../types/shared'
const debug = _debug('vitepress:md')
const cache = new LRUCache<string, MarkdownCompileResult>({ max: 1024 })
@ -30,9 +29,9 @@ export async function createMarkdownToVueRenderFn(
isBuild = false,
base = '/',
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$/, '')))
@ -69,7 +68,7 @@ export async function createMarkdownToVueRenderFn(
md.__path = file
md.__relativePath = relativePath
const html = md.render(content)
const html = md.render(content, { path: file, relativePath, cleanUrls })
const data = md.__data
// validate data.links

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

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

7
types/shared.d.ts vendored

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

Loading…
Cancel
Save