Rules for Blazor apps that stay fast and debuggable after the honeymoon β component discipline, render control, and the publish pitfalls that only appear in Release.
3 chapters Β· 8 rules
4 reusable commands β fill the placeholders, copy, done. Values you fill are never stored.
The current unified template β pick interactivity deliberately (see Rule 3).
dotnet new blazor -n {{project_name}} --interactivity {{interactivity}} --all-interactive
Follows Rule 3
The inner loop β file edits apply without restarting the circuit.
dotnet watchThe artifact that actually ships β and the only build worth smoke-testing (Rule 7).
dotnet publish -c Release -o {{output_dir}}
Careful: Release + trimming behaves differently from dotnet run β test the OUTPUT, not your dev server.
Follows Rule 7
Pushes a published WASM app to SWA β preview environments included.
npx swa deploy {{output_dir}}/wwwroot --deployment-token {{deployment_token}} --env {{swa_environment}}
Follows Rule 8
1 repeatable process β read straight through, or run step by step with a private record of what you did.
From working dev build to a public URL β including the two publish-only failure modes (trimming and sub-path routing) checked before users find them.
dotnet publish -c Release -o {{output_dir}}
This is the artifact that ships. Everything after this step tests the OUTPUT, never the dev server.
Serve the published output (dotnet serve or any static server) and walk the checklist: JSON round-trips, dynamic component rendering, culture formatting. Trimming failures appear here or in prod β nowhere else.
A type going missing in Release only = the trimmer. See the attached failure note.
β Works in dev, throws MissingMethodException after publish
Feature works under dotnet run; the published app throws MissingMethodException or silently returns empty objects during JSON deserialization
Likely cause: The IL trimmer removed types/members only reachable via reflection
Fix: Use System.Text.Json source generators for your DTOs, or add DynamicDependency/TrimmerRootAssembly for the affected types. Re-publish and re-run the smoke checklist.
Create staticwebapp.config.json next to index.html: { "navigationFallback": { "rewrite": "/index.html" } } Without it, every deep link 404s the moment a user refreshes.
β Blank page and framework 404s when hosted under a sub-path
Page is blank; DevTools shows 404 for _framework/blazor.webassembly.js and all assets
Likely cause: <base href="/"> in index.html points at the root while the app lives under /myapp/
Fix: Set <base href="/myapp/"> (trailing slash required) at publish time, or rewrite it in the deploy pipeline per environment.
npx swa deploy {{output_dir}}/wwwroot --deployment-token {{deployment_token}} --env {{swa_environment}}
Preview environments are free β prod should never be the first place a build runs.
npx swa deploy {{output_dir}}/wwwroot --deployment-token {{deployment_token}} --env {{swa_environment}}
Same card, env=production. Same artifact β never a rebuild between preview and prod (build once, deploy the same output).
Open the site in a browser that had the OLD version. Blazor's framework files are cache-busted by hash, but a stale service worker or aggressive CDN rule can pin users to the old app.
2 notes from the field β observations, discoveries, warnings, and lessons.
With server prerendering on, OnInitializedAsync runs twice β so your API gets called twice for every first load. Users don't notice; your API metrics and rate limits do. Persist prerendered state across to the client (PersistentComponentState) instead of re-fetching, or budget for the doubled call rate.
Hosting a WASM app under a sub-path (github.io/repo, an APIM route, a shared domain) breaks every asset load unless index.html's <base href> matches the sub-path, trailing slash included. The symptom β blank page, console full of 404s for framework files β looks catastrophic and is a one-line fix.
2 known failuresβ the approaches that didn't work, preserved so you don't repeat them.
Feature works under dotnet run; the published app throws MissingMethodException or silently returns empty objects during JSON deserialization
Attempted: Published with default Release settings and deployed
Context: Blazor WASM, Release publish with trimming enabled, System.Text.Json used with reflection
Likely cause: The IL trimmer removed types/members only reachable via reflection
Diagnose: Reproduce by serving the publish output locally; check browser console for MissingMethod/JsonException
Fix: Use System.Text.Json source generators for your DTOs, or add DynamicDependency/TrimmerRootAssembly for the affected types. Re-publish and re-run the smoke checklist.
Affects: .NET 6+ WASM with PublishTrimmed=true
Page is blank; DevTools shows 404 for _framework/blazor.webassembly.js and all assets
Attempted: Deployed the app under a path like /myapp/ behind a reverse proxy
Context: Any hosting where the app is not at the domain root
Likely cause: <base href="/"> in index.html points at the root while the app lives under /myapp/
Diagnose: View source: check the base tag against the actual path
Fix: Set <base href="/myapp/"> (trailing slash required) at publish time, or rewrite it in the deploy pipeline per environment.
Blazor rewards structure and punishes improvisation, usually three months in. These rules cover the seams where apps actually tear: component boundaries, the render loop, prerendering, and the trimming surprises waiting inside dotnet publish.
.NET developers building Blazor Server, WASM, or the modern unified model.
Initial publication.
Readers of this book also keep