Assets and Deployment
How Vite serves the front end in development and production, what the manifest is for, why asset versions force a reload, and what to change before shipping.
Assets and Deployment
Section titled “Assets and Deployment”One flag decides everything
Section titled “One flag decides everything”VITE_DEV=true. The shell points at the Vite dev server, and you get hot
module replacement. Nothing is built.
VITE_DEV=false. The shell reads the build manifest and emits the hashed
filenames from it. npm run build must have run.
npm run build # writes static/buildVITE_DEV=false uvicorn app:appThat is the whole difference. {{ inertia_head }} in root.html expands to
one thing or the other.
The manifest
Section titled “The manifest”Vite writes a manifest naming every hashed output file, and the adapter reads it to build the script tags. Without it a production render has no way to find the filenames. The page comes back with no script tag and no error.
build: { outDir: 'static/build', manifest: true, // without this there is no manifest rollupOptions: { input: 'js/main.tsx' },}BUILD_DIR = BASE_DIR / "static" / "build"MANIFEST = BUILD_DIR / ".vite" / "manifest.json"The adapter is given the full path to the manifest rather than a directory.
Vite moved it into .vite/ inside the output directory as of Vite 5 (it used
to sit at the root) so naming the file means a future move fails loudly here
instead of rendering a page with no script tag.
Serving the built files
Section titled “Serving the built files”app/bootstrap.py mounts static/build/assets at /assets, because that is
what the adapter builds URLs against: it takes each file named in the
manifest, strips the leading assets/, and prefixes asset_prefix.
The directory served must be the assets folder inside the build output,
not the output itself. Mount one level up and every script 404s against a
manifest that is perfectly correct.
The mount is skipped when nothing is built, so pytest and uvicorn app:app --reload do
not require npm run build first.
In production, put nginx or Caddy in front and let it serve
static/build/assets directly. This mount never sees traffic.
Asset versions
Section titled “Asset versions”When the client’s X-Inertia-Version does not match the current one, the
middleware answers with a 409 and X-Inertia-Location before the handler
runs. The client does a full visit and comes back on the current build.
That is what stops a browser running yesterday’s JavaScript from rendering a new page object against components that no longer match it.
Inertia(app, root_view=..., version="1.0.0") # a fixed stringInertia(app, root_view=..., version=read_manifest_hash) # re-read per ctxA version of None disables the check.
Set ASSET_VERSION to something that changes per release. A commit SHA does.
Leave it fixed across deploys and clients keep the stale bundle until they
happen to hard-reload.
Before you ship
Section titled “Before you ship”Everything on Deployment applies. Three things are specific here:
- Build the front end as part of the release, before the application
starts.
npm run build. - Set
VITE_DEV=false. Left true, production serves script tags pointing at a dev server that is not running. - Set
ASSET_VERSIONper release, so clients pick up the new bundle.
A release looks like:
uv sync --no-devnpm ci && npm run builduv run sillo db:migrateVITE_DEV=false ASSET_VERSION=$(git rev-parse --short HEAD) uvicorn app:appThings that will bite you
Section titled “Things that will bite you”A page renders with no JavaScript and no error. The entry in
vite.config.ts and ENTRY in app/inertia.py have drifted. Development
still works, because the dev server serves whatever path it is asked for;
production silently ships a page with no script tag.
Every script 404s but the manifest looks right. The static mount is one
level too high. It must be static/build/assets, not static/build.
Styles missing in production only. Tailwind is a Vite plugin here, so the
stylesheet is an output of the build like any other. If npm run build did not
run, there is no CSS to name.
A client stuck on an old bundle. ASSET_VERSION did not change between
releases, so the version check never fires.
Related
Section titled “Related”- Deployment: everything both starters share
- Static Files: the file server itself