I added a genuinely public, [AllowAnonymous] landing page to a Blazor Server app on .NET 10. It rendered as a blank white screen. No error, no server-side exception, nothing in the logs. Just white.
The confusing part: it wasn’t only that page. Every page, for every anonymous user, went blank. The page that was specifically supposed to work without a login was somehow the most broken of all.
What the browser actually saw
Server-side logging showed nothing wrong, which meant the failure had to be client-side. F12 devtools console told a different story:
Access to fetch at 'https://login.microsoftonline.com/...' (redirected from '.../_blazor/initializers')
... has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header
Uncaught (in promise) TypeError: Failed to fetch at blazor.web.jsThat’s a CORS error against Microsoft’s own login endpoint, triggered by a request to /_blazor/initializers — a path I hadn’t written and hadn’t touched. It’s Blazor’s own internal framework machinery, part of how the SignalR circuit bootstraps itself.
The root cause
Program.cs had this, added at some earlier point for what seemed like a reasonable reason — require authentication everywhere by default, opt out per-page with [AllowAnonymous]:
builder.Services.AddAuthorization(options =>
{
options.FallbackPolicy = options.DefaultPolicy;
});The problem is where this policy applies. FallbackPolicy operates at the ASP.NET Core endpoint layer — every routed endpoint that doesn’t carry its own authorization metadata gets challenged. That’s not scoped to your Razor pages. It also catches Blazor’s own internal endpoints: /_blazor/initializers, the negotiate endpoint, the SignalR hub itself. None of those carry page-specific [AllowAnonymous] metadata, because they’re framework internals, not your routes.
So for an anonymous visitor, UseAuthorization() intercepts the framework’s own bootstrap request and issues a redirect to the Microsoft login page. But blazor.web.js calls that endpoint via fetch(), not a top-level browser navigation. A cross-origin redirect response to a fetch() call — with no CORS headers, exactly as you’d expect from an OIDC login endpoint that was never meant to be fetched — throws inside the JS runtime. The circuit bootstrap dies before it ever starts. Every page appears blank, because every page depends on that same bootstrap succeeding, and there’s zero server-side signal that anything went wrong: from the server’s point of view, it correctly issued a redirect exactly as configured.
I confirmed HTML/CSS/JS were all downloading fine and the container was healthy — the failure was purely client-side, and only visible via real browser devtools. A sandboxed browser automation tool’s own network capture missed it, since it doesn’t intercept in-page fetch() calls the same way a real Network tab does.
The fix
Move “require auth by default” out of the ASP.NET Core endpoint layer and into the Blazor router, which already has the right machinery for this:
1. Program.cs — drop the fallback policy entirely:
builder.Services.AddAuthorization();2. _Imports.razor — apply auth-by-default at the component level instead:
@using Microsoft.AspNetCore.Authorization
@attribute [Authorize]This routes enforcement through AuthorizeRouteView (already present in Routes.razor, wrapping <NotAuthorized><RedirectToLogin /></NotAuthorized>), which checks authorization inside the already-connected circuit — after the bootstrap has succeeded — rather than at the HTTP endpoint layer. It never touches /_blazor/*.
3. Any genuinely public page needs [AllowAnonymous] explicitly now, since the _Imports.razor attribute makes auth the default everywhere else.
One thing that turned out not to be the problem: RedirectToLogin.razor was already using Navigation.NavigateTo(..., forceLoad: true) — a real top-level browser navigation rather than a fetch. That’s the correct pattern for a login redirect and needed no changes at all.
The takeaway
If a Blazor Server app with a mix of public and authenticated pages ever shows a blank page with zero server-side error, check the browser console first for a CORS error mentioning /_blazor/ redirected to a login endpoint. That specific signature means something introduced a global endpoint-level fallback policy. Don’t set options.FallbackPolicy = options.DefaultPolicy (or any RequireAuthenticatedUser fallback) in an app that has even one [AllowAnonymous] page — put the default-auth requirement on _Imports.razor instead, so enforcement happens at the component layer where Blazor’s own routing already expects it.