Deployment Guide
The build output is plain static files: run pnpm exec vite build once, and build.outDir (dist for this site) holds the complete site. Any service that hosts static files can deploy it; no Node at runtime, no server-side rendering or API dependencies.
Output structure
dist/
├── index.html # one standalone .html per page; directories are routes
├── guide/
│ ├── index.html # directory index page
│ └── getting-started.html
├── assets/
│ ├── ... # client js/css (theme chrome + islands runtime)
│ └── img/ # content images, content-hash named
├── rss.xml # feed of the latest 20 articles
├── sitemap.xml # all routes
├── robots.txt
├── 404.html # fallback page for unmatched paths (fully inline, noindex)
└── _headers # default emit: cache/Early Hints headers for Cloudflare Pages / Netlify- Routes are extensionless clean URLs:
guide/foo.md→/guide/foo, andindex.mdproduces the index page of its directory (/guide/by default; switch to/guidewithurls.directoryIndex). Emitted file names always carry.html, so any static host serves them directly - Non-ASCII route segments (e.g. Chinese) are URL-encoded in generated links (like the encoded form of the archive page
/tag/主题), but the on-disk file name is the decoded original: static hosts percent-decode the request path before looking up the file, and the two sides line up — Chinese archive routes work out of the box, no rewrite rules needed - The KaTeX stylesheet and lazily loaded island chunks (Mermaid / G2Plot / DocSearch / photoSwipe) also live under
assets/, referenced per page via relative paths automatically; no extra configuration 404.htmlis generated by the build (hosts like Cloudflare Pages / GitHub Pages serve it for unknown paths, avoiding soft 404s); a404.mdat the content root overrides it — see SEO for details and limitations
hostname and sub-paths
hostname is the site's canonical origin (no trailing slash), used for absolute links in canonical/og meta, RSS, sitemap, and robots.txt. Set it to the real domain before going live:
defineSiteConfig({ hostname: 'https://absolute-docs.pages.dev' });All in-page resources and internal links use a relative prefix generated from page depth ('' for root pages, '../' for first-level subdirectories, and so on); there is no absolute-path assumption anywhere in the code. So the site behaves identically whether deployed at a domain root or a sub-path (like https://example.com/blog/), and moving the deployment location takes zero config changes.
Per-host notes
The output has no server-side requirements; mainstream platforms are zero-config or near-zero-config:
- Cloudflare Pages / Netlify / Vercel: build command
pnpm build, output directory =build.outDir; no SPA rewrite needed (noindex.htmlfallback requirement — every page is a real file). On Cloudflare Pages, preferurls.directoryIndex: 'bare'(CF 308s/guide/to/guide; bare is its native form) _headers(default emit): every build writes a_headersfile at the output root —/assets/*(content-hash named) gets a year-long immutableCache-Controlheader, and a site-wide/*rule carries aLinkheader with the client entry chunk as modulepreload, the entry CSS aspreload; as=style, and the locale's shared chrome module (navbar/sidebar/icon table, see the architecture doc) as modulepreload — so every page ships Early Hints warmup, not just the root; a non-default locale's prefix rule (e.g./en/*) overrides it with that locale's own chrome (the most specific rule wins per header). Cloudflare Pages promotes the Link header to a 103 Early Hints response (effective on subsequent requests to the same URL); Netlify consumes the same file, every other host just serves it as an inert file. Shipping your own_headersinpublic/is fine too: the build detects it and skips its own emit in favor of yours (the entry's hashed file name changes every build — keep up if you hand-maintain one)- GitHub Pages: publish the output directory directly; sub-path deployments like
https://user.github.io/repo/rely on the relative-prefix mechanism above and need no base config. The default'slash'form is GH's native form (/guidegets 301ed to/guide/), so nothing to tune - nginx / Caddy: point
root(orfile_server) at the output directory; do not configure an SPA fallback liketry_files ... /index.html
Caching advice: file names under assets/ carry a content hash (images are name.8-char-hash.ext), so cache them long (the default _headers emit already lands immutable caching for /assets/* on Cloudflare Pages and Netlify); use short or negotiated caching for .html and rss.xml / sitemap.xml so publishes take effect immediately.
Build as validation
The build process ships with two kinds of checks, so running one build in CI stops most go-live accidents:
- Dead-link checking: every
./../relative link in content is resolved at build time; failures error out listing the source file and the original link — a broken link cannot be published - icon validation: frontmatter
iconreferencing a key not registered in the site configiconserrors out
Wire the build command into CI (or just use the hosting platform's build pipeline) to get both gates; this site's vite.config.ts is a complete configuration you can reference directly.
CI suggestion
A minimal viable release flow:
pnpm verify # format / static checks / unit tests
pnpm exec vite build # or a docs-specific config
pnpm exec playwright test # e2e (spins up its own preview server)The git history drives each page's "last edited" time, so CI must check out the full history before building. GitHub Actions' actions/checkout defaults to fetch-depth: 1 (a shallow clone), leaving a single commit in history and collapsing every article's update time to the build's trigger moment (the build log warns about the shallow clone):
- uses: actions/checkout@v5
with:
fetch-depth: 0After binding the repo on a hosting platform, fill in the "build command" and "output directory" with the build and output above; changing domains means editing hostname in the site config once, and rss.xml / sitemap.xml / robots.txt follow automatically on the next build.