DEV Community

loading...
Cover image for Pre-render Blazor WebAssembly on static web hosting at publishing time

Pre-render Blazor WebAssembly on static web hosting at publishing time

jsakamoto
Microsoft MVP for Visual Studio and Development Tech. (prefer C#, .NET Core, ASP.NET Core, Azure Web Apps, TypeScript, and Blazor WebAssembly App!)
・8 min read

Introduction

In this article, I'll explain about prerendering a Blazor WebAssembly app hosted on the static web hosting and save them to static HTML files at publishing time.

But first, let me introduce the "Awesome Blazor Browser" website.

Here is the "Awesome Blazor Browser" website.

I created and published this website on the Internet.

Please let me explain about what the "Awesome Blazor Browser" is, and why I created it.
This will be helpful to you to understand the background and requirement of statically prerendering of a Blazor Wasm app.

Maybe you know, there are so many "Awesome Something" sites. Those sites are related to computer programing collections of "Awesome" stuff.

Of course, there is an "Awesome" site about Blazor programming, too.
It is the "Awesome Blazor" GitHub repository.

GitHub logo AdrienTorris / awesome-blazor

Resources for Blazor, a .NET web framework using C#/Razor and HTML that runs in the browser with WebAssembly.

Awesome Blazor Awesome

A collection of awesome Blazor resources.

Blazor is a .NET web framework using C#/Razor and HTML that runs in the browser with WebAssembly.

Contributions are always welcome! Please take a look at the contribution guidelines pages first. Thanks to all contributors, you're awesome and wouldn't be possible without you!

If you need to search on this list you can try this great website: Awesome Blazor Browser Thanks @jsakamoto for this! Source code stars last commit.

Blazor Day 2021 (June 17, 2021).

  • Free online conf about Blazor. After hitting more than 10K live viewers for its 1st edition, BlazorDay comes back. With more Blazor rock stars, even more fun and more ... Blazor love.
  • blazorday.net.

Contents

Introduction

What is Blazor?

Blazor is a .NET web framework to build…

This is an excellent work of Adrien Torris.
(Special thanks to Adrian! 👍)
And I often visited the "Awesome Blazor" site to get helpful software or how to.

But, one day, I ran into one problem.
The "Awesome Blazor" items were getting enormous day by day, so browsing the site was getting harder.
For example, it became hard to find the section I want to see from so many sections on the site at a glance.

This is the motivative I created the "Awesome Blazor Browser" site.

The "Awesome Blazor Browser" is a dedicated web app for browsing the "Awesome Blazor" site contents.
It fetches the README content from the "Awesome Blazor" GitHub repository, parses it, and shows it on user-friendly navigation UI.
The "Awesome Blazor Browser" allows us to filter the contents with keywords or filter particular sections that I want to see or jump to the section directly.

The "Awesome Blazor Browser" is a Blazor WebAssembly app, and I deployed it on GitHub Pages.
I made the publishing task to be automated as a GitHub Actions script.

The "Awesome Blazor Browser" is very useful, at least for me, because the project started from my requirement. 😁

But it had a problem.

The problem is, the Internet search results of the "Awesome Blazor Browser" were really bad!
image

If I hosted the "Awesome Blazor Browser" on an ASP.NET Core server, I could resolve this problem easily using the usual server-side prerendering technic.

But I strongly wanted to host it on GitHub Pages because this is a good showcase that a Blazor Wasm app can be hosted even a static web hosting.

So I have to do a prerendering the Blazor Wasm app at publishing time and save them to static HTML files into the publish folder.

Of course, I have to prerender to static HTML files not only a root index content but also all other routed URLs.

And also, yes, I have to do a prerendering task in a GitHub Actions script.

Solutions

To resolve this problem, at first, I researched to find anything on the Internet that tools or articles about doing publishing-time prerendering of a Blazor Wasm app.

Fortunately, I could find some good resources that already exist on the Internet.
Let me show two resources that I found.

react-snap

One of those is the article by Swimberg.

swimburger image

He explains in his blog post about how to prerender using "react-snap".
The "react-snap" is a NodeJS tool.
This tool launches an owned local webserver to serve the SPA published folder via HTTP.
And it launches a headless Chromium browser.
Then, the "react-snap" starts to access its own webserver by that headless Chromium browser.
At last, the "react-snap" gets the DOM contents the browser process rendered and saves that contents into static HTML files.

This approach is straightforward and also robust, I think.
The Blazor Wasm project side doesn't require any code change.

Create prerendering C# host

Another one I will show you is the article by Andrew Lock.

He explains in his blog post how to prerender by adding an ASP.NET host project.
His approach is based on a well-known technic to do serverside prerendering in a Blazor Wasm app.

To do this, he added a new ASP.NET Core host project, and he configured the host to prerender the Blazor Wasm app.

Finally, when the host program is launched at publishing time, the host program crawls itself via HttpClient and saves the fetched content to static HTML files.

I feel this approach is also nice because this technique is based on a well-known method in ASP.NET Core programming.

Disadvantage of those ways

But I could not be satisfied with those solutions.

Swimberge's approach using the "react-snap" is almost good, but it can't wait to finish the "OnInitializedAsync" method on a Blazor Wasm side.
This caused saving the intermediate rendering result sometimes on my projects.

And also, the results that the "react-snap" outputs must rewrite because the results are not valid SPA bootstrap content.
The reason is the state of contents the "react-snap" saved is after the browser process did rendering, not before the browser process started.

Andrew's approach using the ASP.NET Core hosting is also working fine, but we must write massive and complicated C# code to do it.

Additionally, those C# code is locked into individual Blazor Wasm project tightly.
This means resuing those C # codes for another project is really hard without so many code changes.

So finally, one day, I decided to start a new software project to resolve my dissatisfaction.

My new project

Let me explain about my approach and goal for statically prerender a Blazor Wasm in my way.

First, it is based on standard server-side prerendering that is hosted on an ASP.NET Core server.
This is required to avoid the async initialization problem.

Second, make the code changes of the Blazor Wasm side to be nothing or minimum.
This goal is important to easy to use my achievement for anybody.

Third, it should be packaged as a NuGet package, and it should be distributed on "nuget.org".
This means anybody can use my achievement out-of-the-box.

"BlazorWasmPreRendering.Build"

A few days after I started, finally, I published my achievement as a NuGet package.
The package is "BlazorWasmPreRendering.Build".

Only you need to do to statically prerendering at publishing-time is, just adding this package to your Blazor Wasm project.

I'll show you this package how it affects the result of publishing a Blazor Wasm project.

The following picture is the results of publishing a Blazor Wasm project.

image

As you can see, the "index.html" SPA fallback page just contains the "Loading" message.
This is a normal result as I expected.
There is nothing unusual.

The next picture as follow is the results of publishing a Blazor Wasm project after the "BlazorWasmPreRender.Build" package was added.

image

As you can see, now the "index.html" contains the rendered HTML by Blazor component.
And static HTML files correspond to each route URL are also generated.

Again, this happened by just adding the "BlazorWasmPreRender.Build" package to a Blazor Wasm app.

Therefore, you don't need to change the GitHub action script.

How does it work

Then, I'll explain about how does the "BlazorWasmPreRendering.Build" package work.

1st. Initialization

After the typical publishing process is finished, the "BlazorWasmPreRender.Build" starts its works.

As you can see the picture below, it reads the "index.html" file from the publish folder and use it to create the SPA fallback Razor page on the ASP.NET Core server inside it.

image

And also, the server-side rendering code is inserted into that SPA fallback Razor page.

image

The Blazor Wasm DLL file is also loaded, and the root component class inside it, typically the "App" class, is referenced from the server-side prerendering code.

image

2nd. Crawling

After these steps, it starts crawling to itself.

First, the crawler instance inside it sends an HTTP GET request for root URL to the web server instance inside itself.

Then, the server instance executes rendering and responds the rendering result to the crawler.

When the crawler receives the result, then the crawler saves that result to the corresponding "index.html" file statically.

image

Next, the crawler instance tries to find any links inside that result.

If the crawler finds links, the crawler sends an HTTP GET request for each link and saves them to static HTML files.

image

The crawler does this process recursively.

These processes are executed just before the end of dotnet publish command.

Notice

But of course, it is not perfect.

Service registration is never invoked!

The most important thing, the "BlazorWasmPreRendering.Build" can't invoke the "Main" method of the Blazor Wasm app side because of the limitation of its architecture on this approach.

This means the prerendering process will crash when any Blazor components request to inject any services that will be registered at the "Main" method to the DI container.

Because those services never registered in the server-side process due to the "Main" method can't be invoked.

image

To avoid this, I implemented a hook point in the "BlazorWasmPreRender.Build".

What you have to do to avoid this problem is extract service registrations to a static method named "ConfigureServices".

image

If the "BlazorWasmPreRender.Build" finds the static method named "ConfigureServices", it will invoke that static method in the start-up process of the web server instance inside it.

This implementation makes those services can be used from both prerendering process and the Blazor Wasm app on the web browser process.

This is an experimental project at this time.

And this is still an experimental project.

There are not a lot of results of using the "BlazorWasmPreRendering.Build" yet.

So currently, I'm not sure that "BlazorWasmPreRendering.Build" works well on complicated real-world almost applications or not.

And I am still interested in the "react-snap" approach if it can avoid the problem that it can't wait for the async initialization because developers may use this approach with an out-of-the-box experience.

Conclusion

Let's wrap up.

The "BlazorWasmPreRender.Build" package will improve the internet search results of your static hosting Blazor Wasm app with only minimal or no code changes.

But this is still an experimental project.
I welcome anybody who forks and improve this project or implement other approaches.

I hope the "BlazorWasmPreRendering.Build" package will save you time.

Happy Coding :)

Discussion (0)