When you deploy a Blazor WebAssembly application to Azure Static Web Apps, everything works perfectly when you navigate from the root URL. But the moment a user bookmarks a deep link, shares a URL, or refreshes the page on any route other than /, they’re greeted with a cold 404 Not Found error.
This post explains why that happens, how to fix it with staticwebapp.config.json, and how to layer in proper cache-control headers so your users always receive the latest version of your app after each deployment.
Why Deep Links Break
Blazor WebAssembly is a single-page application (SPA). The entire app — HTML, CSS, JavaScript, and .NET assemblies — is downloaded once and runs in the browser. Routing happens entirely client-side: Blazor intercepts navigation and renders the correct component without ever making a new request to the server.
The problem arises when a request hits the server directly. If a user navigates to https://yourapp.com/game/settings, the browser sends a GET request for that path to Azure. Azure looks for a file or a server-side route at /game/settings, finds nothing, and returns a 404.
The fix is to tell Azure: for any path that doesn’t match a physical file, serve index.html instead. Blazor’s client-side router then takes over and renders the correct page.
The Fix: staticwebapp.config.json
Azure Static Web Apps supports a configuration file called staticwebapp.config.json placed in your wwwroot folder. It is automatically detected and applied at the CDN/hosting layer during deployment.
The key setting is navigationFallback:
{
"navigationFallback": {
"rewrite": "/index.html",
"exclude": [ "/images/*.{png,jpg,gif,ico}", "*.{css,scss,js,json}", "/api/*" ]
}
}
What this does
| Property | Purpose |
|---|---|
rewrite | Serve index.html for any unmatched navigation request |
exclude | Do not rewrite requests for real static assets — let them 404 naturally if missing |
The exclude patterns are important. Without them, a failed request for a missing .js file or image would silently return your HTML page, making debugging very confusing.
Bonus: Prevent Stale Deployments with Cache-Control Headers
Deep linking is only half the story. There is a second, subtler problem: users getting stuck on old versions of your app after you deploy an update.
Blazor WASM uses a service worker to cache all app assets for offline support. When you deploy a new version, the browser needs to detect the change. That detection chain starts with two files:
index.html— the entry point that bootstraps the app and registers the service workerservice-worker.js— the script the browser checks for updates
If either of these is served from a stale browser cache, the update check never happens and users continue running the old version indefinitely — potentially across multiple deployments.
The solution is to instruct Azure to send Cache-Control headers that prevent these two files from ever being cached:
{
"routes": [
{
"route": "/index.html",
"headers": {
"Cache-Control": "no-cache"
}
},
{
"route": "/service-worker.js",
"headers": {
"Cache-Control": "no-store, no-cache"
}
}
]
}
Header behaviour explained
| File | Header | Behaviour |
|---|---|---|
index.html | no-cache | Browser always revalidates with the server before serving from cache. If unchanged, the server returns 304 (no data transfer). If changed, the new version is fetched. |
service-worker.js | no-store, no-cache | Never written to any cache at all. Always fetched fresh from the network. |
All other assets — .dll, .wasm, .js, .css — are intentionally left with default caching. Blazor’s publish pipeline fingerprints these files with content hashes in their names (e.g., app.abc123.js), so they are effectively immutable. Caching them aggressively is safe and improves load performance.
Complete Configuration
Putting it all together, here is the complete staticwebapp.config.json for a Blazor WebAssembly app:
{
"routes": [
{
"route": "/index.html",
"headers": {
"Cache-Control": "no-cache"
}
},
{
"route": "/service-worker.js",
"headers": {
"Cache-Control": "no-store, no-cache"
}
}
],
"navigationFallback": {
"rewrite": "/index.html",
"exclude": [ "/images/*.{png,jpg,gif,ico}", "*.{css,scss,js,json}", "/api/*" ]
}
}
Place this file at wwwroot/staticwebapp.config.json in your Blazor project. It will be included in the published output automatically and picked up by Azure Static Web Apps on the next deployment — no portal configuration required.
Enabling Immediate Service Worker Updates
Cache-control headers ensure the browser always fetches a fresh service-worker.js. However, even after the browser detects a new service worker, it normally waits in a “waiting” state until every open tab running the old version is closed. For users who never fully close their browser, this means they could be on a stale version for days.
You can resolve this by adding skipWaiting() and clients.claim() to your service-worker.published.js:
async function onInstall(event) {
console.info('Service worker: Install');
self.skipWaiting(); // Activate immediately, don't wait for tabs to close
const assetsRequests = self.assetsManifest.assets
.filter(asset => offlineAssetsInclude.some(pattern => pattern.test(asset.url)))
.filter(asset => !offlineAssetsExclude.some(pattern => pattern.test(asset.url)))
.map(asset => new Request(asset.url, { integrity: asset.hash, cache: 'no-cache' }));
await caches.open(cacheName).then(cache => cache.addAll(assetsRequests));
}
async function onActivate(event) {
console.info('Service worker: Activate');
await clients.claim(); // Take control of all open pages immediately
const cacheKeys = await caches.keys();
await Promise.all(cacheKeys
.filter(key => key.startsWith(cacheNamePrefix) && key !== cacheName)
.map(key => caches.delete(key)));
}
With these two additions:
skipWaiting()— the new service worker activates as soon as it finishes installing, without waiting for old tabs to closeclients.claim()— the newly activated service worker immediately takes control of all open pages, replacing the old cached responses with new ones
Users may still see the old version on the first page load after a deployment (the old service worker serves the initial response), but a single page reload will give them the fully updated app. No manual cache clearing required.
Summary
| Problem | Solution |
|---|---|
| Deep links return 404 | Add navigationFallback to staticwebapp.config.json |
| Users stuck on old app version | Add no-cache headers for index.html and service-worker.js |
| Service worker waits forever to activate | Add skipWaiting() + clients.claim() to service-worker.published.js |
These three changes together give you reliable deep linking and confident, automatic deployments for any Blazor WebAssembly app hosted on Azure Static Web Apps.