Add Static Page Generation to Blazor WebAssembly

Introduction
Blazor WebAssembly is an interesting technology for building web applications that run .NET directly in the client browser without requiring a server-side application component. It is also a Single Page Application (SPA) technology, which means that it shares many of the same characteristics as applications built with frameworks such as React, Angular, and Vue.js.
One of those characteristics is how content is rendered. The HTML returned by the server typically contains the application shell, while the actual page content is created by the application after it has started in the browser.
That can be a problem when some pages contain public content that should be discoverable by search engines, social media crawlers, AI agents, and other bots. Crawlers that only download and inspect the static HTML never see content that exists only after the SPA has rendered it. Some crawlers can execute JavaScript and render the application, but many do not.
This is where adding static page generation to Blazor WebAssembly can help.
Blazorade Static Pages adds build-time static page generation to an existing Blazor WebAssembly application. Your application remains the source of truth, and you decide which pages and which parts of those pages should also be generated as static HTML.
There are several ways to solve the discoverability problem, but I wanted an approach that would work with an ordinary Blazor WebAssembly application. I did not want to build a separate static site, duplicate routes, or make static content the foundation of the application.
In this article, I will show you how Blazorade Static Pages works, how you can mix static and interactive content, and how to start using it in your own Blazor WebAssembly application.
What About Blazorade Scraibe Then?
I previously tried to solve the same problem with Blazorade Scraibe.
The main difference is the direction. Blazorade Scraibe is content-first: you start with static content and build a Blazor WebAssembly application around it. Blazorade Static Pages is application-first: you start with an existing Blazor WebAssembly application and add static page generation only where you need it.
I believe the second approach fits Blazor WebAssembly better. These are primarily interactive applications, so it feels more natural to add discoverable static content to an existing application than to build the application around static content.
For now, I have therefore abandoned the Blazorade Scraibe approach in favour of Blazorade Static Pages.
Getting Started With Blazorade Static Pages
Getting started is quite simple. First, add the Blazorade Static Pages NuGet package to your Blazor WebAssembly application.
dotnet add package Blazorade.StaticPages
Then pick a routable page that you want to make available as static HTML and mark it with the StaticPage attribute.
@page "/products"
@attribute [StaticPage]
<StaticMetadata
Title="Products"
Description="Explore our products." />
<StaticContent>
<h1>Products</h1>
<p>Browse our product catalogue.</p>
</StaticContent>
There are three important parts here. The normal @page directive still defines the route, just like in any other Blazor application. The StaticPage attribute tells Blazorade Static Pages that this route should participate in static generation. Finally, StaticMetadata defines the metadata for the page, while StaticContent defines the content that should be included in the generated HTML.
When you build the application, Blazorade Static Pages scans the Razor source files and finds routable components marked with the StaticPage attribute. It then analyzes their static metadata and content, generates HTML files, and copies those files to the application’s build output.
Pages marked with StaticPage are also included in the generated sitemap by default. If you have a static page that you do not want to include in the sitemap, you can disable that directly on the attribute.
@attribute [StaticPage(IncludeInSitemap = false)]
The sitemap itself is generated when you have configured the public site URL for the application. I will come back to that later when we look at metadata and configuration in more detail.
Pages that are not marked with StaticPage are simply ignored by the generator and continue to behave just like any other page in your Blazor WebAssembly application. This means that you can add static generation gradually, only to the pages where you actually need it.
Static and Interactive Content
A Blazor WebAssembly page does not have to be either static or interactive. With Blazorade Static Pages, you can decide which parts of a page should be included in the generated static HTML and which parts should only exist when the Blazor application is actually running in the browser.
The StaticContent component marks content that should be included in the generated static page.
<StaticContent>
<h1>Products</h1>
<p>Browse our product catalogue.</p>
</StaticContent>
The StaticContent component itself is transparent. It does not add any extra HTML markup at runtime, and its wrapper is not emitted into the generated HTML either. Only the content inside it is included in the generated page.
Sometimes you have content inside an otherwise static section that only makes sense when the Blazor application is running. This is where InteractiveContent comes in.
<StaticContent>
<h1>Products</h1>
<p>Browse our product catalogue.</p>
<InteractiveContent>
<ProductConfigurator />
</InteractiveContent>
</StaticContent>
The complete subtree inside InteractiveContent is excluded from the generated static HTML, but it renders normally when the Blazor application runs in the browser. This makes it possible to have crawler-visible content and normal Blazor interactivity on the same page without maintaining two separate implementations.
Reusable components can also participate in static generation. If a reusable component contains a StaticContent section, that section becomes the component’s static representation.
<StaticContent>
<section class="product-summary">
<h2>@Name</h2>
<p>Additional details are available interactively.</p>
</section>
</StaticContent>
<InteractiveContent>
<ProductEditor />
</InteractiveContent>
When a page uses this component, Blazorade Static Pages includes only the component’s StaticContent in the generated HTML. Ordinary markup outside StaticContent is not included automatically, and a reusable component without a static representation contributes no static content.
This explicit separation is intentional. Blazorade Static Pages does not try to execute arbitrary components and hope that their output can be turned into static HTML. Instead, you decide what is safe and meaningful to publish statically, while everything that depends on runtime behavior stays in the interactive application.
How Static Generation Works
Blazorade Static Pages generates static HTML during the normal build of your Blazor WebAssembly application. The generator runs after the application has been built, scans the Razor source files, and looks for routable components marked with the StaticPage attribute.
The important thing to understand is that Blazorade Static Pages does not run your application. It does not execute components, invoke lifecycle methods, resolve services, call JavaScript, or fetch runtime data. Instead, it analyzes the source code and builds the static output from values and markup that can be determined at build time.
This makes the generation process deterministic, but there is also a more fundamental reason for this restriction: we are talking about static content. Everything that is emitted into the generated HTML needs to be static in nature too.
Reusing Compile-Time Values
You do not have to duplicate the same text in several places just because the generator works from source. Blazorade Static Pages can resolve supported compile-time string values and reuse them both in metadata and in static content.
For instance, you can define the page title and description once and reference them from both StaticMetadata and StaticContent.
@page "/products"
@attribute [StaticPage]
@code {
private const string PageTitle = "Products";
private const string PageDescription = "Explore our products.";
}
<StaticMetadata
Title="@PageTitle"
Description="@PageDescription" />
<StaticContent>
<h1>@PageTitle</h1>
<p>@PageDescription</p>
</StaticContent>
The analyzer can also resolve supported constants from matching .razor.cs code-behind files and from standard project C# files, including qualified references such as @Constants.Author.
The key point is that these values must be constants. A const value cannot change after compilation, which makes it safe to use as part of generated static HTML. A normal field, even a static field, is still mutable and therefore not truly static in this sense.
That distinction fits the purpose of the library quite well. If the generated HTML is supposed to be a deterministic static artifact, the values used to produce it must also be deterministic and unchangeable at build time.
What Cannot Be Resolved at Build Time
Because the application is not executed, runtime values cannot be used for static metadata or static content.
That includes values that depend on things such as:
- Dependency-injected services.
- Lifecycle methods.
- Property getters.
- Authentication or user-specific state.
- Browser APIs.
- External data fetched at runtime.
- Mutable fields or variables whose value can change.
Unsupported expressions cause a build error instead of silently producing incomplete static output.
If some part of a page depends on runtime behavior, that content should stay in the normal Blazor application and be placed inside InteractiveContent where appropriate.
Generated HTML
Each generated page uses the application’s existing wwwroot/index.html as its template. Blazorade Static Pages replaces the contents of the normal Blazor application root with the extracted static content, updates the page title and metadata, and keeps the rest of the application shell intact.
This means that the generated document already contains meaningful HTML when it is downloaded, but it can still start the normal Blazor WebAssembly application afterwards. The static page is the baseline, and Blazor can enhance it with interactive functionality once the application is running in the browser.
Metadata and Configuration
Static HTML is only useful if the page also contains the metadata that search engines, social media platforms, and other crawlers expect. Blazorade Static Pages therefore generates metadata alongside the static page content.
Page-specific metadata is defined with the StaticMetadata component.
@page "/products"
@attribute [StaticPage]
<StaticMetadata
Title="Products"
Description="Explore our products."
Author="Mika Berglund"
Image="images/products.jpg"
Locale="en-US"
Date="2026-08-26" />
<StaticContent>
<h1>Products</h1>
<p>Browse our product catalogue.</p>
</StaticContent>
The Title parameter is required. The other metadata values are optional, but when supplied they also need to be compile-time-resolvable values, just like the static page content.
From these values, Blazorade Static Pages generates the normal page title together with metadata such as description, author, Open Graph values, Twitter card metadata, locale, publication date, and image information.
Configuring the Public Site URL
Some metadata cannot be generated from the page itself. Canonical URLs and sitemap entries, for instance, need to know the public address where the application will eventually be hosted.
You configure that in a blazorade.config.json file next to your Blazor WebAssembly project file.
{
"staticPages": {
"siteUrl": "https://www.example.com"
}
}
The configured siteUrl is combined with the normal Blazor route when generating canonical URLs.
For example:
https://www.example.com + /products = https://www.example.com/products
That URL is then used for metadata such as the canonical link and og:url.
Blazorade Static Pages does not try to derive this URL from the host where the application happens to run. That would not be reliable, because the same build could be served locally, in a preview environment, or from the final production site.
Sitemap Generation
A sitemap is particularly important when you generate static pages, because it gives crawlers a clear list of the routes you want them to discover.
When staticPages.siteUrl is configured, Blazorade Static Pages generates a sitemap.xml file containing the static pages in the application. Each sitemap URL is created from the configured site URL and the normal @page route.
Static pages are included in the sitemap by default. If you have a page that should still be generated as static HTML but should not be advertised through the sitemap, you can exclude it with the StaticPage attribute.
@attribute [StaticPage(IncludeInSitemap = false)]
This lets static generation and sitemap inclusion remain separate decisions. A page can be available as static HTML without necessarily being listed in sitemap.xml.
If siteUrl is not configured, the static pages are still generated, but canonical URLs and the sitemap are omitted.
Static Web Apps Route Configuration
Blazorade Static Pages is primarily designed to be hosted with Azure Static Web Apps. That is also why the library generates a staticwebapp.config.json file as part of the static output.
The generated configuration contains an explicit rewrite for each static route, mapping the normal Blazor route to the corresponding generated HTML file. For example, a request to:
/products
can be rewritten to:
/products.html
This means that a crawler requesting /products receives the generated static HTML directly instead of having to start and render the Blazor WebAssembly application first.
The generated configuration also adds a navigation fallback to /index.html for routes that are not backed by generated static pages. This preserves the normal SPA routing behaviour for the rest of the Blazor WebAssembly application. Static assets, framework files, the sitemap, and other known files are excluded from that fallback.
In other words, Azure Static Web Apps is not just an arbitrary hosting option that happens to work with Blazorade Static Pages. It is the primary hosting model the library is designed around. Static routes are served directly as generated HTML, while the rest of the application continues to behave like a normal Blazor WebAssembly SPA.
Configuration-Specific Settings
You can also override configuration based on the active MSBuild configuration.
For example:
blazorade.config.json blazorade.config.Debug.json blazorade.config.Release.json blazorade.config.Pre-Prod.json
Blazorade Static Pages first reads the default blazorade.config.json file and then merges values from the configuration-specific file on top of it. This means that you can use normal Visual Studio or MSBuild build configurations without introducing a separate environment mechanism just for Static Pages.
This is useful when different builds need different static generation settings while keeping the application itself unchanged.
Migrating My WordPress Blog to Blazorade Static Pages
My current blog is running on WordPress. It has worked well for me, but I am planning to migrate the site to a Blazor WebAssembly application that uses Blazorade Static Pages.
There are a couple of reasons for this. First, I will move the site into a technology stack where I feel much more at home. I spend a lot of my time working with .NET, Blazor, Azure, and related technologies, so maintaining the blog as a Blazor application feels like a natural fit.
The second reason is hosting. Blazor WebAssembly applications can be hosted as static files, which makes Azure Static Web Apps a very attractive option. Azure Static Web Apps starts with a free tier, so I expect the hosting costs for this blog to become significantly lower than they are today.
Blazorade Static Pages is an important part of that plan. The blog will still be a Blazor WebAssembly application, but the public article pages can be generated as static HTML during the build. That means search engines, crawlers, and other bots can access the article content without having to execute and render the Blazor application first.
I will write a separate article about the actual WordPress-to-Blazor migration once I have completed it. That article will cover the practical details of moving the content, routing, hosting, and everything else that turns out to be involved. When that article is available, I will update this article with a link to it.
Common Questions and Answers
Is Blazorade Static Pages a prerendering solution?
No. Blazorade Static Pages does not execute the Blazor application or render components at build time. It analyzes the Razor source files and generates static HTML from content that can be resolved directly from the source.
Does Blazorade Static Pages replace my Blazor WebAssembly application?
No. Your Blazor WebAssembly application remains the source of truth. Static Pages only adds generated HTML for the routes you explicitly mark with StaticPage.
Can a page contain both static and interactive content?
Yes. Use StaticContent for content that should be included in the generated HTML, and InteractiveContent for parts that should only be available when the Blazor application runs in the browser.
Can reusable components contribute static content?
Yes. A reusable component can expose a static representation by placing that content inside StaticContent. Only that explicitly declared static content is included in the generated page.
Can I use variables and runtime values in static content?
Only compile-time-resolvable constants and supported string expressions can be used. Runtime values, services, lifecycle state, property getters, and other mutable or dynamic values are not evaluated during static generation.
Are all Blazor pages automatically generated as static pages?
No. Only routable components marked with @attribute [StaticPage] are included in static generation. Other pages remain normal Blazor WebAssembly pages.
Does Blazorade Static Pages generate a sitemap?
Yes, when staticPages.siteUrl is configured. Static pages are included in sitemap.xml by default, but individual pages can be excluded with IncludeInSitemap = false.
Is Blazorade Static Pages designed for Azure Static Web Apps?
Primarily, yes. Blazorade Static Pages is designed with Azure Static Web Apps as its main hosting target, which is why it generates a staticwebapp.config.json file as part of the build output. That file contains route rewrites for generated static pages and a navigation fallback for the rest of the Blazor WebAssembly application.
Can I fetch data from an API during static generation?
No. The current generator does not execute application code or fetch runtime data. Content that depends on external data needs to remain runtime-only, or be represented through deterministic static content that is available at build time.
Summary
Blazorade Static Pages takes an application-first approach to static page generation. You keep building your Blazor WebAssembly application the way you normally would, and add static generation only to the pages and content that need to be discoverable outside the running application.
The generated HTML is created during the build without executing the application. Static content, metadata, sitemap entries, and Azure Static Web Apps routing configuration are all produced from information that can be resolved deterministically from the source.
For me, this approach fits Blazor WebAssembly much better than starting from static content and building an application around it. Blazor WebAssembly is primarily an application technology, and Blazorade Static Pages adds static content capabilities without changing that model.
I am also planning to use Blazorade Static Pages when I migrate this blog from WordPress to Blazor WebAssembly. I will cover that migration in a separate article once it is complete and update this article with a link to it.
0 Comments