diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 94315e62..e62d3c97 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -6,6 +6,7 @@ export default defineConfig({ description: 'Vite & Vue powered static site generator.', lastUpdated: true, + cleanUrls: true, themeConfig: { nav: nav(), diff --git a/src/client/app/router.ts b/src/client/app/router.ts index 39418559..450e009e 100644 --- a/src/client/app/router.ts +++ b/src/client/app/router.ts @@ -42,9 +42,14 @@ export function createRouter( 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 + // ensure correct deep link so page refresh lands on correct files. + if (siteDataRef.value.cleanUrls) { + // Should we replace `/foo.html` -> `/foo` ? :thinking: + } else { + 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 @@ -101,7 +106,12 @@ export function createRouter( } } } catch (err: any) { - if (!err.message.match(/fetch/) && !href.match(/^[\\/]404\.html$/)) { + if ( + !err.message.match(/fetch/) && + (siteDataRef.value.cleanUrls + ? !href.match(/^[\\/]404$/) + : !href.match(/^[\\/]404\.html$/)) + ) { console.error(err) } diff --git a/src/node/build/render.ts b/src/node/build/render.ts index c4330e3e..41e326da 100644 --- a/src/node/build/render.ts +++ b/src/node/build/render.ts @@ -165,11 +165,22 @@ export async function renderPage( ${inlinedScript} `.trim() - const htmlFileName = path.join(config.outDir, page.replace(/\.md$/, '.html')) + const htmlFileName = path.join( + config.outDir, + transformHTMLFileName(page, config.cleanUrls) + ) await fs.ensureDir(path.dirname(htmlFileName)) await fs.writeFile(htmlFileName, html) } +function transformHTMLFileName(page: string, shouldCleanUrls: boolean): string { + if (page === 'index.md' || page.endsWith('/index.md') || page === '404.md') { + return page.replace(/\.md$/, '.html') + } + + return page.replace(/\.md$/, shouldCleanUrls ? '/index.html' : '.html') +} + function resolvePageImports( config: SiteConfig, page: string, diff --git a/src/node/config.ts b/src/node/config.ts index fca1df47..5a6c9328 100644 --- a/src/node/config.ts +++ b/src/node/config.ts @@ -71,6 +71,14 @@ export interface UserConfig { * @default false */ ignoreDeadLinks?: boolean + + /** + * Always use "clean URLs" without the `.html`. + * Also generate static files as `foo/index.html` insted of `foo.html`. + * Works with MPA config too. + * (default: false) + */ + cleanUrls?: boolean } export type RawConfigExports = @@ -98,6 +106,7 @@ export interface SiteConfig tempDir: string alias: AliasOptions pages: string[] + cleanUrls: boolean } const resolve = (root: string, file: string) => @@ -166,7 +175,8 @@ export async function resolveConfig( vite: userConfig.vite, shouldPreload: userConfig.shouldPreload, mpa: !!userConfig.mpa, - ignoreDeadLinks: userConfig.ignoreDeadLinks + ignoreDeadLinks: userConfig.ignoreDeadLinks, + cleanUrls: !!userConfig.cleanUrls } return config @@ -270,7 +280,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 || false } } diff --git a/src/node/markdown/markdown.ts b/src/node/markdown/markdown.ts index 77276c62..5dc2b6ba 100644 --- a/src/node/markdown/markdown.ts +++ b/src/node/markdown/markdown.ts @@ -56,7 +56,8 @@ export type { Header } export const createMarkdownRenderer = async ( srcDir: string, options: MarkdownOptions = {}, - base = '/' + base = '/', + cleanUrls: boolean = false ): Promise => { const md = MarkdownIt({ html: true, @@ -81,7 +82,8 @@ export const createMarkdownRenderer = async ( rel: 'noopener noreferrer', ...options.externalLinks }, - base + base, + cleanUrls ) // 3rd party plugins diff --git a/src/node/markdown/plugins/link.ts b/src/node/markdown/plugins/link.ts index e88e425c..452dbe7c 100644 --- a/src/node/markdown/plugins/link.ts +++ b/src/node/markdown/plugins/link.ts @@ -12,7 +12,8 @@ const indexRE = /(^|.*\/)index.md(#?.*)$/i export const linkPlugin = ( md: MarkdownIt, externalAttrs: Record, - base: string + base: string, + shouldCleanUrls: boolean ) => { md.renderer.rules.link_open = (tokens, idx, options, env, self) => { const token = tokens[idx] @@ -37,7 +38,7 @@ export const linkPlugin = ( // links to files (other than html/md) !/\.(?!html|md)\w+($|\?)/i.test(url) ) { - normalizeHref(hrefAttr) + normalizeHref(hrefAttr, shouldCleanUrls) } // encode vite-specific replace strings in case they appear in URLs @@ -50,7 +51,7 @@ export const linkPlugin = ( return self.renderToken(tokens, idx, options) } - function normalizeHref(hrefAttr: [string, string]) { + function normalizeHref(hrefAttr: [string, string], shouldCleanUrls: boolean) { let url = hrefAttr[1] const indexMatch = url.match(indexRE) @@ -58,13 +59,17 @@ export const linkPlugin = ( const [, path, hash] = indexMatch url = path + hash } else { - let cleanUrl = url.replace(/[?#].*$/, '') - // .md -> .html + let cleanUrl = url.replace(/[?#].*$/, '').replace(/\?.*$/, '') + // transform foo.md -> foo[.html] if (cleanUrl.endsWith('.md')) { - cleanUrl = cleanUrl.replace(/\.md$/, '.html') + cleanUrl = cleanUrl.replace(/\.md$/, shouldCleanUrls ? '' : '.html') } - // ./foo -> ./foo.html - if (!cleanUrl.endsWith('.html') && !cleanUrl.endsWith('/')) { + // transform ./foo -> ./foo[.html] + if ( + !shouldCleanUrls && + !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 8959cce2..d3a2e3e8 100644 --- a/src/node/markdownToVue.ts +++ b/src/node/markdownToVue.ts @@ -28,9 +28,10 @@ export async function createMarkdownToVueRenderFn( userDefines: Record | undefined, isBuild = false, base = '/', - includeLastUpdatedData = false + includeLastUpdatedData = false, + cleanUrls: boolean = false ) { - const md = await createMarkdownRenderer(srcDir, options, base) + const md = await createMarkdownRenderer(srcDir, options, base, cleanUrls) pages = pages.map((p) => slash(p.replace(/\.md$/, ''))) diff --git a/src/node/plugin.ts b/src/node/plugin.ts index a5fb7cce..33e0468a 100644 --- a/src/node/plugin.ts +++ b/src/node/plugin.ts @@ -45,7 +45,8 @@ export async function createVitePressPlugin( vue: userVuePluginOptions, vite: userViteConfig, pages, - ignoreDeadLinks + ignoreDeadLinks, + cleanUrls } = siteConfig let markdownToVue: Awaited> @@ -83,7 +84,8 @@ export async function createVitePressPlugin( config.define, config.command === 'build', config.base, - siteConfig.lastUpdated + siteConfig.lastUpdated, + cleanUrls ) }, diff --git a/types/shared.d.ts b/types/shared.d.ts index 6dbbdf4f..3ff68c91 100644 --- a/types/shared.d.ts +++ b/types/shared.d.ts @@ -57,6 +57,7 @@ export interface SiteData { label: string } > + cleanUrls: boolean } export type HeadConfig =