Deploy
Static hosts
Deploy a fully prerendered site to GitHub Pages, Cloudflare, Netlify, S3 or any file server: dist/client is the whole site.
lumiose build writes no dist/server when no route renders per request (no server route, no
spa route rendered on the server per request, no form action): see
Rendering modes. What dist/client holds:
- the pages as
path/index.html(path.htmlwithlumiose({ trailingSlash: "never" })), and404.html, rendered from your rootnot-foundpage; assets/, named by content hash: cache them for a year (files you put inpublic/assets/are not hashed: keep them elsewhere inpublic/, or out of the long cache);_headers: Lumiose's default headers andlumiose({ headers })in the format Netlify and Cloudflare read;- a
.brand a.gzcopy of every text file of 1 kB or more, for servers that serve precompressed files. Hosts that compress on their own serve them as plain files; leave them out of the upload there.
Set Vite's base when the site is served below a path (base: "/my-repo/" for a GitHub Pages
project site): links built with href() follow it.
How far each host's steps were checked is in the verification table.
#Cloudflare
Use the Cloudflare adapter even for a static site: it skips the precompressed
copies and adds a year of caching for the hashed files of /assets/ to _headers. Without a server, the build writes
no Worker, and the wrangler config has assets only:
// wrangler.jsonc
{
"name": "my-site",
"compatibility_date": "2026-09-01",
"assets": { "directory": "dist/client", "not_found_handling": "404-page" },
}pnpm wrangler deploy uploads it. not_found_handling: "404-page" serves 404.html with status
404 for unknown URLs. On Cloudflare Pages, pnpm wrangler pages deploy dist/client deploys the
same directory (Pages reads _headers and serves 404.html by itself); add a
dist/client/.assetsignore with *.br and *.gz there, or build with the adapter.
#Netlify
Use the Netlify adapter and the same netlify.toml: for a static site the build
writes no function, and Netlify serves 404.html and applies _headers by itself.
#GitHub Pages
Deploy dist/client with GitHub's Pages actions (set the repository's Pages source to "GitHub
Actions"). GitHub Pages serves 404.html for unknown URLs and ignores _headers; it compresses
on its own.
# .github/workflows/pages.yml
name: Pages
on:
push:
branches: [main]
permissions:
contents: read
pages: write
id-token: write
jobs:
deploy:
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 24
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm lumiose build
- run: find dist/client -name "*.br" -o -name "*.gz" | xargs rm -f
- uses: actions/upload-pages-artifact@v3
with:
path: dist/client
- id: deployment
uses: actions/deploy-pages@v4For a project site (https://<user>.github.io/<repo>/), build with base: "/<repo>/" in
vite.config.ts.
#S3 and CloudFront
Upload dist/client to the bucket with a long cache for the hashed assets and revalidation for the
rest, then let CloudFront compress (enable "Compress objects automatically") and answer unknown URLs
with 404.html (a custom error response for 403 and 404, status 404):
aws s3 sync dist/client s3://my-bucket --delete \
--exclude "*.br" --exclude "*.gz" --exclude "_headers" \
--exclude "assets/*" --cache-control "public, max-age=0, must-revalidate"
aws s3 sync dist/client/assets s3://my-bucket/assets --delete \
--exclude "*.br" --exclude "*.gz" --cache-control "public, max-age=31536000, immutable"S3 serves about/index.html for /about/ only from its website endpoint (or with a CloudFront
function that appends index.html). Headers from lumiose({ headers }) go into a CloudFront
response headers policy.
#Any file server
nginx, Caddy or another server can serve dist/client as it is, .br/.gz copies included
(nginx's brotli_static/gzip_static, Caddy's precompressed). lumiose preview serves it
locally the way static hosts do.