feat(cleanUrls): Generate "clean urls" in SPA and MPA mode

pull/487/head
Georges Gomes 4 years ago
parent 51978a3ff5
commit 670498804e

@ -1,3 +1,4 @@
export default { export default {
lang: 'en-US', lang: 'en-US',
title: 'VitePress', title: 'VitePress',

@ -2,6 +2,7 @@ import { reactive, inject, markRaw, nextTick, readonly } from 'vue'
import type { Component, InjectionKey } from 'vue' import type { Component, InjectionKey } from 'vue'
import { PageData } from '../shared' import { PageData } from '../shared'
import { inBrowser } from './utils' import { inBrowser } from './utils'
import { siteDataRef } from './data'
export interface Route { export interface Route {
path: string path: string
@ -42,10 +43,15 @@ export function createRouter(
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. // ensure correct deep link so page refresh lands on correct files.
const url = new URL(href, fakeHost) if (siteDataRef.value.cleanUrls) {
if (!url.pathname.endsWith('/') && !url.pathname.endsWith('.html')) { // TODO
url.pathname += '.html' }
href = url.pathname + url.search + url.hash else {
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 (inBrowser) { if (inBrowser) {
// save scroll position before changing url // save scroll position before changing url

@ -138,11 +138,19 @@ export async function renderPage(
${inlinedScript} ${inlinedScript}
</body> </body>
</html>`.trim() </html>`.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.ensureDir(path.dirname(htmlFileName))
await fs.writeFile(htmlFileName, html) await fs.writeFile(htmlFileName, html)
} }
function transformHTMLFileName(page: string, shouldCleanUrls: boolean): string {
if (page === 'index.md' || page.endsWith('/index.md')) {
return page.replace(/\.md$/, '.html');
}
return page.replace(/\.md$/, shouldCleanUrls ? '/index.html' : '.html');
}
function resolvePageImports( function resolvePageImports(
config: SiteConfig, config: SiteConfig,
page: string, page: string,

@ -54,6 +54,14 @@ export interface UserConfig<ThemeConfig = any> {
* @experimental * @experimental
*/ */
mpa?: boolean mpa?: boolean
/**
* Always use "clean URLs" without the `.html`.
* Also generate static files as `foo/index.html` insted of `foo.html`.
* (default: false)
* @experimental (Works better with mpa mode)
*/
cleanUrls?: boolean
} }
export type RawConfigExports = export type RawConfigExports =
@ -73,6 +81,7 @@ export interface SiteConfig<ThemeConfig = any>
themeDir: string themeDir: string
outDir: string outDir: string
tempDir: string tempDir: string
cleanUrls: boolean;
alias: AliasOptions alias: AliasOptions
pages: string[] pages: string[]
} }
@ -124,6 +133,7 @@ export async function resolveConfig(
configPath, configPath,
outDir: resolve(root, 'dist'), outDir: resolve(root, 'dist'),
tempDir: path.resolve(APP_PATH, 'temp'), tempDir: path.resolve(APP_PATH, 'temp'),
cleanUrls: !!userConfig.cleanUrls,
markdown: userConfig.markdown, markdown: userConfig.markdown,
alias: resolveAliases(themeDir), alias: resolveAliases(themeDir),
vue: userConfig.vue, vue: userConfig.vue,
@ -229,6 +239,7 @@ export async function resolveSiteData(
head: userConfig.head || [], head: userConfig.head || [],
themeConfig: userConfig.themeConfig || {}, themeConfig: userConfig.themeConfig || {},
locales: userConfig.locales || {}, locales: userConfig.locales || {},
langs: createLangDictionary(userConfig) langs: createLangDictionary(userConfig),
cleanUrls: userConfig.cleanUrls || false
} }
} }

@ -16,6 +16,7 @@ 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-table-of-contents' import toc from 'markdown-it-table-of-contents'
import { SiteConfig } from 'config'
export interface MarkdownOptions extends MarkdownIt.Options { export interface MarkdownOptions extends MarkdownIt.Options {
lineNumbers?: boolean lineNumbers?: boolean
@ -47,7 +48,7 @@ export interface MarkdownRenderer {
export type { Header } export type { Header }
export const createMarkdownRenderer = ( export const createMarkdownRenderer = (
srcDir: string, siteConfig: SiteConfig,
options: MarkdownOptions = {} options: MarkdownOptions = {}
): MarkdownRenderer => { ): MarkdownRenderer => {
const md = MarkdownIt({ const md = MarkdownIt({
@ -61,7 +62,7 @@ export const createMarkdownRenderer = (
md.use(componentPlugin) md.use(componentPlugin)
.use(highlightLinePlugin) .use(highlightLinePlugin)
.use(preWrapperPlugin) .use(preWrapperPlugin)
.use(snippetPlugin, srcDir) .use(snippetPlugin, siteConfig.srcDir)
.use(hoistPlugin) .use(hoistPlugin)
.use(containerPlugin) .use(containerPlugin)
.use(extractHeaderPlugin) .use(extractHeaderPlugin)
@ -69,7 +70,7 @@ export const createMarkdownRenderer = (
target: '_blank', target: '_blank',
rel: 'noopener noreferrer', rel: 'noopener noreferrer',
...options.externalLinks ...options.externalLinks
}) }, siteConfig.cleanUrls)
// 3rd party plugins // 3rd party plugins
.use(attrs, options.attrs) .use(attrs, options.attrs)
.use(anchor, { .use(anchor, {

@ -11,7 +11,8 @@ const indexRE = /(^|.*\/)index.md(#?.*)$/i
export const linkPlugin = ( export const linkPlugin = (
md: MarkdownIt, md: MarkdownIt,
externalAttrs: Record<string, string> externalAttrs: Record<string, string>,
shouldCleanUrls: boolean
) => { ) => {
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]
@ -30,7 +31,7 @@ export const linkPlugin = (
// mail links // mail links
!url.startsWith('mailto:') !url.startsWith('mailto:')
) { ) {
normalizeHref(hrefAttr) normalizeHref(hrefAttr, shouldCleanUrls)
} }
// encode vite-specific replace strings in case they appear in URLs // encode vite-specific replace strings in case they appear in URLs
@ -43,7 +44,7 @@ export const linkPlugin = (
return self.renderToken(tokens, idx, options) return self.renderToken(tokens, idx, options)
} }
function normalizeHref(hrefAttr: [string, string]) { function normalizeHref(hrefAttr: [string, string], shouldCleanUrls: boolean) {
let url = hrefAttr[1] let url = hrefAttr[1]
const indexMatch = url.match(indexRE) const indexMatch = url.match(indexRE)
@ -52,12 +53,12 @@ export const linkPlugin = (
url = path + hash url = path + hash
} else { } else {
let cleanUrl = url.replace(/\#.*$/, '').replace(/\?.*$/, '') let cleanUrl = url.replace(/\#.*$/, '').replace(/\?.*$/, '')
// .md -> .html // transform foo.md -> foo[.html]
if (cleanUrl.endsWith('.md')) { if (cleanUrl.endsWith('.md')) {
cleanUrl = cleanUrl.replace(/\.md$/, '.html') cleanUrl = cleanUrl.replace(/\.md$/, shouldCleanUrls ? '' : '.html')
} }
// ./foo -> ./foo.html // transform ./foo -> ./foo[.html]
if (!cleanUrl.endsWith('.html') && !cleanUrl.endsWith('/')) { if (!shouldCleanUrls && !cleanUrl.endsWith('.html') && !cleanUrl.endsWith('/')) {
cleanUrl += '.html' cleanUrl += '.html'
} }
const parsed = new URL(url, 'http://a.com') const parsed = new URL(url, 'http://a.com')

@ -8,6 +8,7 @@ import { PageData, HeadConfig } from './shared'
import { slash } from './utils/slash' import { slash } from './utils/slash'
import chalk from 'chalk' import chalk from 'chalk'
import _debug from 'debug' import _debug from 'debug'
import { SiteConfig } from 'config'
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 })
@ -21,13 +22,15 @@ export interface MarkdownCompileResult {
} }
export function createMarkdownToVueRenderFn( export function createMarkdownToVueRenderFn(
srcDir: string, siteConfig: SiteConfig,
options: MarkdownOptions = {}, options: MarkdownOptions = {},
pages: string[], pages: string[],
userDefines: Record<string, any> | undefined, userDefines: Record<string, any> | undefined,
isBuild = false isBuild = false
) { ) {
const md = createMarkdownRenderer(srcDir, options) const { srcDir } = siteConfig;
const md = createMarkdownRenderer(siteConfig, options)
pages = pages.map((p) => slash(p.replace(/\.md$/, ''))) pages = pages.map((p) => slash(p.replace(/\.md$/, '')))
const userDefineRegex = userDefines const userDefineRegex = userDefines

@ -80,7 +80,7 @@ export function createVitePressPlugin(
configResolved(resolvedConfig) { configResolved(resolvedConfig) {
config = resolvedConfig config = resolvedConfig
markdownToVue = createMarkdownToVueRenderFn( markdownToVue = createMarkdownToVueRenderFn(
srcDir, siteConfig,
markdown, markdown,
pages, pages,
config.define, config.define,

3
types/shared.d.ts vendored

@ -40,7 +40,8 @@ export interface SiteData<ThemeConfig = any> {
*/ */
label: string label: string
} }
> >,
cleanUrls: boolean;
} }
export type HeadConfig = export type HeadConfig =

Loading…
Cancel
Save