<?xml version="1.0" encoding="utf-8"?>
<rss xmlns:a10="http://www.w3.org/2005/Atom" version="2.0">
  <channel xmlns:media="http://search.yahoo.com/mrss/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <title>ABP.IO Stories</title>
    <link>https://abp.io/community/articles</link>
    <description>A hub for ABP Framework, .NET, and software development. Access articles, tutorials, news, and contribute to the ABP community.</description>
    <lastBuildDate>Thu, 22 Oct 2026 08:16:19 Z</lastBuildDate>
    <generator>Community - ABP.IO</generator>
    <image>
      <url>https://abp.io/assets/favicon.ico/favicon-32x32.png</url>
      <title>ABP.IO Stories</title>
      <link>https://abp.io/community/articles</link>
    </image>
    <a10:link rel="self" type="application/rss+xml" title="self" href="https://abp.io/community/rss?member=Xeevis" />
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/prerendering-blazor-wasm-application-with-abp-6.x-2v8590g3</guid>
      <link>https://abp.io/community/posts/prerendering-blazor-wasm-application-with-abp-6.x-2v8590g3</link>
      <a10:author>
        <a10:name>Xeevis</a10:name>
        <a10:uri>https://abp.io/community/members/Xeevis</a10:uri>
      </a10:author>
      <category>blazor</category>
      <category>wasm-prerendering</category>
      <category>prerendering</category>
      <title>Prerendering Blazor Wasm application with ABP 6.x</title>
      <description>A step-by-step guide on how to implement Blazor WebAssembly prerendering in ABP solution.</description>
      <pubDate>Wed, 07 Sep 2022 16:02:37 Z</pubDate>
      <a10:updated>2026-10-22T06:54:29Z</a10:updated>
      <content:encoded><![CDATA[<h1>Prerendering your Blazor Wasm application with ABP 6.x</h1>
<h2>Introduction</h2>
<p>In this article, we will demonstrate how to create basic hosted monolith <strong>Blazor WebAssembly application</strong> that will be prerendered by the server.</p>
<p>Prerendering has following advantages:</p>
<ul>
<li>💚 Instant initial load of page content while WebAssembly application downloads and hydrates in the background</li>
<li>💚 Improved scores in Chromium Lighthouse</li>
<li>💚 Static markup analysis by search engines (SEO) and tools that don't render WebAssembly apps</li>
</ul>
<p>Prerendering also has some drawbacks:</p>
<ul>
<li>🛑 You can no longer host your app on static file hosting</li>
<li>🛑 Different approach is required, no JS Interop calls in <code>OnInitialized()</code>, may be a problem with 3rd party components that use JS, but didn't account for non-browser rendering.</li>
<li>🛑 Double rendering, once on server, once on client. Takes extra steps to preserve state in order to avoid duplicate calls and content swap.</li>
<li>🛑 Server rendering doesn't support authentication, so it works only on public pages. For best experience, parts of content specific to unauthenticated/authenticated state should use placeholders until it's decided which one it will be.</li>
</ul>
<blockquote>
<p>⚠ <strong>Warning: This isn't production ready solution, it's a rudimentary starter guide on how to make prerendering work. Expect  problems.</strong></p>
</blockquote>
<h3>Source Code</h3>
<p>Source code for the final application is <a href="https://github.com/Xeevis/Acme.BookStore.WasmPrerendered">available on GitHub</a>.</p>
<h3>Screenshot</h3>
<p><img src="https://user-images.githubusercontent.com/5835044/188908662-e42d9430-4a1b-41b4-9552-514ada10de89.png" alt="image" /> </p>
<h2>Requirements</h2>
<p>The following tools are needed to be able to run the solution.</p>
<ul>
<li>.NET 6.0 SDK</li>
<li>ABP CLI</li>
<li>Compatible database engine</li>
</ul>
<h2>Step by step guide</h2>
<h3>Preparing the solution</h3>
<ol>
<li>Use the following ABP CLI command to create Blazor WebAssembly app:</li>
</ol>
<pre><code class="language-ps">abp new Acme.BookStore.WasmPrerendered -u blazor -d mongodb --create-solution-folder --theme basic --preview
</code></pre>
<pre><code class="language-ps">cd Acme.BookStore.WasmPrerendered
</code></pre>
<ol start="2">
<li>Reference <code>Blazor</code> project in project <code>HttpApi.Host</code></li>
</ol>
<pre><code class="language-ps">dotnet add src/Acme.BookStore.WasmPrerendered.HttpApi.Host reference src/Acme.BookStore.WasmPrerendered.Blazor
</code></pre>
<blockquote>
<p>ℹ For simplicity we are reusing <code>HttpApi.Host</code> project to have just a single point of entry to run, but nothing stops you from creating separate project to decouple client (+ host) &amp; server API like it's done by default.</p>
</blockquote>
<ol start="3">
<li>While this step isn't absolutely necessary, when using IDE like <strong>Visual Studio Code</strong> it's better to build solution for IntelliSense to work correctly.</li>
</ol>
<pre><code class="language-ps">dotnet build
# Open solution in Visual Studio Code
code .
</code></pre>
<h3>Adding the prerendering</h3>
<ol start="4">
<li>Move and rename Blazor's index.html file to our host (<code>Pages</code> folder needs to be created first)</li>
</ol>
<pre><code class="language-diff">- src/Acme.BookStore.WasmPrerendered.Blazor/wwwroot/index.html
+ src/Acme.BookStore.WasmPrerendered.HttpApi.Host/Pages/_Layout.cshtml
</code></pre>
<ol start="5">
<li>On top of this <code>_Layout.cshtml</code> file prepend</li>
</ol>
<pre><code class="language-diff">+ @using Microsoft.AspNetCore.Components.Web
+ @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
</code></pre>
<ol start="6">
<li>Inside <code>&lt;head&gt;</code> element we can replace static tag <code>&lt;title&gt;</code> with new dynamic component that will prerender title and metatags coming from pages</li>
</ol>
<pre><code class="language-diff">- &lt;title&gt;Acme.BookStore.WasmPrerendered.Blazor&lt;/title&gt;
+ &lt;component type=&quot;typeof(HeadOutlet)&quot; render-mode=&quot;WebAssemblyPrerendered&quot; /&gt;
</code></pre>
<ol start="7">
<li>Last thing to do in this file is to replace loading animation which won't be needed anymore</li>
</ol>
<pre><code class="language-diff">- &lt;div id=&quot;ApplicationContainer&quot;&gt;...&lt;/div&gt;
+ @RenderBody()
</code></pre>
<ol start="8">
<li>Next to <code>_Layout.cshtml</code> create new Razor page <code>_Host.cshtml</code> that will use _Layout and prerender the application</li>
</ol>
<pre><code class="language-cshtml">@page
@using Volo.Abp.AspNetCore.Components.Web.BasicTheme.Themes.Basic
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@{
    Layout = &quot;_Layout&quot;;
}

@if (HttpContext.Request.Path.StartsWithSegments(&quot;/authentication&quot;))
{
    &lt;component type=&quot;typeof(App)&quot; render-mode=&quot;WebAssembly&quot; /&gt;
}
else
{
    &lt;component type=&quot;typeof(App)&quot; render-mode=&quot;WebAssemblyPrerendered&quot; /&gt;
    &lt;persist-component-state /&gt;
}
</code></pre>
<blockquote>
<p>Tag helper <code>&lt;persist-component-state /&gt;</code> allows us to persist state from first render on server to second render in client preventing duplicate calls and content swap. This doesn't work automatically and has to be implemented for every component separately.</p>
</blockquote>
<ol start="9">
<li>Open <code>src\Acme.BookStore.WasmPrerendered.HttpApi.Host\WasmPrerenderedHttpApiHostModule.cs</code> and to the botom of <code>OnApplicationInitialization</code> append</li>
</ol>
<pre><code class="language-diff">    app.UseAbpSerilogEnrichers();
    app.UseConfiguredEndpoints();
+   ((WebApplication)app).MapFallbackToPage(&quot;/_Host&quot;);
}
</code></pre>
<ol start="10">
<li>To use <code>_Host</code> instead of swagger delete the controller</li>
</ol>
<pre><code class="language-diff">- src\Acme.BookStore.WasmPrerendered.HttpApi.Host\Controllers\HomeController.cs
</code></pre>
<ol start="11">
<li>Since we are using <code>HttpApi.Host</code> project to serve our Blazor application we are interiting its port. To avoid problems with authentication in <code>src\Acme.BookStore.WasmPrerendered.DbMigrator\appsettings.json</code> change <code>WasmPrerendered_Blazor</code> RootUrl to match <code>WasmPrerendered_Swagger</code>.</li>
</ol>
<pre><code class="language-diff">&quot;WasmPrerendered_Blazor&quot;: {
  &quot;ClientId&quot;: &quot;WasmPrerendered_Blazor&quot;,
- &quot;RootUrl&quot;: &quot;https://localhost:44351&quot;
+ &quot;RootUrl&quot;: &quot;https://localhost:44314&quot;
},
&quot;WasmPrerendered_Swagger&quot;: {
  &quot;ClientId&quot;: &quot;WasmPrerendered_Swagger&quot;,
  &quot;RootUrl&quot;: &quot;https://localhost:44314&quot;
}
</code></pre>
<ol start="12">
<li><p>We can now migrate our database, run <code>Acme.BookStore.WasmPrerendered.DbMigrator</code> project</p>
</li>
<li><p>Now is a good time to run our project to see if everything compiles and host is actually at least trying to prerender our WebAssembly application, don't worry error is to be expected.</p>
</li>
</ol>
<pre><code class="language-ps">dotnet run --project src\Acme.BookStore.WasmPrerendered.HttpApi.Host
</code></pre>
<blockquote>
<p>⚠ Please take note that you should always run the <code>HttpApi.Host</code> project and never the <code>Blazor</code> project!</p>
</blockquote>
<h3>Fixing the errors</h3>
<ul>
<li>Right off the bat we should get DI exception</li>
</ul>
<blockquote>
<p>There is no registered service of type 'Microsoft.AspNetCore.Components.Authorization.AuthenticationStateProvider'.</p>
</blockquote>
<p>Let's fix that, in file <code>WasmPrerenderedHttpApiHostModule.cs</code> under <code>ConfigureServices</code> method we need to append</p>
<pre><code class="language-diff">+ context.Services.AddScoped&lt;Microsoft.AspNetCore.Components.Authorization.AuthenticationStateProvider, Microsoft.AspNetCore.Components.Server.ServerAuthenticationStateProvider&gt;();
</code></pre>
<ol start="14">
<li>Next in line is router problem</li>
</ol>
<blockquote>
<p>The Router component requires a value for the parameter AppAssembly.</p>
</blockquote>
<p>Same file, same method as in previous step</p>
<pre><code class="language-diff">+ Configure&lt;Volo.Abp.AspNetCore.Components.Web.Theming.Routing.AbpRouterOptions&gt;(options =&gt; options.AppAssembly = typeof(Blazor.WasmPrerenderedBlazorModule).Assembly);
</code></pre>
<ol start="15">
<li>Now it's another DI exception</li>
</ol>
<blockquote>
<p>There is no registered service of type 'Volo.Abp.AspNetCore.Components.Web.Theming.Toolbars.IToolbarManager'.</p>
</blockquote>
<p>We can solve this with dependency</p>
<pre><code class="language-diff">[DependsOn(
    (...)
    typeof(AbpSwashbuckleModule),
+   typeof(Volo.Abp.AspNetCore.Components.Web.Theming.AbpAspNetCoreComponentsWebThemingModule)
)]
public class WasmPrerenderedHttpApiHostModule : AbpModule
</code></pre>
<ol start="16">
<li>Again ... DI exception</li>
</ol>
<blockquote>
<p>InvalidOperationException: Cannot provide a value for property 'ClassProvider' on type 'Blazorise.Badge'. There is no registered service of type 'Blazorise.IClassProvider'.</p>
</blockquote>
<p>This is another one for file <code>WasmPrerenderedHttpApiHostModule.cs</code> under <code>ConfigureServices</code></p>
<pre><code class="language-cs">context.Services
  .AddBootstrap5Providers()
  .AddFontAwesomeIcons();
</code></pre>
<p>Don't forget the usings</p>
<pre><code class="language-cs">using Blazorise.Bootstrap5;
using Blazorise.Icons.FontAwesome;
</code></pre>
<blockquote>
<p>⚠ Please note this simplification is duplicating logic from Blazor app, you should definitely create shared code both <code>Blazor</code> and <code>HttpApi.Host</code> will use, otherwise you'll have to make changes in 2 places, asking for trouble.</p>
</blockquote>
<ul>
<li>Hooray! We should be able to prerender some markup now. But if you run the app now, you'll get flooded by integrity errors and app won't hydrate. It's still far from over.</li>
</ul>
<hr />
<p>Before continuing we can do a simple trick that will tell us if we are looking at prerendered markup or at fully loaded WebAssembly app without devtools.</p>
<p>For <code>HttpApi.Host</code> we can change <code>WasmPrerenderedBrandingProvider.cs</code> to include message about loading</p>
<pre><code class="language-cs">public override string AppName =&gt; &quot;🔁 Wasm is loading ..&quot;;
</code></pre>
<p>While for <code>Blazor</code> we can change <code>WasmPrerenderedBrandingProvider.cs</code> to include message that it finished</p>
<pre><code class="language-cs">public override string AppName =&gt; &quot;✅ Wasm has loaded!&quot;;
</code></pre>
<hr />
<ol start="17">
<li>To fix integrity errors we need a package <code>Microsoft.AspNetCore.Components.WebAssembly.Server</code> on our <code>HttpApi.Host</code> project</li>
</ol>
<pre><code class="language-ps">dotnet add src/Acme.BookStore.WasmPrerendered.HttpApi.Host package Microsoft.AspNetCore.Components.WebAssembly.Server
</code></pre>
<p>this in turn lets us append new line in <code>WasmPrerenderedHttpApiHostModule.cs</code></p>
<pre><code class="language-diff">app.UseCorrelationId();
+ app.UseBlazorFrameworkFiles();
app.UseStaticFiles();
</code></pre>
<ol start="18">
<li>Next error I must admin I'm not sure why is happening</li>
</ol>
<blockquote>
<p>'Volo.Abp.AspNetCore.Components.Web.BasicTheme.Themes.Basic.App' could not be found in the assembly 'Volo.Abp.AspNetCore.Components.Web.BasicTheme'. This is likely a result of trimming (tree shaking).)</p>
</blockquote>
<p>I could swear it's there, until someone figures what is happening we can copy this <code>App.razor</code> to our <code>Blazor</code> project, most people will probably override it anyway.</p>
<pre><code class="language-razor">@using Microsoft.Extensions.Options
@using Volo.Abp.AspNetCore.Components.Web.Theming.Routing
@using Volo.Abp.AspNetCore.Components.Web.BasicTheme.Themes.Basic
@inject IOptions&lt;AbpRouterOptions&gt; RouterOptions

&lt;CascadingAuthenticationState&gt;
    &lt;Router AppAssembly=&quot;RouterOptions.Value.AppAssembly&quot;
            AdditionalAssemblies=&quot;RouterOptions.Value.AdditionalAssemblies&quot;&gt;
        &lt;Found Context=&quot;routeData&quot;&gt;
            &lt;AuthorizeRouteView RouteData=&quot;@routeData&quot; DefaultLayout=&quot;@typeof(MainLayout)&quot;&gt;
                &lt;NotAuthorized&gt;
                    @if (!context.User.Identity.IsAuthenticated)
                    {
                        &lt;RedirectToLogin /&gt;
                    }
                    else
                    {
                        &lt;p&gt;You are not authorized to access this resource.&lt;/p&gt;
                    }
                &lt;/NotAuthorized&gt;
            &lt;/AuthorizeRouteView&gt;
        &lt;/Found&gt;
        &lt;NotFound&gt;
            &lt;LayoutView Layout=&quot;@typeof(MainLayout)&quot;&gt;
                &lt;p&gt;Sorry, there's nothing at this address.&lt;/p&gt;
            &lt;/LayoutView&gt;
        &lt;/NotFound&gt;
    &lt;/Router&gt;
&lt;/CascadingAuthenticationState&gt;
</code></pre>
<p>In <code>HttpApi.Host/Pages/_Host.cshtml</code> replace the using to use our custom <code>App.razor</code></p>
<pre><code class="language-diff">- @using Volo.Abp.AspNetCore.Components.Web.BasicTheme.Themes.Basic
+ @using Acme.BookStore.WasmPrerendered.Blazor
</code></pre>
<ol start="19">
<li>Wasm is loading now! But hey there's still one last error to solve</li>
</ol>
<p>In <code>Blazor/WasmPrerenderedBlazorModule.cs</code> we can safely remove</p>
<pre><code class="language-diff">- builder.RootComponents.Add&lt;App&gt;(&quot;#ApplicationContainer&quot;);
</code></pre>
<h3>✅ In this state prerendering should be pretty stable with no errors. You can even authenticate! Try looking at static page source (ctrl + u).</h3>
<blockquote>
<p>Sadly this isn't end of the road, toolbars don't preprerender correctly, you can't manage your account and when you switch language, prerendered and client language will be different.</p>
</blockquote>
<h2>TODO: To be continued ...</h2>
<p>Do you find this article useful? Would you like to see more like it? Coffee helps me stay awake at night to write for you. ❤</p>
<p><a href="https://ko-fi.com/xeevis"><img src="https://user-images.githubusercontent.com/5835044/188901166-972fef50-2aea-4720-86b4-01eb71ad9079.png" width="256"/> </a></p>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/b64713b5-b552-a372-3fd3-3a062add3fe9" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/b64713b5-b552-a372-3fd3-3a062add3fe9" medium="image" />
    </item>
  </channel>
</rss>