trailing slash option

pull/869/head
Georges Gomes 3 years ago
parent a745d1bab9
commit dc49b6fcf1

@ -8,7 +8,7 @@ export default defineConfig({
description: 'Vite & Vue powered static site generator.', description: 'Vite & Vue powered static site generator.',
lastUpdated: true, lastUpdated: true,
cleanUrls: true, cleanUrls: 'with-trailing-slash',
themeConfig: { themeConfig: {
nav: nav(), nav: nav(),

@ -175,18 +175,26 @@ export default {
## cleanUrls ## cleanUrls
- Type: `boolean` - Type: `"off" | "with-trailing-slash" | "without-trailing-slash"`
- Default: `false` - Default: `"off"`
When set to `true`, page `foo/bar.md` is generated into `foo/bar/index.html` instead of `foo/bar.html`. This gives URL location look like `foo/bar` instead of `foo/bar.html`. When set to `"off"`, page `foo/bar.md` is generated into `foo/bar.html`.
Also work in MPA mode. When set to `"with-trailing-slash"` or `"without-trailing-slash"`, page `foo/bar.md` is generated into `foo/bar/index.html`.
Note: `404.md` page is kept transforming to `404.html` for hosting services. When set to `"with-trailing-slash"`, URLs will be `foo/bar/`.
When set to `"without-trailing-slash"`, URLs will be `foo/bar`.
Notes:
- `404.md` page is kept transforming to `404.html` for hosting services.
- Also work in MPA mode.
```ts ```ts
export default { export default {
cleanUrls: true cleanUrls: "without-trailing-slash"
} }
``` ```
### Hosting on Netlify
Always use `"off"` or `"with-trailing-slash"` when hosted on Netlify.

@ -42,11 +42,23 @@ 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) const url = new URL(href, fakeHost)
// ensure correct deep link so page refresh lands on correct files. if (siteDataRef.value.cleanUrls === 'off') {
if (!url.pathname.endsWith('/') && !url.pathname.endsWith('.html')) { // No clean URLs
url.pathname += siteDataRef.value.cleanUrls ? '' : '.html' // Let's add ".html" if missing
if (!url.pathname.endsWith('/') && !url.pathname.endsWith('.html')) {
url.pathname += '.html'
href = url.pathname + url.search + url.hash
}
} else if (
siteDataRef.value.cleanUrls === 'with-trailing-slash' &&
!url.pathname.endsWith('/')
) {
// Clean URLs with trailing slash
// Let's add missing slashes
url.pathname += '/'
href = url.pathname + url.search + url.hash href = url.pathname + url.search + url.hash
} }
if (inBrowser) { if (inBrowser) {
// save scroll position before changing url // save scroll position before changing url
history.replaceState({ scrollPosition: window.scrollY }, document.title) history.replaceState({ scrollPosition: window.scrollY }, document.title)

@ -1,5 +1,6 @@
import { ref } from 'vue' import { ref } from 'vue'
import { withBase } from 'vitepress' import { withBase } from 'vitepress'
import { cleanUrlsOptions } from '../../../../types/shared'
export const HASH_RE = /#.*$/ export const HASH_RE = /#.*$/
export const EXT_RE = /(index)?\.(md|html)$/ export const EXT_RE = /(index)?\.(md|html)$/
@ -69,7 +70,10 @@ export function normalize(path: string): string {
return decodeURI(path).replace(HASH_RE, '').replace(EXT_RE, '') return decodeURI(path).replace(HASH_RE, '').replace(EXT_RE, '')
} }
export function normalizeLink(url: string, cleanUrls: boolean = false): string { export function normalizeLink(
url: string,
cleanUrls: cleanUrlsOptions = 'off'
): string {
if (isExternal(url)) { if (isExternal(url)) {
return url return url
} }
@ -81,7 +85,11 @@ export function normalizeLink(url: string, cleanUrls: boolean = false): string {
? url ? url
: `${pathname.replace( : `${pathname.replace(
/(\.md)?$/, /(\.md)?$/,
cleanUrls ? '' : '.html' cleanUrls === 'off'
? '.html'
: cleanUrls === 'with-trailing-slash'
? '/'
: ''
)}${search}${hash}` )}${search}${hash}`
return withBase(normalizedPath) return withBase(normalizedPath)

@ -7,6 +7,7 @@ import { RollupOutput, OutputChunk, OutputAsset } from 'rollup'
import { HeadConfig, PageData, createTitle, notFoundPageData } from '../shared' import { HeadConfig, PageData, createTitle, notFoundPageData } from '../shared'
import { slash } from '../utils/slash' import { slash } from '../utils/slash'
import { SiteConfig, resolveSiteDataByRoute } from '../config' import { SiteConfig, resolveSiteDataByRoute } from '../config'
import { cleanUrlsOptions } from '../../../types/shared'
export async function renderPage( export async function renderPage(
config: SiteConfig, config: SiteConfig,
@ -158,12 +159,18 @@ export async function renderPage(
await fs.writeFile(htmlFileName, html) await fs.writeFile(htmlFileName, html)
} }
function transformHTMLFileName(page: string, shouldCleanUrls: boolean): string { function transformHTMLFileName(
page: string,
shouldCleanUrls: cleanUrlsOptions
): string {
if (page === 'index.md' || page.endsWith('/index.md') || page === '404.md') { if (page === 'index.md' || page.endsWith('/index.md') || page === '404.md') {
return page.replace(/\.md$/, '.html') return page.replace(/\.md$/, '.html')
} }
return page.replace(/\.md$/, shouldCleanUrls ? '/index.html' : '.html') return page.replace(
/\.md$/,
shouldCleanUrls !== 'off' ? '/index.html' : '.html'
)
} }
function resolvePageImports( function resolvePageImports(

@ -21,6 +21,7 @@ import {
import { resolveAliases, DEFAULT_THEME_PATH } from './alias' import { resolveAliases, DEFAULT_THEME_PATH } from './alias'
import { MarkdownOptions } from './markdown/markdown' import { MarkdownOptions } from './markdown/markdown'
import _debug from 'debug' import _debug from 'debug'
import { cleanUrlsOptions } from '../../types/shared'
export { resolveSiteDataByRoute } from './shared' export { resolveSiteDataByRoute } from './shared'
@ -77,7 +78,7 @@ export interface UserConfig<ThemeConfig = any> {
* Also generate static files as `foo/index.html` insted of `foo.html`. * Also generate static files as `foo/index.html` insted of `foo.html`.
* @default false * @default false
*/ */
cleanUrls?: boolean cleanUrls?: cleanUrlsOptions
} }
export type RawConfigExports<ThemeConfig = any> = export type RawConfigExports<ThemeConfig = any> =
@ -105,7 +106,7 @@ export interface SiteConfig<ThemeConfig = any>
tempDir: string tempDir: string
alias: AliasOptions alias: AliasOptions
pages: string[] pages: string[]
cleanUrls: boolean cleanUrls: cleanUrlsOptions
} }
const resolve = (root: string, file: string) => const resolve = (root: string, file: string) =>
@ -175,7 +176,7 @@ export async function resolveConfig(
shouldPreload: userConfig.shouldPreload, shouldPreload: userConfig.shouldPreload,
mpa: !!userConfig.mpa, mpa: !!userConfig.mpa,
ignoreDeadLinks: userConfig.ignoreDeadLinks, ignoreDeadLinks: userConfig.ignoreDeadLinks,
cleanUrls: !!userConfig.cleanUrls cleanUrls: userConfig.cleanUrls || 'off'
} }
return config return config
@ -280,7 +281,7 @@ export async function resolveSiteData(
locales: userConfig.locales || {}, locales: userConfig.locales || {},
langs: createLangDictionary(userConfig), langs: createLangDictionary(userConfig),
scrollOffset: userConfig.scrollOffset || 90, scrollOffset: userConfig.scrollOffset || 90,
cleanUrls: userConfig.cleanUrls || false cleanUrls: userConfig.cleanUrls || 'off'
} }
} }

@ -18,6 +18,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-toc-done-right' import toc from 'markdown-it-toc-done-right'
import { cleanUrlsOptions } from '../../../types/shared'
export type ThemeOptions = Theme | { light: Theme; dark: Theme } export type ThemeOptions = Theme | { light: Theme; dark: Theme }
@ -57,7 +58,7 @@ export const createMarkdownRenderer = async (
srcDir: string, srcDir: string,
options: MarkdownOptions = {}, options: MarkdownOptions = {},
base = '/', base = '/',
cleanUrls = false cleanUrls: cleanUrlsOptions = 'off'
): Promise<MarkdownRenderer> => { ): Promise<MarkdownRenderer> => {
const md = MarkdownIt({ const md = MarkdownIt({
html: true, html: true,

@ -6,6 +6,7 @@ import MarkdownIt from 'markdown-it'
import { MarkdownRenderer } from '../markdown' import { MarkdownRenderer } from '../markdown'
import { URL } from 'url' import { URL } from 'url'
import { EXTERNAL_URL_RE } from '../../shared' import { EXTERNAL_URL_RE } from '../../shared'
import { cleanUrlsOptions } from '../../../../types/shared'
const indexRE = /(^|.*\/)index.md(#?.*)$/i const indexRE = /(^|.*\/)index.md(#?.*)$/i
@ -13,7 +14,7 @@ export const linkPlugin = (
md: MarkdownIt, md: MarkdownIt,
externalAttrs: Record<string, string>, externalAttrs: Record<string, string>,
base: string, base: string,
shouldCleanUrls: boolean shouldCleanUrls: cleanUrlsOptions
) => { ) => {
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]
@ -51,7 +52,10 @@ export const linkPlugin = (
return self.renderToken(tokens, idx, options) return self.renderToken(tokens, idx, options)
} }
function normalizeHref(hrefAttr: [string, string], shouldCleanUrls: boolean) { function normalizeHref(
hrefAttr: [string, string],
shouldCleanUrls: cleanUrlsOptions
) {
let url = hrefAttr[1] let url = hrefAttr[1]
const indexMatch = url.match(indexRE) const indexMatch = url.match(indexRE)
@ -62,11 +66,18 @@ export const linkPlugin = (
let cleanUrl = url.replace(/[?#].*$/, '').replace(/\?.*$/, '') let cleanUrl = url.replace(/[?#].*$/, '').replace(/\?.*$/, '')
// transform foo.md -> foo[.html] // transform foo.md -> foo[.html]
if (cleanUrl.endsWith('.md')) { if (cleanUrl.endsWith('.md')) {
cleanUrl = cleanUrl.replace(/\.md$/, shouldCleanUrls ? '' : '.html') cleanUrl = cleanUrl.replace(
/\.md$/,
shouldCleanUrls === 'off'
? '.html'
: shouldCleanUrls === 'with-trailing-slash'
? '/'
: ''
)
} }
// transform ./foo -> ./foo[.html] // transform ./foo -> ./foo[.html]
if ( if (
!shouldCleanUrls && shouldCleanUrls === 'off' &&
!cleanUrl.endsWith('.html') && !cleanUrl.endsWith('.html') &&
!cleanUrl.endsWith('/') !cleanUrl.endsWith('/')
) { ) {

@ -9,6 +9,7 @@ import { deeplyParseHeader } from './utils/parseHeader'
import { getGitTimestamp } from './utils/getGitTimestamp' import { getGitTimestamp } from './utils/getGitTimestamp'
import { createMarkdownRenderer, MarkdownOptions } from './markdown/markdown' import { createMarkdownRenderer, MarkdownOptions } from './markdown/markdown'
import _debug from 'debug' import _debug from 'debug'
import { cleanUrlsOptions } from '../../types/shared'
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 })
@ -29,7 +30,7 @@ export async function createMarkdownToVueRenderFn(
isBuild = false, isBuild = false,
base = '/', base = '/',
includeLastUpdatedData = false, includeLastUpdatedData = false,
cleanUrls: boolean = false cleanUrls: cleanUrlsOptions = 'off'
) { ) {
const md = await createMarkdownRenderer(srcDir, options, base, cleanUrls) const md = await createMarkdownRenderer(srcDir, options, base, cleanUrls)

7
types/shared.d.ts vendored

@ -18,6 +18,11 @@ export interface Header {
slug: string slug: string
} }
export type cleanUrlsOptions =
| 'off'
| 'with-trailing-slash'
| 'without-trailing-slash'
export interface SiteData<ThemeConfig = any> { export interface SiteData<ThemeConfig = any> {
base: string base: string
@ -57,7 +62,7 @@ export interface SiteData<ThemeConfig = any> {
label: string label: string
} }
> >
cleanUrls: boolean cleanUrls: cleanUrlsOptions
} }
export type HeadConfig = export type HeadConfig =

Loading…
Cancel
Save