Blazor WebAssembly
A Blazor WebAssembly app is a folder of static files, so nothing is special-cased for its sake. This is the static file handler with the three settings a Blazor publish needs — and getting any of them wrong produces a blank page and a stack trace from inside the runtime rather than an obvious error.
app.UseBlazorWebAssembly("./wwwroot");app.MapGet("/api/readings", …); // the app's own API, alongside itFor the packaged case — a MAUI or single-file build with no wwwroot on disk:
app.UseBlazorWebAssembly(typeof(App).Assembly, "MyApp.wwwroot");Or, better once the publish is more than a handful of files, zip the wwwroot at build time and
embed the one archive — the paths survive instead of being flattened into dotted resource names, and
the assets stay compressed inside the binary:
app.UseBlazorWebAssembly( new ZipFileSource(typeof(App).Assembly, "MyApp.wwwroot.zip") { PrecompressedEncodings = ["br", "gzip"] });See serving from a zip.
What it arranges
Section titled “What it arranges”The single-page fallback. A Blazor route like /counter exists only in the client-side router, so
a reload of that URL has to return index.html. The fallback is still restricted to requests that
look like navigations, so a missing .js 404s honestly.
Precompressed assets. A publish emits .br and .gz beside every file, compressed once at
maximum effort. Serving those beats recompressing 20-odd megabytes per request at a level chosen for
speed. ServePrecompressedFiles is on, and the source built for you — from a path or an assembly —
is configured to look for br then gzip. A source you construct yourself needs
PrecompressedEncodings set on it, as the zip example above does: a publish zips its sidecars too,
and they are stored rather than deflated, so serving one is a straight copy out of the archive.
Cache policy. Everything under _framework and _content is content-fingerprinted by the
publish, so it is served public, max-age=31536000, immutable. The entry document is no-cache —
it keeps the same URL by definition, and caching it is how a deploy silently fails to reach anyone.
no-cache rather than no-store: the browser may keep its copy, it just has to revalidate, and with
an ETag that is a conditional request answered by a 304.
You can still adjust everything through the optional configure callback, which runs after those
defaults are applied.
Content types
Section titled “Content types”Only one thing was ever actually missing when this was verified against a real publish — 26 MB of
assets and 36 wasm assemblies, loaded in Chrome and interactive. .dat, the ICU globalization data,
had no content type, and an unknown extension is not served, so the runtime died on a 404 before it
started.
.dat, .blat, .webcil, .dll, .pdb and .webmanifest are in the built-in map. If a publish
ever emits something else new, add it rather than turning on ServeUnknownFileTypes:
app.UseBlazorWebAssembly("./wwwroot", o => o.ContentTypeOverrides[".xyz"] = "application/octet-stream");Why you would do this
Section titled “Why you would do this”The interesting deployment is not a web server. It is a Blazor app served by the device it talks to: a MAUI app hosting its own UI, an appliance with no cloud, a piece of equipment whose configuration page is only reachable on the LAN. Pair it with a tunnel and the same app is reachable from anywhere without any of it being deployed anywhere.


