From 56229e4efed6379dcf73209bd14d50c5ea9f3dc2 Mon Sep 17 00:00:00 2001 From: Divyansh Singh <40380293+brc-dd@users.noreply.github.com> Date: Thu, 7 Jul 2022 19:17:40 +0530 Subject: [PATCH] feat/clean-urls (#929) * clean urls * Better comment for option Co-authored-by: Divyansh Singh <40380293+brc-dd@users.noreply.github.com> * Remove typing because implicit Co-authored-by: Divyansh Singh <40380293+brc-dd@users.noreply.github.com> * suggestion to simplify check * clean urls in next/previous and sidebar * accept suggestion Co-authored-by: Divyansh Singh <40380293+brc-dd@users.noreply.github.com> * accept suggestion Co-authored-by: Divyansh Singh <40380293+brc-dd@users.noreply.github.com> * documentation of option * nicer without trailing slash * trailing slash option * Fixing trailing slash setup * new options + improved documentation * feat: add without subfolders option for clean urls * fix: remove .html from algolia results * chore: update deps Co-authored-by: Georges Gomes --- docs/.vitepress/config.ts | 1 + docs/config/app-configs.md | 26 ++++++- docs/guide/migration-from-vitepress-0.md | 2 +- docs/guide/migration-from-vuepress.md | 4 +- docs/guide/theme-nav.md | 2 +- package.json | 8 +-- pnpm-lock.yaml | 72 +++++++++---------- src/client/app/router.ts | 13 ++-- .../components/VPAlgoliaSearchBox.vue | 9 ++- src/client/theme-default/support/utils.ts | 8 ++- src/node/build/render.ts | 10 ++- src/node/config.ts | 25 +++++-- src/node/markdown/plugins/link.ts | 24 +++++-- src/node/markdownToVue.ts | 7 +- src/node/plugin.ts | 7 +- src/shared/shared.ts | 3 +- types/shared.d.ts | 6 ++ 17 files changed, 155 insertions(+), 72 deletions(-) diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 44af484c..634faf87 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -8,6 +8,7 @@ export default defineConfig({ description: 'Vite & Vue powered static site generator.', lastUpdated: true, + cleanUrls: 'without-subfolders', themeConfig: { nav: nav(), diff --git a/docs/config/app-configs.md b/docs/config/app-configs.md index 54657e3f..816fdfd2 100644 --- a/docs/config/app-configs.md +++ b/docs/config/app-configs.md @@ -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,3 +173,27 @@ export default { } ``` +## cleanUrls (Experimental) + +- Type: `'disabled' | 'without-subfolders' | 'with-subfolders'` +- Default: `'disabled'` + +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' +} +``` diff --git a/docs/guide/migration-from-vitepress-0.md b/docs/guide/migration-from-vitepress-0.md index f7041dc8..ff5cf3df 100644 --- a/docs/guide/migration-from-vitepress-0.md +++ b/docs/guide/migration-from-vitepress-0.md @@ -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`. diff --git a/docs/guide/migration-from-vuepress.md b/docs/guide/migration-from-vuepress.md index c18c754f..e646ae54 100644 --- a/docs/guide/migration-from-vuepress.md +++ b/docs/guide/migration-from-vuepress.md @@ -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 `` regex to find and replace it with `![$2]($1)` to replace all the images with `![](...)` syntax. diff --git a/docs/guide/theme-nav.md b/docs/guide/theme-nav.md index 81575e9b..d361d3a0 100644 --- a/docs/guide/theme-nav.md +++ b/docs/guide/theme-nav.md @@ -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 { diff --git a/package.json b/package.json index 90fbf0a2..8904c493 100644 --- a/package.json +++ b/package.json @@ -75,7 +75,7 @@ "@docsearch/js": "^3.1.1", "@vitejs/plugin-vue": "^3.0.0-beta.1", "@vue/devtools-api": "^6.2.0", - "@vueuse/core": "^8.7.5", + "@vueuse/core": "^8.9.0", "body-scroll-lock": "^4.0.0-beta.0", "shiki": "^0.10.1", "vite": "^3.0.0-beta.7", @@ -97,7 +97,7 @@ "@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", @@ -138,8 +138,8 @@ "sirv": "^2.0.2", "supports-color": "^9.2.2", "typescript": "^4.7.4", - "vitest": "^0.17.0", - "vue-tsc": "^0.38.2" + "vitest": "^0.17.1", + "vue-tsc": "^0.38.3" }, "pnpm": { "peerDependencyRules": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 512cbfff..d528f275 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -21,12 +21,12 @@ 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 '@vitejs/plugin-vue': ^3.0.0-beta.1 '@vue/devtools-api': ^6.2.0 - '@vueuse/core': ^8.7.5 + '@vueuse/core': ^8.9.0 body-scroll-lock: ^4.0.0-beta.0 chokidar: ^3.5.3 compression: ^1.7.4 @@ -68,9 +68,9 @@ importers: supports-color: ^9.2.2 typescript: ^4.7.4 vite: ^3.0.0-beta.7 - vitest: ^0.17.0 + vitest: ^0.17.1 vue: ^3.2.37 - vue-tsc: ^0.38.2 + vue-tsc: ^0.38.3 dependencies: '@docsearch/css': 3.1.1 '@docsearch/js': 3.1.1 @@ -138,8 +138,8 @@ importers: sirv: 2.0.2 supports-color: 9.2.2 typescript: 4.7.4 - vitest: 0.17.0_supports-color@9.2.2 - vue-tsc: 0.38.2_typescript@4.7.4 + vitest: 0.17.1_supports-color@9.2.2 + vue-tsc: 0.38.3_typescript@4.7.4 docs: specifiers: {} @@ -312,7 +312,7 @@ packages: resolution: {integrity: sha512-bt7l2aKRoSnLUuX+s4LVQ1a7AF2c9myiZNv5uvQCePG5tpvVGpwrnMwqVXOUJn9q6FwVVhOrQMO/t+QmnnAEUw==} dependencies: '@docsearch/react': 3.1.1 - preact: 10.8.2 + preact: 10.9.0 transitivePeerDependencies: - '@algolia/client-search' - '@types/react' @@ -695,32 +695,32 @@ packages: vue: 3.2.37 dev: false - /@volar/code-gen/0.38.2: - resolution: {integrity: sha512-H81I6d7rZB7teqL+zhK/Xz1v0/kKkUwkB0Aq6b4+BTCqcJeiZkoWxd0gFhrhWTnUoqiM83lhoTGo2vkvx5YagQ==} + /@volar/code-gen/0.38.3: + resolution: {integrity: sha512-0yCkDtaxffyfC9e2dSLGXJmG3b0rCfTa6vqxjr70ZFTtcf/VytmMBwboFicnm+Zoen9EI8wUNfw4upw9Slz5RQ==} dependencies: - '@volar/source-map': 0.38.2 + '@volar/source-map': 0.38.3 dev: true - /@volar/source-map/0.38.2: - resolution: {integrity: sha512-DWcYbYt9SPwk0r4VmXk1F0v4X5+hCqH1JRkAWSeJymQyXCQ2OQDEbY2PF12a7y2qn4FUBD2gOba2TynAqI8ZFQ==} + /@volar/source-map/0.38.3: + resolution: {integrity: sha512-8aVM+r4lsHnLjhvnjQ6kn4J++3I6VXtJblcGzWuIOn9M8pJmRGW6Si/eOVjayLWfvPCxXUM7e3sg4Nm2tufTmg==} dev: true - /@volar/vue-code-gen/0.38.2: - resolution: {integrity: sha512-whLunD6phSGWBUHZKdTxeglrpzQu26ii8CRVapFdjfyMaVhQ7ESNeIAhkTVyg2ovOPc0PiDYPQEPzfWAADIWog==} + /@volar/vue-code-gen/0.38.3: + resolution: {integrity: sha512-euVuKtwV/KurRSVwNz5bZbCBJLwVOE56+Uh2PhsHcAM5Wzlt82cwLj07FbFagCftoC3IC/bsn43yuLc2I+ZjAQ==} dependencies: - '@volar/code-gen': 0.38.2 - '@volar/source-map': 0.38.2 + '@volar/code-gen': 0.38.3 + '@volar/source-map': 0.38.3 '@vue/compiler-core': 3.2.37 '@vue/compiler-dom': 3.2.37 '@vue/shared': 3.2.37 dev: true - /@volar/vue-typescript/0.38.2: - resolution: {integrity: sha512-5IKvSK2m5yUmH6iu/tNScVlvJGuiHawTfSmjxaMs+/tod25WeK37LEdf+pdKtlJ30bYTQmmkAuEfG01QvvBRGQ==} + /@volar/vue-typescript/0.38.3: + resolution: {integrity: sha512-rXh4RQBZrNfkiSnpBYbHrsxg7vBbZeYsGFgE/n8FVLcZfGlelsdXFIINsr/aZGUCJre9I15wQ44eEmXnc4+qww==} dependencies: - '@volar/code-gen': 0.38.2 - '@volar/source-map': 0.38.2 - '@volar/vue-code-gen': 0.38.2 + '@volar/code-gen': 0.38.3 + '@volar/source-map': 0.38.3 + '@volar/vue-code-gen': 0.38.3 '@vue/compiler-sfc': 3.2.37 '@vue/reactivity': 3.2.37 dev: true @@ -805,8 +805,8 @@ packages: /@vue/shared/3.2.37: resolution: {integrity: sha512-4rSJemR2NQIo9Klm1vabqWjD8rs/ZaJSzMxkMNeJS6lHiUjjUeYFbooN19NgFjztubEKh3WlZUeOLVdbbUWHsw==} - /@vueuse/core/8.8.0_vue@3.2.37: - resolution: {integrity: sha512-TyvcNuA6O9WGkT8oQB4ERt8aBxe/e0fUs3SnibaxtLOr4eVXq42m3sLZgwgWOrJi4s9/8pTsMaJNn/6BUefwpQ==} + /@vueuse/core/8.9.0_vue@3.2.37: + resolution: {integrity: sha512-eKWehF6gsiLYxnYM/1xgDu16bKED7AWvkk56JIFNQes8OKgktr3Jc1wUy8UWIulrnwCXICUu9YUo+Wkq4r2JNw==} 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.8.0 - '@vueuse/shared': 8.8.0_vue@3.2.37 + '@vueuse/metadata': 8.9.0 + '@vueuse/shared': 8.9.0_vue@3.2.37 vue: 3.2.37 vue-demi: 0.13.2_vue@3.2.37 dev: false - /@vueuse/metadata/8.8.0: - resolution: {integrity: sha512-bRF+QPrw/RtP0al3nT/DtJ7CN0a6y6tEEO6hQ4CuJcGuUqd15eCOF6WKqQnC5DRaGFhsq/YwnQYsLTdJsW8f1A==} + /@vueuse/metadata/8.9.0: + resolution: {integrity: sha512-pjkIbQgJPRUrxK5/iXVKQFGC+OhJ+Vd6fhBsdwgj+NNJEHUotRliYymwdvhnEke/o+kkulT0xMvoK19nyPoiMw==} dev: false - /@vueuse/shared/8.8.0_vue@3.2.37: - resolution: {integrity: sha512-DNZEs5Wy8hxxjAyWni6UK4BX/OGa8R7g0GX1tid5+AvmRbUwvUXL+0lVmGEuWPSQY4OZdYef1lvuFCi4Bfd59A==} + /@vueuse/shared/8.9.0_vue@3.2.37: + resolution: {integrity: sha512-Pmu3Fopk/JJjN8b90uQuFrVCc/RPcSA/0zDFRTyn3YIhoB5ESna/1Sac5WZxK+n82g/ERXHHQTetGI9yxEdPfA==} peerDependencies: '@vue/composition-api': ^1.1.0 vue: ^2.6.0 || ^3.2.0 @@ -2890,8 +2890,8 @@ packages: picocolors: 1.0.0 source-map-js: 1.0.2 - /preact/10.8.2: - resolution: {integrity: sha512-AKGt0BsDSiAYzVS78jZ9qRwuorY2CoSZtf1iOC6gLb/3QyZt+fLT09aYJBjRc/BEcRc4j+j3ggERMdNE43i1LQ==} + /preact/10.9.0: + resolution: {integrity: sha512-jO6/OvCRL+OT8gst/+Q2ir7dMybZAX8ioP02Zmzh3BkQMHLyqZSujvxbUriXvHi8qmhcHKC2Gwbog6Kt+YTh+Q==} dev: false /prettier/2.7.1: @@ -3577,8 +3577,8 @@ packages: optionalDependencies: fsevents: 2.3.2 - /vitest/0.17.0_supports-color@9.2.2: - resolution: {integrity: sha512-5YO9ubHo0Zg35mea3+zZAr4sCku32C3usvIH5COeJB48TZV/R0J9aGNtGOOqEWZYfOKP0pGZUvTokne3x/QEFg==} + /vitest/0.17.1_supports-color@9.2.2: + resolution: {integrity: sha512-d6NsFC6FPmZ5XdiSYfW5rwJ/b8060wqe2steNNlVbhO69HWma6CucIm5g7PXlCSkmKvrdEbUsZHPAarlH83VGw==} engines: {node: '>=v14.16.0'} hasBin: true peerDependencies: @@ -3639,13 +3639,13 @@ packages: vue: 3.2.37 dev: false - /vue-tsc/0.38.2_typescript@4.7.4: - resolution: {integrity: sha512-+OMmpw9BZC9khul3I1HGtWchv7BCiaM7NvfdilVAiOFkjnivIoaW6jJm6YPQJaEPouePtpkDUWovyzgNxWdDsw==} + /vue-tsc/0.38.3_typescript@4.7.4: + resolution: {integrity: sha512-mWlneSF+PG2kXYGJI12N4XEAG4ljAkae7IcB93fspqSkEt/oKwDbWy3DzcPSgUm0LsXqOUprTMaZkwDVSRBIvw==} hasBin: true peerDependencies: typescript: '*' dependencies: - '@volar/vue-typescript': 0.38.2 + '@volar/vue-typescript': 0.38.3 typescript: 4.7.4 dev: true diff --git a/src/client/app/router.ts b/src/client/app/router.ts index 40d91146..cecdb198 100644 --- a/src/client/app/router.ts +++ b/src/client/app/router.ts @@ -40,11 +40,14 @@ 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 (!url.pathname.endsWith('/') && !url.pathname.endsWith('.html')) { - url.pathname += '.html' - href = url.pathname + url.search + url.hash + 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 @@ -96,7 +99,7 @@ export function createRouter( } } } catch (err: any) { - if (!err.message.match(/fetch/) && !href.match(/^[\\/]404\.html$/)) { + if (!/fetch/.test(err.message) && !/^\/404(\.html|\/)?$/.test(href)) { console.error(err) } diff --git a/src/client/theme-default/components/VPAlgoliaSearchBox.vue b/src/client/theme-default/components/VPAlgoliaSearchBox.vue index a2eb314d..2251704c 100644 --- a/src/client/theme-default/components/VPAlgoliaSearchBox.vue +++ b/src/client/theme-default/components/VPAlgoliaSearchBox.vue @@ -6,7 +6,7 @@ import { useRouter, useRoute, useData } from 'vitepress' const router = useRouter() const route = useRoute() -const { theme } = useData() +const { theme, site } = useData() onMounted(() => { initialize(theme.value.algolia) @@ -120,7 +120,12 @@ function isSpecialClick(event: MouseEvent) { function getRelativePath(absoluteUrl: string) { const { pathname, hash } = new URL(absoluteUrl) - return pathname + hash + return ( + pathname.replace( + /\.html$/, + site.value.cleanUrls !== 'disabled' ? '' : '.html' + ) + hash + ) } diff --git a/src/client/theme-default/support/utils.ts b/src/client/theme-default/support/utils.ts index be2a39c8..4b55dae9 100644 --- a/src/client/theme-default/support/utils.ts +++ b/src/client/theme-default/support/utils.ts @@ -1,5 +1,5 @@ import { ref } from 'vue' -import { withBase } from 'vitepress' +import { withBase, useData } from 'vitepress' export const HASH_RE = /#.*$/ export const EXT_RE = /(index)?\.(md|html)$/ @@ -74,12 +74,16 @@ export function normalizeLink(url: string): string { return url } + const { site } = useData() const { pathname, search, hash } = new URL(url, 'http://example.com') const normalizedPath = pathname.endsWith('/') || pathname.endsWith('.html') ? url - : `${pathname.replace(/(\.md)?$/, '.html')}${search}${hash}` + : `${pathname.replace( + /(\.md)?$/, + site.value.cleanUrls === 'disabled' ? '.html' : '' + )}${search}${hash}` return withBase(normalizedPath) } diff --git a/src/node/build/render.ts b/src/node/build/render.ts index 1827a7e1..01f326f8 100644 --- a/src/node/build/render.ts +++ b/src/node/build/render.ts @@ -150,7 +150,15 @@ export async function renderPage( ${inlinedScript} `.trim() - const htmlFileName = path.join(config.outDir, page.replace(/\.md$/, '.html')) + const createSubDirectory = + config.cleanUrls === 'with-subfolders' && + !/(^|\/)(index|404).md$/.test(page) + + const htmlFileName = path.join( + config.outDir, + page.replace(/\.md$/, createSubDirectory ? '/index.html' : '.html') + ) + await fs.ensureDir(path.dirname(htmlFileName)) await fs.writeFile(htmlFileName, html) } diff --git a/src/node/config.ts b/src/node/config.ts index b0d2074c..f5999ab0 100644 --- a/src/node/config.ts +++ b/src/node/config.ts @@ -16,7 +16,8 @@ import { LocaleConfig, DefaultTheme, APPEARANCE_KEY, - createLangDictionary + createLangDictionary, + CleanUrlsMode } from './shared' import { resolveAliases, DEFAULT_THEME_PATH } from './alias' import { MarkdownOptions } from './markdown/markdown' @@ -60,7 +61,7 @@ export interface UserConfig { scrollOffset?: number | string /** - * Enable MPA / zero-JS mode + * Enable MPA / zero-JS mode. * @experimental */ mpa?: boolean @@ -71,6 +72,19 @@ export interface UserConfig { * @default false */ ignoreDeadLinks?: boolean + + /** + * @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?: CleanUrlsMode } export type RawConfigExports = @@ -88,6 +102,7 @@ export interface SiteConfig | 'mpa' | 'lastUpdated' | 'ignoreDeadLinks' + | 'cleanUrls' > { root: string srcDir: string @@ -166,7 +181,8 @@ export async function resolveConfig( vite: userConfig.vite, shouldPreload: userConfig.shouldPreload, mpa: !!userConfig.mpa, - ignoreDeadLinks: userConfig.ignoreDeadLinks + ignoreDeadLinks: userConfig.ignoreDeadLinks, + cleanUrls: userConfig.cleanUrls || 'disabled' } return config @@ -270,7 +286,8 @@ export async function resolveSiteData( themeConfig: userConfig.themeConfig || {}, locales: userConfig.locales || {}, langs: createLangDictionary(userConfig), - scrollOffset: userConfig.scrollOffset || 90 + scrollOffset: userConfig.scrollOffset || 90, + cleanUrls: userConfig.cleanUrls || 'disabled' } } diff --git a/src/node/markdown/plugins/link.ts b/src/node/markdown/plugins/link.ts index e88e425c..8bc4ca2e 100644 --- a/src/node/markdown/plugins/link.ts +++ b/src/node/markdown/plugins/link.ts @@ -5,7 +5,7 @@ import MarkdownIt from 'markdown-it' import { MarkdownRenderer } from '../markdown' import { URL } from 'url' -import { EXTERNAL_URL_RE } from '../../shared' +import { EXTERNAL_URL_RE, CleanUrlsMode } from '../../shared' const indexRE = /(^|.*\/)index.md(#?.*)$/i @@ -37,7 +37,7 @@ export const linkPlugin = ( // links to files (other than html/md) !/\.(?!html|md)\w+($|\?)/i.test(url) ) { - normalizeHref(hrefAttr) + normalizeHref(hrefAttr, env.cleanUrl) } // encode vite-specific replace strings in case they appear in URLs @@ -50,7 +50,10 @@ export const linkPlugin = ( return self.renderToken(tokens, idx, options) } - function normalizeHref(hrefAttr: [string, string]) { + function normalizeHref( + hrefAttr: [string, string], + shouldCleanUrls: CleanUrlsMode + ) { let url = hrefAttr[1] const indexMatch = url.match(indexRE) @@ -59,12 +62,19 @@ export const linkPlugin = ( url = path + hash } else { let cleanUrl = url.replace(/[?#].*$/, '') - // .md -> .html + // transform foo.md -> foo[.html] if (cleanUrl.endsWith('.md')) { - cleanUrl = cleanUrl.replace(/\.md$/, '.html') + cleanUrl = cleanUrl.replace( + /\.md$/, + shouldCleanUrls === 'disabled' ? '.html' : '' + ) } - // ./foo -> ./foo.html - if (!cleanUrl.endsWith('.html') && !cleanUrl.endsWith('/')) { + // transform ./foo -> ./foo[.html] + if ( + shouldCleanUrls === 'disabled' && + !cleanUrl.endsWith('.html') && + !cleanUrl.endsWith('/') + ) { cleanUrl += '.html' } const parsed = new URL(url, 'http://a.com') diff --git a/src/node/markdownToVue.ts b/src/node/markdownToVue.ts index 86a07750..c8272748 100644 --- a/src/node/markdownToVue.ts +++ b/src/node/markdownToVue.ts @@ -3,7 +3,7 @@ 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' @@ -28,7 +28,8 @@ export async function createMarkdownToVueRenderFn( userDefines: Record | undefined, isBuild = false, base = '/', - includeLastUpdatedData = false + includeLastUpdatedData = false, + cleanUrls: CleanUrlsMode = 'disabled' ) { const md = await createMarkdownRenderer(srcDir, options, base) @@ -67,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 diff --git a/src/node/plugin.ts b/src/node/plugin.ts index 7a2cf80d..f9c38be1 100644 --- a/src/node/plugin.ts +++ b/src/node/plugin.ts @@ -45,7 +45,9 @@ export async function createVitePressPlugin( vue: userVuePluginOptions, vite: userViteConfig, pages, - ignoreDeadLinks + ignoreDeadLinks, + lastUpdated, + cleanUrls } = siteConfig let markdownToVue: Awaited> @@ -83,7 +85,8 @@ export async function createVitePressPlugin( config.define, config.command === 'build', config.base, - siteConfig.lastUpdated + lastUpdated, + cleanUrls ) }, diff --git a/src/shared/shared.ts b/src/shared/shared.ts index a47b033f..fa714b47 100644 --- a/src/shared/shared.ts +++ b/src/shared/shared.ts @@ -7,7 +7,8 @@ export type { LocaleConfig, Header, DefaultTheme, - PageDataPayload + PageDataPayload, + CleanUrlsMode } from '../../types/shared' export const EXTERNAL_URL_RE = /^https?:/i diff --git a/types/shared.d.ts b/types/shared.d.ts index 6dbbdf4f..7890e82e 100644 --- a/types/shared.d.ts +++ b/types/shared.d.ts @@ -18,8 +18,14 @@ export interface Header { slug: string } +export type CleanUrlsMode = + | 'disabled' + | 'without-subfolders' + | 'with-subfolders' + export interface SiteData { base: string + cleanUrls?: CleanUrlsMode /** * Language of the site as it should be set on the `html` element.