Blazor webassembly hosted in sub folder

Viewed 881

I have a quite complicated existing asp.net core site with an SPA. This SPA uses # for routing, so all of the existing razor pages and API endpoints just work.

I first tried to host just using app.UseBlazorFrameworkFiles(), but all sorts of things break with this.

Then I tried to put it in a sub folder: app.UseBlazorFrameworkFiles("/UI"), updated the main Index.cshtml to redirect to /UI/ and serve the appropriate HTML there in /Areas/UI/Pages/Index.cshtml and added <StaticWebAssetBasePath>UI</StaticWebAssetBasePath> and hacked the AddHttpClient to use baseaddress of the whole site.

This works... Except when it doesn't, in particularly when using a url to a subpage in blazor or a from blazor navigating to a route that doesn't exists. This will end up visiting the hosted site and serve my 404 instead.

I then tried various variations of

app.MapWhen(ctx => ctx.Request.Path.StartsWithSegments("/UI/"), blazor => {
   blazor.UseEndpoints(endpoints => {
      endpoints.MapFallbackToAreaPage("/UI/{*path:nonfile}", "/", "UI");
   });
});

Result: InvalidOperationException: Cannot find the fallback endpoint specified by route values: { page: /, area: UI }.

app.MapFallbackToAreaPage("/", "UI");

Result: InvalidOperationException: Cannot find the fallback endpoint specified by route values: { page: /, area: UI }.

app.MapFallbackToAreaPage("/Index", "UI");

Result: AmbiguousMatchException: The request matched multiple endpoints. Matches: /Index /Index

or any other sorts of variations I could come up with, all with the result of either a) blows up the existing configuration or b) blows up at startup or c) blows up when visting a page that doesn't exist.

Help please. How do I make this blazor hosted on my site?

3 Answers

I can't be sure here - it's not easy building a picture from some code snippets - but you are probably missing the <base href=....> setup in index.html

There's an StackOverflow answer, repo and demo I put together recently for a very similar question - this one on hosting multiple SPAs on the same site.

Create a multiple WebAssembly projects in a single solution

Here's an example index.html:

<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
    <title>Blazr.Medusa.Grey</title>
    <base href="/grey/" />
    <link href="/grey/css/bootstrap/bootstrap.min.css" rel="stylesheet" />
    <link href="/grey/css/app.css" rel="stylesheet" />
    <link href="Blazr.Medusa.Grey.styles.css" rel="stylesheet" />
</head>

Project File:

<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">

    <PropertyGroup>
        <TargetFramework>net6.0</TargetFramework>
        <Nullable>enable</Nullable>
        <ImplicitUsings>enable</ImplicitUsings>
        <StaticWebAssetBasePath>grey</StaticWebAssetBasePath>
    </PropertyGroup>

    <ItemGroup>
        <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="6.0.2" />
        <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="6.0.2" PrivateAssets="all" />
    </ItemGroup>

    <ItemGroup>
      <ProjectReference Include="..\Blazr.Medusa.SPA\Blazr.Medusa.SPA.csproj" />
    </ItemGroup>

</Project>

And Web Site program setup section:

app.MapWhen(ctx => ctx.Request.Path.StartsWithSegments("/grey"), app1 =>
{
    app1.UseBlazorFrameworkFiles("/grey");
    app1.UseRouting();
    app1.UseEndpoints(endpoints =>
    {
        endpoints.MapFallbackToFile("/grey/{*path:nonfile}", "/grey/index.html");
    });
});

Addition

Here's my full Program to show the middleware order:

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddRazorPages();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.MapWhen(ctx => ctx.Request.Path.StartsWithSegments("/grey"), app1 =>
{
    app1.UseBlazorFrameworkFiles("/grey");
    app1.UseRouting();
    app1.UseEndpoints(endpoints =>
    {
        endpoints.MapFallbackToFile("/grey/{*path:nonfile}", "/grey/index.html");
    });
});

app.MapWhen(ctx => ctx.Request.Path.StartsWithSegments("/green"), app1 =>
{
    app1.UseBlazorFrameworkFiles("/green");
    app1.UseRouting();
    app1.UseEndpoints(endpoints =>
    {
        endpoints.MapFallbackToFile("/green/{*path:nonfile}", "/green/index.html");
    });
});

app.MapWhen(ctx => ctx.Request.Path.StartsWithSegments("/purple"), app1 =>
{
    app1.UseBlazorFrameworkFiles("/purple");
    app1.UseRouting();
    app1.UseEndpoints(endpoints =>
    {
        endpoints.MapFallbackToFile("/purple/{*path:nonfile}", "/purple/index.html");
    });
});

app.UseBlazorFrameworkFiles("");

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();


app.MapFallbackToFile("/index.html");

app.Run();

Any questions - ask.

TLDR; Fix it at the start of pipeline, not at the end (with mapfallback)

I was able to find a solution simply by hacking the pipeline and rewriting the request.

app.UseHttpsRedirection(); //after this

//host blazor in this folder
app.UseBlazorFrameworkFiles("/UI");
app.Use((ctx, next) => {
   if (ctx.Request.Path.StartsWithSegments("/UI", out var rest) && !rest.StartsWithSegments("/"))
   {
      //detected paths that needs to be routed by Blazor and not server
      ctx.Request.Path = new PathString("/UI/");
      return next();
   }
   return next();

});

If this is placed after UseHttpsRedirection and before everything else (can be after UseStaticFiles also if the new path is a non-file, like a razor page), it will rewrite EVERYTHING like /UI/* into /UI/.

I created a standard Blazor WASM hosted solution [Client/Server/Shared].

It built and ran OK in Visual studio but when I published it to a subfolder on my website it failed to load.

I finally identified that the problem was with the <base href="/" /> in the Client index.html.

The subfolder was named the same as the solution [BlazorWASMHosted2022] so I changed the tag to <base href="/BlazorWASMHosted2022/" /> and published the solution again and all worked ok.

From what I have googled it appears that this is a problem with Blazor WASM.

So I have come up with a script added into index.html that tests the current URL and sets the base.href accordingly which works OK - here is my sample (with some info displayed while loading):

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
    <title>BlazorWASMHosted2022</title>

    <script type="text/javascript">
        var base = document.createElement('base');
        var currentUrl = window.location.href;
        var localHostUrl = 'https://localhost';
        if (currentUrl.startsWith(localHostUrl))
        {
            base.href = '/';
        } else {
            base.href = '/BlazorWASMHosted2022/';
        }
        document.getElementsByTagName('head')[0].appendChild(base);
    </script>

    <link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" />
    <link href="css/app.css" rel="stylesheet" />
    <link href="BlazorWASMHosted2022.Client.styles.css" rel="stylesheet" />
</head>

<body>
    <div id="app">Loading...</div>
    <div id="blazor-error-ui">
        An unhandled error has occurred.
        <a href="" class="reload">Reload</a>
        <a class="dismiss"></a>
    </div>

    <script type="text/javascript">
        document.write("location.pathname:- <br/>");
        document.write(window.location.href);
        document.write("<br/>");
        document.write("base.href:- <br/>");
        document.write(base.href);
    </script>
    <script src="_framework/blazor.webassembly.js"></script> 
</body>
</html>
Related