Routing for Blazor,
done right.
A modern, declarative, nestable router with everything today's router libraries are
measured by - async guards, data loaders with stale-while-revalidate caching,
keep-alive, view transitions and compile-time-safe URLs - while your existing
@page components keep working unchanged.
# the router dotnet add package Bit.Brouter # optional: typed, compile-time-safe URL builders dotnet add package Bit.Brouter.Generators
@* AppRouter.razor - routes are components, so nesting is just markup *@ <Brouter NotFoundUrl="404" AppAssembly="@GetType().Assembly"> <Broute Path="/" Component="@typeof(HomePage)" /> <Broute Path="/users/{id:int}" Guard="@RequireSignIn" Loader="@LoadUser" StaleTime="@TimeSpan.FromMinutes(1)"> <Content><UserPage /></Content> <Routes> <Broute Path="/orders" Component="@typeof(OrdersPage)" /> </Routes> </Broute> </Brouter>
@page template matches identicallyCapabilities
Everything a modern router does - in Blazor
API design informed by React Router, Vue Router, Angular Router, SvelteKit and TanStack Router - built on ASP.NET Core's own route-template semantics.
Full template grammar
Literals, typed parameters, optionals, defaults, complex segments
({name}.{ext?}) and catch-alls - a superset-compatible match of the
built-in router, with specificity-based conflict resolution.
Nested routes & outlets
Parents render persistent layout and children fill outlets - including several named regions per route, and pathless groups for shared guards and loaders.
Nested routes →Guards & navigation locks
Async enter and leave guards that cancel preventively - the URL never flickers - plus component-level locks that hold a navigation open for a custom "unsaved changes" dialog.
Guards →Data loaders & caching
Fetch before render, with stale-while-revalidate caching, hover and viewport preloading, streamed deferred data, revalidation after mutations, and error boundaries with retry.
Data loading →View transitions
Direction-aware page animations and shared-element morphs through the browser's View Transitions API - one option to enable, overridable with plain CSS.
View transitions →Keep-alive & lifecycle
Keep pages mounted - per route or per parameter, LRU-bounded - so Back restores exact state, with activate/deactivate/renavigate callbacks to pause and resume background work.
Lifecycle →Typed routes
A source generator turns your route declarations into compile-time-safe URL
builders - BrouterRoutes.Counter(42) instead of a string, so a
broken link becomes a build error.
Awaitable navigation
NavigateAsync tells you how a navigation actually ended - succeeded,
cancelled, redirected, not found, failed or superseded - plus history entry state
and functional query updates.
Drop-in migration
@page discovery, the Found template, zero-template
authorization and layouts all keep the built-in Router's behavior - migrate
first, adopt features incrementally.
In practice
See it in code
Guard it - preventively
Guards run before the URL commits. Cancel or redirect and the address bar never moves - no flicker, no corrupted Back button, and real "unsaved changes" prompts become possible.
Guards & locks →<Broute Path="/admin" Guard="@CheckAdmin">…</Broute> private async ValueTask CheckAdmin(BrouterNavigationContext ctx) { if (await auth.IsAdminAsync(ctx.CancellationToken) is false) ctx.Redirect("/login"); // URL never committed }
Load data before render
A route's loader runs after guards pass, and its result cascades to the page
as a typed wrapper. Add a StaleTime and Back/Forward becomes
instant; add Preload on a link and the data is ready before the
click.
<Broute Path="/users/{id:int}" Loader="@LoadUser" StaleTime="@TimeSpan.FromMinutes(1)">…</Broute> <BrouterLink Href="/users/42" Preload="BrouterLinkPreload.Intent"> Saleh </BrouterLink>
Never ship a broken link
The optional source generator reads your @page directives and
<Broute> declarations and emits typed URL builders -
constraint types included, so {id:int} becomes an
int argument.
@* generated from: <Broute Name="counter" Path="/counter/{init:int}" /> *@ <BrouterLink Href="@BrouterRoutes.Counter(1234)">Counter</BrouterLink> brouter.Navigate(BrouterRoutes.ProfileByUsername("saleh", query: "tab=posts"));
Comparison
Why not the built-in Router?
Because the built-in Router stops at matching. Everything it does, Brouter does the same way - and then keeps going. Where the framework's implementation is the right one - authorization, layouts - Brouter composes it instead of replacing it.
| Capability | Built-in Router | Brouter |
|---|---|---|
| Route templates & constraints | Full grammar | Same grammar, identical matching semantics |
| Nested routes & outlets | - | Route trees, outlets, named views, pathless groups |
| Navigation guards | - | Async enter/leave guards + component-level locks, all preventive |
| Route data loading | Per-component OnParametersSetAsync | Loaders + SWR cache + preloading + deferred data + error boundaries |
| Navigation result | Fire-and-forget | Awaitable outcome (succeeded / cancelled / redirected / …) |
| View transitions | Hand-rolled JS | One option, direction-aware defaults, plain-CSS overrides |
| Keep-alive page state | - | Per-route or per-parameter retention with LRU eviction |
| Typed URLs | Strings | Source-generated BrouterRoutes builders |
| Scroll & focus management | FocusOnNavigate | Scroll-to-top, fragment scrolling, position restoration, focus-on-navigate |
@page components & [Authorize] | ✓ | ✓ unchanged - discovery, the Found template and the framework's own AuthorizeRouteView |
Where to start
Choose your path
New to Brouter?
Install, register, declare two routes - running in five minutes.
Getting started →On the built-in Router?
Your Found template ports verbatim; @page pages just keep working.
Want to see it live?
Every feature on this site has a clickable demo - this site is the router.
Open the playground →Building with an AI agent?
This site is an MCP server: point your agent at /mcp and it reads the shipped library instead of guessing.
Building something specific?
Protected areas, unsaved-changes prompts, query-state filters, breadcrumbs - answered as code.
Browse the recipes →Looking up a parameter?
Every component parameter, service member, option and value type, with its default.
API reference →Questions
Frequently asked
Does Brouter replace the built-in Blazor Router?
Yes - it is a drop-in replacement. Point AppAssembly at your app and
every @page component is discovered and matched with identical
template semantics; the built-in Router's <Found> /
<NotFound> / <Navigating> templates port over
as-is. See the migration guide.
Does it work with Server, WebAssembly and Auto render modes - and prerendering?
Yes. This site runs the same shared pages under all three hosts. Loader results captured during prerender can be persisted so they do not double-fetch, and on .NET 10 an unmatched URL during static SSR produces a real HTTP 404.
What about [Authorize] pages?
Set NotAuthorized / Authorizing on
<Brouter> and Brouter composes the framework's own
AuthorizeRouteView internally - authorization correctness stays
Microsoft's code, and native rendering fails closed on [Authorize]
components it cannot enforce.
Is there any JavaScript to set up?
No. Brouter brings its script along as a static web asset - there is no script tag to add and nothing to bundle. On browsers without the View Transitions API the animations are simply inert.
What does it cost at scale?
Every route is a live component instance: roughly 3-6 KB of retained memory each, so ~500 routes add about 2.5 MB and ~4 ms of startup. A first-segment index keeps per-navigation matching fast regardless. Details and a runnable benchmark are on the performance page.
More answers - including what to check when something does not behave as expected - on the FAQ & troubleshooting page.
Route your Blazor app the modern way
One package, one component, and every @page you already have.
dotnet add package Bit.Brouter