chore: tighten comments and docs

Comment pass over the branch: stale claims corrected (the sentinel-swap
description, plugin ordering), repeated rationale consolidated onto the
sentinel constant, jsdoc merged to read as single docs at neighbor scale,
and restated-code comments dropped. The asset-handling base section now
defers value rules and the CLI override to the site-config reference.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
pull/5406/head
Divyansh Singh 2 weeks ago
parent 12b94474eb
commit f8a7062dd1

@ -22,8 +22,8 @@ afterAll(async () => {
await t.browser.close()
})
// no hydration over file:// — module scripts are CORS-blocked from disk in
// every engine — but the pre-rendered site must stay styled and navigable
// module scripts are cors-blocked from disk, so nothing hydrates here; the
// pre-rendered site must still be styled and navigable
describe('relative base opened over file://', () => {
test('pages render styled with working images', async () => {
await t.page.goto(fileUrl('sub/page.html'))

@ -14,7 +14,8 @@ afterAll(async () => {
await t.browser.close()
})
// mark the window so a passing test proves navigation stayed client-side
// mark the window with a marker that only survives client-side navigation,
// proving no full document reload occurred
const mark = () => t.page.evaluate(() => ((window as any).__spa_marker = 1))
const marked = () => t.page.evaluate(() => (window as any).__spa_marker === 1)

@ -9,7 +9,6 @@ export default defineConfig({
hookTimeout: timeout,
teardownTimeout: timeout,
globals: true,
// suites share fixture builds but not servers/pages; keep them serial
fileParallelism: false
}
})

@ -58,14 +58,12 @@ let browserServer: BrowserServer
let servers: Server[] = []
export async function setup() {
// the cdn server starts before its dist exists (requests just 404 until
// the build lands) so the real port can be baked into assetsBase
// started before its dist exists so its real port can go into assetsBase
const cdnServer = await serveStatic([['/', dist('cdn')]], true)
const cdnPort = portOf(cdnServer)
// each flavor builds in its own process: the markdown renderer is a
// process-wide singleton, so sequential in-process builds would leak the
// first build's base into the rest
// one process per flavor: the markdown renderer is a module-level
// singleton, so in-process builds would leak the first base into the rest
for (const mode of ['plain', 'relative', 'cdn', 'mpa']) {
const res = spawnSync(process.execPath, [bin, 'build', 'fixture'], {
cwd: dir,

@ -36,23 +36,15 @@ Note that you should reference files placed in `public` using root absolute path
## Base URL
If your site is deployed to a non-root URL, you will need to set the `base` option in `.vitepress/config.js`. For example, if you plan to deploy your site to `https://foo.github.io/bar/`, then `base` should be set to `'/bar/'` (it should always start and end with a slash).
If your site is deployed to a non-root URL, set the [`base`](../reference/site-config#base) option. For example, if you plan to deploy your site to `https://foo.github.io/bar/`, then `base` should be set to `'/bar/'`
All your static asset paths are automatically processed to adjust for different `base` config values. For example, if you have an absolute reference to an asset under `public` in your markdown:
Static asset references are automatically adjusted for the base, so an absolute reference to a file in `public` works with any `base` and never needs updating:
```md
![An image](/image-inside-public.png)
```
You do **not** need to update it when you change the `base` config value in this case. This includes a relative base (`'./'`), which makes the whole build [relocatable](./deploy#relocatable-builds-relative-base).
However, if you are authoring a theme component that links to assets dynamically, e.g. an image whose `src` is based on a theme config value:
```vue
<img :src="theme.logoPath" />
```
In this case it is recommended to wrap the path with the [`withBase` helper](../reference/runtime-api#withbase) provided by VitePress:
Only dynamically constructed paths need care — for example, an image whose `src` is based on a theme config value. Wrap those with the [`withBase` helper](../reference/runtime-api#withbase) so the base is prepended at runtime:
```vue
<script setup>

@ -374,7 +374,7 @@ export default {
The base URL the site will be deployed at. You will need to set this if you plan to deploy your site under a sub path, for example, GitHub pages. If you plan to deploy your site to `https://foo.github.io/bar/`, then you should set base to `'/bar/'`. It should always start and end with a slash.
Setting base to `'./'` produces a [relocatable build](../guide/deploy#relocatable-builds-relative-base) whose pages reference everything relative to their own location, so the same output works from any sub path (IPFS gateways, archives) without rebuilding and stays browsable when opened directly from the file system.
The one exception is `'./'`, which produces a [relocatable build](../guide/deploy#relocatable-builds-relative-base): pages reference everything relative to their own location, so the same output works from any sub path (IPFS gateways, archives) without rebuilding and stays browsable when opened directly from the file system.
The base is automatically prepended to all the URLs that start with / in other options, so you only need to specify it once.
@ -384,6 +384,8 @@ export default {
}
```
Can also be set per build with `vitepress build --base /base/`.
## Routing
### cleanUrls

@ -13,8 +13,6 @@ const createLink = () => document.createElement('link')
const viaDOM = (url: string) => {
const link = createLink()
link.rel = `prefetch`
// chunks on an external assetsBase are later fetched in CORS mode; the
// prefetch must match or the cache entry is not reused
if (EXTERNAL_URL_RE.test(url)) link.crossOrigin = ''
link.href = url
document.head.appendChild(link)

@ -18,11 +18,14 @@ export { joinPath } from '../shared'
let resolvedBase: string | undefined
/**
* The base the site is actually served under. Equals the configured base,
* except for a relative base ('./'), which cannot be known at build time:
* there it is recovered from the per-page `__VP_SITE_ROOT__` inline script
* in the browser, is '/' in dev (dev always serves at the root), and is the
* build sentinel during SSR so rendered URLs can be relativized per page.
* Runtime base path used by the app.
*
* Usually this is the configured site base.
*
* For a relative base (`'./'`), the mount point is unknown at build time, so:
* - SSR: uses `RELATIVE_BASE_SENTINEL` (for per-page URL relativization)
* - dev browser: uses `'/'` (dev server always mounts at root)
* - prod browser: resolves from the page's `__VP_SITE_ROOT__`
*/
export function runtimeBase(): string {
if (resolvedBase === undefined) {
@ -63,6 +66,8 @@ export function pathToFile(path: string) {
// /foo/bar.html -> ./foo_bar.md
if (inBrowser) {
const base = runtimeBase()
// the site root may arrive without its trailing slash; anything
// outside the base has no page chunk at all
if (pagePath + '/' === base) pagePath = base
if (!pagePath.startsWith(base)) return null
pagePath =

@ -14,9 +14,8 @@ const route = useRoute()
const { hasSidebar, hasAside, leftAside } = useLayout()
const pageName = computed(() => {
// under a relative base the mount point is unknowable at build time, so
// the page class must be derived from the site-relative path to stay
// identical between SSR and any hydration location
// the mount point is unknown at build time, so the class must come from
// the site-relative path or ssr and hydration disagree
const path = isRelativeBase(site.value.base)
? '/' + route.path.slice(runtimeBase().length)
: route.path

@ -58,8 +58,6 @@ export function normalizeLink(url: string): string {
)
if (isRelativeBase(site.value.base) && !site.value.cleanUrls) {
// file:// has no directory index; the router strips index.html back
// out of the address bar on navigation
const pathPart = normalizedPath.replace(/[?#].*$/, '')
if (pathPart.endsWith('/')) {
normalizedPath =

@ -76,8 +76,6 @@ export async function renderPage(
const siteData = resolveSiteDataByRoute(config.site, page, pageData.filePath)
const relativeBase = isRelativeBase(siteData.base)
// under a relative base every page addresses the site root through its
// own ../-prefix; otherwise this is just the configured base
const pageBase = relativeBase ? relativePathToRoot(page) : siteData.base
const assetUrl = (file: string) => (config.assetsBase ?? pageBase) + file
@ -131,8 +129,8 @@ export async function renderPage(
rel,
// don't add base to external urls
href: EXTERNAL_URL_RE.test(file) ? file : assetUrl(file),
// keep the prefetch/preload request mode aligned with the later
// cross-origin module fetch, or the cache entry is not reused
// must match the cors mode of the later module fetch, or the
// cached response is not reused
...(assetsCrossOrigin && !EXTERNAL_URL_RE.test(file)
? { crossorigin: '' }
: {})
@ -234,8 +232,6 @@ export async function renderPage(
const htmlFileName = path.join(config.outDir, page.replace(/\.md$/, '.html'))
await mkdir(path.dirname(htmlFileName), { recursive: true })
// relativized before the hook: transforms see (and may inject) final
// urls, never the build sentinel
const finalHtml = desentinel(html)
const transformedHtml = await config.transformHtml?.(
finalHtml,

@ -85,10 +85,7 @@ export const linkPlugin = (
// append base to internal (non-relative) urls
if (hrefAttr[1].startsWith('/')) {
if (isRelativeBase(base)) {
// resolve site-absolute links relative to this page so the
// output is identical in both builds and correct at any mount
// point; content-loader output is embedded in other pages, so
// there the site-absolute form is the only meaningful one
// page-relative, so the same html works at any mount point
if (env.relativizeUrls && env.relativePath != null) {
hrefAttr[1] =
relativePathToRoot(env.relativePath) + hrefAttr[1].slice(1)
@ -112,8 +109,7 @@ export const linkPlugin = (
) {
let url = hrefAttr[1]
// a relative base has no server guaranteed to resolve directory urls,
// so page links must point at the index.html file itself
// directory urls need a server to resolve them, and file:// has none
const explicitIndex = isRelativeBase(base) && !env.cleanUrls
const indexMatch = url.match(indexRE)

@ -130,9 +130,8 @@ export async function createVitePressPlugin(
markdownToVue = await createMarkdownToVueRenderFn(
srcDir,
markdown ?? {},
// the site base, not config.base: the SSR build runs under the
// relative-base sentinel, but markdown must compile identically in
// both builds (they share one md singleton and one compile cache)
// the site base, not the vite base: the ssr build runs under the
// sentinel, and one md singleton serves both builds
site.base,
lastUpdated ?? false,
cleanUrls ?? false,
@ -457,7 +456,7 @@ export async function createVitePressPlugin(
hmrFix,
webFontsPlugin(siteConfig.useWebFonts),
...(userViteConfig?.plugins || []),
// last so its config hook sees (and chains behind) any user renderBuiltUrl
// must stay after the user plugins; see assetsBasePlugin
...(siteConfig.assetsBase ? [assetsBasePlugin(siteConfig)] : []),
await localSearchPlugin(siteConfig),
staticDataPlugin,

@ -1,22 +1,18 @@
import type { Plugin, UserConfig as ViteUserConfig } from 'vite'
import type { Plugin } from 'vite'
import type { SiteConfig } from '../config'
export type RenderBuiltUrl = NonNullable<
NonNullable<ViteUserConfig['experimental']>['renderBuiltUrl']
>
/**
* Routes built asset URLs through `assetsBase` via Vite's renderBuiltUrl,
* chaining behind any user-provided hook. Only plain-string returns are
* produced: {runtime} would poison the SSR bundle that pre-renders pages
* (it executes at module scope in Node) and errors in CSS.
* Routes built asset URLs through `assetsBase`, chaining behind any user
* renderBuiltUrl. Plain strings only: a `{ runtime }` return would execute
* at module scope in the Node SSR bundle, and is an error in CSS.
*/
export function assetsBasePlugin(config: SiteConfig): Plugin {
return {
name: 'vitepress:assets-base',
// post + appended last: the config hook must run after every user
// plugin so it chains behind (not under) their renderBuiltUrl
// 'post', plus a position after the user plugins in plugin.ts: the
// config hook must run after theirs to chain behind (not under) their
// renderBuiltUrl
enforce: 'post',
config(userConfig, env) {
if (env.command !== 'build') return

@ -20,7 +20,6 @@ export async function serve(options: ServeOptions = {}) {
const port = options.port ?? 4173
const config = await resolveConfig(options.root, 'serve', 'production')
// a build may have been made with --assetsBase; let preview mirror it
const assetsBase =
typeof options.assetsBase === 'string'
? normalizeAssetsBase(options.assetsBase)
@ -31,7 +30,7 @@ export async function serve(options: ServeOptions = {}) {
config?.site?.base ??
'/'
if (isRelativeBase(rawBase)) {
// a relocatable build works at any mount point; serve it at the root
// a relative base works at any mount point; serve it at the root
rawBase = '/'
} else if (EXTERNAL_URL_RE.test(rawBase)) {
rawBase = new URL(rawBase, 'http://a.com').pathname

@ -92,10 +92,9 @@ export interface UserConfig<
*/
extends?: RawConfigExports<ThemeConfig>
/**
* The base URL the site is deployed at. Must start and end with a slash.
* Can also be `'./'` to build a relocatable site whose pages reference
* everything relatively, so the output works from any subpath (IPFS,
* archives) and stays browsable over `file://`.
* The base URL the site is deployed at. Usually starts and ends with a
* slash. Use `'./'` to make page references relative to their own depth,
* so the output works at any subpath.
* @default '/'
*/
base?: string
@ -123,14 +122,13 @@ export interface UserConfig<
assetsDir?: string
/**
* URL prefix the built assets (everything under `assetsDir`) are served
* from, e.g. a CDN. The emitted asset URL is this prefix joined with the
* output-relative file path, so the target should mirror the layout of
* `outDir` (`https://cdn.example.com/` serves `outDir/assets/*` at
* `https://cdn.example.com/assets/*`). Must be an absolute URL, a
* protocol-relative URL, or a root-absolute path; a trailing slash is
* appended if missing. HTML pages, `withBase` links, `public/` files,
* `hashmap.json` and `vp-icons.css` stay on `base`. Applied only to
* production builds and preview, never to dev.
* from, e.g. a CDN. Must be an absolute URL, a protocol-relative URL, or
* a root-absolute path, and must mirror the layout of `outDir`: each URL
* is this prefix plus the file's output-relative path. Pages, `withBase`
* links, `public/` files, `hashmap.json` and `vp-icons.css` stay on
* `base`. A cross-origin prefix must send CORS headers, as the generated
* tags are marked `crossorigin`. Applies to builds and preview, not dev.
* @example 'https://cdn.example.com/'
*/
assetsBase?: string
/**
@ -365,8 +363,7 @@ export interface SiteConfig<ThemeConfig = any> extends Pick<
*/
assetsDir: string
/**
* Normalized URL prefix for built assets (ends with a slash), when
* configured.
* URL prefix for built assets, normalized to end with a slash.
*/
assetsBase?: string
/**

@ -30,9 +30,11 @@ export type {
export const EXTERNAL_URL_RE = /^(?:[a-z]+:|\/\/)/i
export const APPEARANCE_KEY = 'vitepress-theme-appearance'
// stand-in base for the SSR build under a relative base — every URL the SSR
// bundle base-joins carries it into the rendered HTML, where renderPage
// replaces it with the page's own ../-prefix as the final build step
/**
* Placeholder base used by SSR when base is relative.
* It is prepended to emitted URLs, then replaced with the ../ prefix
* from each file back to the site root.
*/
export const RELATIVE_BASE_SENTINEL = '/__VP_BASE__/'
export function isRelativeBase(base: string): boolean {
@ -49,8 +51,8 @@ export function relativePathToRoot(relativePath: string): string {
}
/**
* Join two paths by resolving the slash collision, preserving the double
* slash of an absolute or protocol-relative URL base.
* Join two paths, collapsing slash collisions but keeping the `//` that
* follows a protocol.
*/
export function joinPath(base: string, path: string): string {
const protocol = /^(?:[a-z]+:)?\/\//i.exec(base)?.[0] ?? ''

@ -1,8 +1,3 @@
// the editor's project for the sources. Self-contained: `vitepress` resolves
// onto src (same trick as tsconfig.client.json), so the built dist whose
// bundled types re-declare the `vite` and default-theme augmentations under
// different type identities never enters this program. Tests and docs have
// their own nearest tsconfigs and resolve the package normally.
{
"extends": "./tsconfig.base.json",
"compilerOptions": {

10
types/shared.d.ts vendored

@ -168,8 +168,8 @@ export interface Header {
*/
export interface SiteData<ThemeConfig = any> {
/**
* The base URL the site is deployed at, or './' for a relocatable build
* whose pages reference everything relative to their own depth.
* The base URL the site is deployed at, or `'./'` when each page
* references the site relative to its own depth.
* @default '/'
*/
base: string
@ -588,9 +588,9 @@ export interface MarkdownEnv {
*/
cleanUrls: boolean
/**
* Whether the rendered HTML is emitted at `relativePath`'s location, so
* site-absolute links may be rewritten relative to it (page renders set
* this; content-loader output is embedded in other pages, so it must not).
* Whether the rendered HTML is emitted at `relativePath`, so site-absolute
* links may be rewritten relative to it. Content loaders must not set it:
* their HTML is embedded in other pages.
* @internal
*/
relativizeUrls?: boolean

Loading…
Cancel
Save