diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index a8740369..76bbe328 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -10,7 +10,7 @@ export default defineConfig({ description: 'Vite & Vue powered static site generator.', lastUpdated: true, - cleanUrls: 'without-subfolders', + cleanUrls: true, head: [['meta', { name: 'theme-color', content: '#3c8772' }]], diff --git a/docs/config/app-configs.md b/docs/config/app-configs.md index de69006c..c8c65775 100644 --- a/docs/config/app-configs.md +++ b/docs/config/app-configs.md @@ -266,29 +266,21 @@ export default { } ``` -## cleanUrls (Experimental) +## cleanUrls -- Type: `'disabled' | 'without-subfolders' | 'with-subfolders'` -- Default: `'disabled'` +- Type: `boolean` +- Default: `false` -Allows removing trailing `.html` from URLs and, optionally, generating clean directory structure. +Allows removing trailing `.html` from URLs. ```ts export default { - cleanUrls: 'with-subfolders' + cleanUrls: true } ``` -This option has several modes you can choose. Here is the list of all modes available. - -| 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 **without a redirect**. +Enabling this may require additional configuration on your hosting platform. For it to work, your server must serve `/foo.html` on requesting `/foo` **without a redirect**. ::: ## rewrites diff --git a/docs/guide/routing.md b/docs/guide/routing.md index 917b4eeb..ccc43d8b 100644 --- a/docs/guide/routing.md +++ b/docs/guide/routing.md @@ -88,26 +88,14 @@ By default, VitePress generates the final static page files by adding `.html` ex └─ index.md ``` -However, you may also generate a clean URL by setting up [`cleanUrls`](/config/app-configs#cleanurls-experimental) option. +However, you may also generate a clean URL by setting up [`cleanUrls`](/config/app-configs#cleanurls) option. ```ts export default { - cleanUrls: 'with-subfolders' + cleanUrls: true } ``` -This option has several modes you can choose. Here is the list of all modes available. The default behavior is `disabled` mode. - -| 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 **without a redirect**. -::: - ## Customize the Mappings You may customize the mapping between directory structure and URL. It's useful when you have complex document structure. For example, let's say you have several packages and would like to place documentations along with the source files like this. diff --git a/src/client/app/router.ts b/src/client/app/router.ts index 8b097254..f0ab3ec3 100644 --- a/src/client/app/router.ts +++ b/src/client/app/router.ts @@ -49,7 +49,7 @@ export function createRouter( async function go(href: string = inBrowser ? location.href : '/') { await router.onBeforeRouteChange?.(href) const url = new URL(href, fakeHost) - if (siteDataRef.value.cleanUrls === 'disabled') { + if (!siteDataRef.value.cleanUrls) { // 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')) { @@ -89,6 +89,18 @@ export function createRouter( if (inBrowser) { nextTick(() => { + let actualPathname = + '/' + + __pageData.relativePath.replace(/(?:(^|\/)index)?\.md$/, '$1') + if (!siteDataRef.value.cleanUrls && !actualPathname.endsWith('/')) { + actualPathname += '.html' + } + if (actualPathname !== targetLoc.pathname) { + targetLoc.pathname = actualPathname + href = actualPathname + targetLoc.search + targetLoc.hash + history.replaceState(null, '', href) + } + if (targetLoc.hash && !scrollPosition) { let target: HTMLElement | null = null try { diff --git a/src/client/theme-default/components/VPAlgoliaSearchBox.vue b/src/client/theme-default/components/VPAlgoliaSearchBox.vue index 63681dbd..00e90b15 100644 --- a/src/client/theme-default/components/VPAlgoliaSearchBox.vue +++ b/src/client/theme-default/components/VPAlgoliaSearchBox.vue @@ -90,7 +90,7 @@ function getRelativePath(absoluteUrl: string) { return ( pathname.replace( /\.html$/, - site.value.cleanUrls === 'disabled' ? '.html' : '' + site.value.cleanUrls ? '' : '.html' ) + hash ) } diff --git a/src/client/theme-default/composables/langs.ts b/src/client/theme-default/composables/langs.ts index d80ccb26..8000e2ab 100644 --- a/src/client/theme-default/composables/langs.ts +++ b/src/client/theme-default/composables/langs.ts @@ -24,7 +24,7 @@ export function useLangs({ value.link || (key === 'root' ? '/' : `/${key}/`), theme.value.i18nRouting !== false && correspondingLink, page.value.relativePath.slice(currentLang.value.link.length - 1), - site.value.cleanUrls === 'disabled' + !site.value.cleanUrls ) } ) diff --git a/src/client/theme-default/support/utils.ts b/src/client/theme-default/support/utils.ts index 2d35f94f..0af969ea 100644 --- a/src/client/theme-default/support/utils.ts +++ b/src/client/theme-default/support/utils.ts @@ -44,7 +44,7 @@ export function normalizeLink(url: string): string { /(?:(^\.+)\/)?.*$/, `$1${pathname.replace( /(\.md)?$/, - site.value.cleanUrls === 'disabled' ? '.html' : '' + site.value.cleanUrls ? '' : '.html' )}${search}${hash}` ) diff --git a/src/node/build/render.ts b/src/node/build/render.ts index 920a31c7..c8dd9d89 100644 --- a/src/node/build/render.ts +++ b/src/node/build/render.ts @@ -173,14 +173,7 @@ export async function renderPage( ${inlinedScript} `.trim() - const createSubDirectory = - config.cleanUrls === 'with-subfolders' && - !/(^|\/)(index|404).md$/.test(page) - - const htmlFileName = path.join( - config.outDir, - page.replace(/\.md$/, createSubDirectory ? '/index.html' : '.html') - ) + const htmlFileName = path.join(config.outDir, page.replace(/\.md$/, '.html')) await fs.ensureDir(path.dirname(htmlFileName)) const transformedHtml = await config.transformHtml?.(html, htmlFileName, { diff --git a/src/node/config.ts b/src/node/config.ts index 3c186ddc..32872bba 100644 --- a/src/node/config.ts +++ b/src/node/config.ts @@ -16,7 +16,6 @@ import type { MarkdownOptions } from './markdown/markdown' import { APPEARANCE_KEY, type Awaitable, - type CleanUrlsMode, type DefaultTheme, type HeadConfig, type LocaleConfig, @@ -77,17 +76,11 @@ export interface UserConfig ignoreDeadLinks?: boolean | 'localhostLinks' /** - * @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 + * Don't force `.html` on URLs. * - * @default 'disabled' + * @default false */ - cleanUrls?: CleanUrlsMode + cleanUrls?: boolean /** * Use web fonts instead of emitting font files to dist. @@ -279,7 +272,7 @@ export async function resolveConfig( shouldPreload: userConfig.shouldPreload, mpa: !!userConfig.mpa, ignoreDeadLinks: userConfig.ignoreDeadLinks, - cleanUrls: userConfig.cleanUrls || 'disabled', + cleanUrls: !!userConfig.cleanUrls, useWebFonts: userConfig.useWebFonts ?? typeof process.versions.webcontainer === 'string', @@ -394,7 +387,7 @@ export async function resolveSiteData( themeConfig: userConfig.themeConfig || {}, locales: userConfig.locales || {}, scrollOffset: userConfig.scrollOffset || 90, - cleanUrls: userConfig.cleanUrls || 'disabled' + cleanUrls: !!userConfig.cleanUrls } } diff --git a/src/node/markdown/env.ts b/src/node/markdown/env.ts index 3dd5fef8..b079c281 100644 --- a/src/node/markdown/env.ts +++ b/src/node/markdown/env.ts @@ -1,5 +1,5 @@ import type { MarkdownSfcBlocks } from '@mdit-vue/plugin-sfc' -import type { CleanUrlsMode, Header } from '../shared' +import type { Header } from '../shared' // Manually declaring all properties as rollup-plugin-dts // is unable to merge augmented module declarations @@ -34,6 +34,6 @@ export interface MarkdownEnv { title?: string path: string relativePath: string - cleanUrls: CleanUrlsMode + cleanUrls: boolean links?: string[] } diff --git a/src/node/markdown/plugins/link.ts b/src/node/markdown/plugins/link.ts index 1582cc65..4688f4c0 100644 --- a/src/node/markdown/plugins/link.ts +++ b/src/node/markdown/plugins/link.ts @@ -68,14 +68,11 @@ export const linkPlugin = ( let cleanUrl = url.replace(/[?#].*$/, '') // transform foo.md -> foo[.html] if (cleanUrl.endsWith('.md')) { - cleanUrl = cleanUrl.replace( - /\.md$/, - env.cleanUrls === 'disabled' ? '.html' : '' - ) + cleanUrl = cleanUrl.replace(/\.md$/, env.cleanUrls ? '' : '.html') } // transform ./foo -> ./foo[.html] if ( - env.cleanUrls === 'disabled' && + !env.cleanUrls && !cleanUrl.endsWith('.html') && !cleanUrl.endsWith('/') ) { diff --git a/src/node/markdownToVue.ts b/src/node/markdownToVue.ts index 2cc002dc..a5e9a2f1 100644 --- a/src/node/markdownToVue.ts +++ b/src/node/markdownToVue.ts @@ -4,12 +4,7 @@ import c from 'picocolors' import LRUCache from 'lru-cache' import { resolveTitleFromToken } from '@mdit-vue/shared' import type { SiteConfig } from './config' -import { - type PageData, - type HeadConfig, - EXTERNAL_URL_RE, - type CleanUrlsMode -} from './shared' +import { type PageData, type HeadConfig, EXTERNAL_URL_RE } from './shared' import { slash } from './utils/slash' import { getGitTimestamp } from './utils/getGitTimestamp' import { @@ -43,7 +38,7 @@ export async function createMarkdownToVueRenderFn( isBuild = false, base = '/', includeLastUpdatedData = false, - cleanUrls: CleanUrlsMode = 'disabled', + cleanUrls = false, siteConfig: SiteConfig | null = null ) { const md = await createMarkdownRenderer(srcDir, options, base) diff --git a/src/shared/shared.ts b/src/shared/shared.ts index 653e0a12..a2a43d82 100644 --- a/src/shared/shared.ts +++ b/src/shared/shared.ts @@ -2,7 +2,6 @@ import type { HeadConfig, PageData, SiteData } from '../../types/shared.js' export type { Awaitable, - CleanUrlsMode, DefaultTheme, HeadConfig, Header, diff --git a/types/shared.d.ts b/types/shared.d.ts index 4429b041..8bacd813 100644 --- a/types/shared.d.ts +++ b/types/shared.d.ts @@ -43,14 +43,9 @@ export interface Header { children: Header[] } -export type CleanUrlsMode = - | 'disabled' - | 'without-subfolders' - | 'with-subfolders' - export interface SiteData { base: string - cleanUrls?: CleanUrlsMode + cleanUrls?: boolean lang: string dir: string title: string