In VitePress 1.6, what does `vitepress build` check about internal links, and what changes when you set `cleanUrls: true` or deploy under a sub-path with `base`?
answer
- the build refuses broken links
- only links written in Markdown
- anchors are not checked
- the host must answer without .html
- slashes on both ends
basics
~20 svitepress build fails when an internal Markdown link points to no page, unless ignoreDeadLinks allows it. cleanUrls: true drops .html from links, so the host must serve /foo.html for /foo; base: '/nimbus-ui/' prefixes every root-relative link.
solid answer
~50 sDuring `vitepress build`, every internal link written in Markdown, such as `./props.md` or `/guide/theming`, is resolved against the site's pages and `public/`; if nothing matches, the build logs each one as a dead link and fails. `vitepress dev` does not run this check, links to a `localhost` port count as dead, and the query and hash are stripped first, so a missing anchor is not caught. `ignoreDeadLinks` relaxes it with `true`, `'localhostLinks'` or a list of strings, regexes and functions. By default links end in `.html`; `cleanUrls: true` removes it from generated links while the files stay `foo.html`, so the host must serve `/foo.html` at `/foo` without a redirect. `base: '/nimbus-ui/'`, starting and ending with a slash, is prepended to root-relative links in Markdown and config, but strings built in your own components need `withBase('/logo.svg')`.
code
ts · 11 lines// docs/.vitepress/config.mts
import { defineConfig } from 'vitepress'
export default defineConfig({
base: '/nimbus-ui/',
cleanUrls: true, // the host must serve /foo.html at /foo
ignoreDeadLinks: [
/^\/playground\//, // served by a separate app
/^https?:\/\/localhost/, // local dev links in the contributing guide
],
})go deeper
Recall that the build fails on broken internal links, that links end in .html by default, and that base is needed for a sub-path deployment.
Explain how links are resolved and checked, what ignoreDeadLinks accepts, and what cleanUrls changes in links versus files.
Show how you prevent broken docs in CI, match cleanUrls to the host's behaviour, and handle base in component code with withBase.
Decide URL policy for the docs, clean or .html, root or sub-path, knowing that changing it later breaks every external link to the site.
## The dead-link check When VitePress compiles a Markdown page, its link plugin collects every **internal link** written in Markdown syntax. At the end of `vitepress build`, each one is resolved: 1. The query string and hash are removed, and a trailing `.md` or `.html` is dropped. 2. A trailing `/` becomes `index`, and relative links are resolved against the page's folder. 3. The result is looked up among the site's pages, taking `rewrites` into account, and among `.html` files in `public/`. 4. Anything unmatched is reported as `(!) Found dead link <url> in file <file>`, and the build throws `N dead link(s) found.` Details that matter in practice: - **Only `vitepress build` fails.** The check runs in a build-only hook, so `vitepress dev` never complains; run a build in CI. - **Only Markdown links are checked.** Raw HTML `<a>` tags, links inside Vue components and `themeConfig` nav or sidebar entries are not. - **Anchors are not validated.** `./props.md#size` passes as long as `props.md` exists. - **Localhost links count as dead.** A link with a localhost port, such as `http://localhost:5173`, fails the build unless ignored. `ignoreDeadLinks` (default `false`) relaxes the check: | Value | Effect | |---|---| | `true` | never fail on dead links | | `'localhostLinks'` | fail on dead links but ignore localhost ones | | an array | ignore links matching an exact string, a `RegExp` or a predicate function | A targeted array keeps the safety net for everything else; `true` turns it off. Note that `'localhostLinks'` is a value of its own: inside an array, a string only matches a link that is exactly that string, so to combine exceptions use a regex such as `/^https?:\/\/localhost/` in the array. ## `cleanUrls`: links without `.html` With the default `cleanUrls: false`, VitePress writes `guide/theming.html` and rewrites links like `./theming.md` to `./theming.html`. Any static host serves that. With `cleanUrls: true`: - generated links drop the extension: `./theming.md` becomes `./theming`; - the files are still written as `theming.html`; - if a visitor arrives on a `.html` path, the client router redirects to the extensionless one. So the **host** must answer `/guide/theming` with `guide/theming.html` **without a redirect**. If it cannot, a refresh or a shared link to `/guide/theming` gets the host's not-found page. The alternative when the host cannot do this is to structure pages as `theming/index.md`, which any host serves at `/guide/theming/`. ## `base`: deploying under a sub-path When the docs live at `https://example.github.io/nimbus-ui/`, set `base: '/nimbus-ui/'`. It should start and end with a slash; VitePress adds a missing trailing slash. The base is prepended to: - root-relative Markdown links, such as `/guide/theming`; - URLs in other config options that start with `/`, such as nav links. It is **not** added to strings in your own Vue code. A component that builds `'/logo.svg'` for an image in `public/` must use `withBase('/logo.svg')` from `vitepress`, or the image breaks under the sub-path. ## Verifying before deploying 1. `vitepress build docs` must pass, dead links included. 2. `vitepress preview docs` serves `.vitepress/dist` locally; with a `base`, the site appears under that path. 3. Configure long-lived caching for hashed files under `assets/`, since their names change with their content, and short caching for HTML. ## Common failure chains - A page is renamed; three other pages still link to the old name; the build fails, which is the point. - `cleanUrls` is switched on for a host that cannot map `/foo` to `foo.html`; pages work while navigating inside the app, then 404 on refresh or when opened from a shared link. - The site moves under a sub-path; Markdown links keep working, but hard-coded asset paths in components break until they use `withBase`. ## Renaming pages safely Because URLs come from file paths, moving `components/button.md` to `components/actions/button.md` changes a public URL. The dead-link check catches internal links to the old path, but not bookmarks, search results or links from other sites. Two tools help: - **`rewrites`** maps a source path to a different URL, such as `'components/actions/:slug*': 'components/:slug*'`, so files can be reorganised without changing URLs; the dead-link check takes rewrites into account, and relative links must then be written against the rewritten paths. - **Host-level redirects** from old URLs to new ones, configured on the static host, for pages whose URL really must change. Choosing `cleanUrls` and `base` early matters for the same reason: switching either later changes every URL on the site at once.
- Why does a link to ./props.md#size pass the dead-link check even though the page has no size heading?VitePress strips the query and hash before resolving a link, then only checks that a page exists at the path. Heading anchors are not validated, so renaming a heading can break deep links silently; fixing anchors with the {#size} syntax makes them stable across renames.
- How should a Vue component in the docs reference an image in public/ when the site uses a base?Wrap the path with `withBase` from `vitepress`: `withBase('/logo.svg')` returns `/nimbus-ui/logo.svg` under that base. VitePress adds the base automatically only to links it processes, such as Markdown links and config URLs, not to strings computed in component code.
saying these in an interview costs you the question
- vitepress dev fails on dead links just like the build.
- The dead-link check also validates heading anchors.
- With cleanUrls: true VitePress writes each page as folder/index.html.
- cleanUrls works on any host because the client router handles the rest.
- base is added to every URL string in the site, including component code.
- Links in themeConfig.sidebar are covered by the dead-link check.