Pre-render Blazor WebAssembly at build time to optimize for search engines

The output of a published Blazor WebAssembly application consists of static files exclusively. Hence these applications can be hosted on static site hosts like Azure Static Web Apps, GitHub Pages, Firebase Hosting, and more. But just like other single page application (SPA) frameworks, Blazor WASM is not properly indexed by search engines. This makes it very hard to do search engine optimization (SEO).
The issues discussed in this article are not exclusive to SPA frameworks, but any web application requiring JavaScript execution to render the content.
The SEO problems with SPA's #
Search engines crawl your website using "crawlers" also called "spiders" sometimes. These crawlers are essentially bots visiting every page it can find on your website. The content on those pages are then added to the search index. But what do crawlers see when they visit SPA's? For Blazor WASM applications, this is what crawlers see:
The only thing the crawlers see are the words "Loading…" or a blank screen.
To optimize for search engines at the margins, you can add some content above and below, or use the title and description meta tags in the head, but that won’t get you far. The reason why it doesn’t see your entire SPA, is because most search engine crawlers do not execute JavaScript.
The exception to this is Google which depending on the geography you are targeting is the primary search engine. Google crawls websites just like other search engines, but for JavaScript applications will go through an additional phase of rendering the pages with JavaScript execution.
This rendering phase also works for Blazor WASM applications. Below is a Google search query which is returning content generated by Blazor WASM, proving that Google’s crawlers are able to render and index Blazor WASM applications:
Even though Google is able to render and index JavaScript applications, the gold standard is still server side rendered HTML. Google takes longer to index JavaScript applications than it does indexing plain HTML. But most importantly, there are a lot of other search engines out there. If your content targets an Eastern European audience, Yandex is another search engine that you need to optimize for. In China, Baidu is the most popular search engine and Google Search doesn’t even exist. And of course, there are many smaller players all around the world you should consider optimizing for.
The SEO solutions for SPA's #
Many SPA frameworks have support for rendering the same application on both client and server. Being able to render both server and client side combines best of both worlds. You have the speed and SEO benefits from server rendered applications and the UX benefits of a SPA. Since .NET 5, Blazor WebAssembly also has support for pre-rendering on the server.
Server side pre-rendering is a great solution, but it does require your code to be executed on a server which is not possible if you’re using a static site host.
Static site hosting services do not allow server side code execution and only serve static files. This limitation does mean these services are incredibly performant and affordable, often with generous free tiers. Hence why these services are so popular.
So instead of pre-rendering server side at request time, you can pre-render at build-time. You can then host the HTML files outputted at build-time on static site hosts, giving you the best performance, best SEO, best UX, and a more affordable hosting.
There is no official support for doing this in Blazor WASM, but you can quickly hack something together with existing open-source pre-rendering tools.
Pre-render Blazor WASM at build time #
This tutorial will walk you through creating a Blazor WASM project and adding PowerShell/Bash scripts to pre-render the application. Then the functionality will be integrated into GitHub Actions to deploy the result to GitHub Pages.
Prerequisites #
You will need the following:
- OS: Windows or Linux distro supported by .NET and NodeJS
- .NET SDK (3.1 and up)
- NodeJS
- git
- GitHub account
You can find the source code for the sample at this GitHub repository.
Setup the Blazor WASM project #
Use the following command to create a folder named Client and create a Blazor WASM project inside of that folder:
Integrating a pre-render tool #
There are many tools in the open-source community that provide pre-rendering functionality. For this tutorial, you will be using a very popular NodeJS tool called 'react-snap'. React-snap can pre-render your application using a headless Chrome browser. Point the tool to your SPA output files and it will render your application and follow any links it can find while saving the results to disk as HTML files. React-snap supports any SPA framework, even though the name may give you the impression it's only for react applications.
You can install the tool globally using the following command:
React-snap does require some configuration and it has to be stored in a json-file named package.json. When you run react-snap, it expects package.json to be in the current working directory (CWD). Using the following commands, create a folder named Prerender and create an empty package.json file in the folder:
PowerShell:
Bash:
Now publish the Blazor WASM project into a new subfolder of Prerender named output:
The publish command, published all the static files to Prerender/output/wwwroot.
Now you need to add the necessary configuration for react-snap to Prerender/package.json:
The most important configuration is the "source" property which tells react-snap where the static web app files are to pre-render. Refer react-snap’s GitHub readme for more information on the other properties.
With the configuration in place and the Blazor WASM published, you can now run react-snap using the following commands:
You can see which files have been created in the output. Let's see if the pre-rendered project works as expected by running a static http server at Prerender/output/wwwroot.
To serve the static files locally, you can use a .NET global tool called 'dotnet-serve' Use the following commands to install the tool and then serve the static files:
The 'o' argument instructs the tool to open the browser automatically. The 'd' argument tells the tool which directory to serve.
You can verify in the browser that the application has been pre-rendered successfully. You can navigate around and verify the source returns all the HTML content. But the interactivity is broken. When you click on the "Click me" button at the /counter page it doesn't do anything. When you check the browser console, you can get a glimpse of what is going wrong:
Normally, the output of a Blazor WASM application will have the following JavaScript reference at the bottom of the body-tag:
In the pre-rendered HTML files, there are multiple script-tags:
In a normal Blazor WASM scenario, the new script-tags are generated by the script blazor.webassembly.js when the browser loads the script. The problem arises because the script was executed as part of the pre-rendering, but then the script is executed again when the browser loads it. The script wasn't developed with this edge case in mind and cannot handle this scenario.
When you remove these two extra script-tags, and refresh the browser, the Blazor's interactivity will be restored. To make this easier, you can use some PowerShell or Bash scripting to automate this.
PowerShell:
Bash:
It is likely that the HTML will differ depending on the version of .NET you are using. Here’s the same script with the HTML from .NET 5 copied in:
PowerShell:
Bash:
This script will search for all HTML-files recursively and remove the extra script tags from the files. (React-snap minified the HTML and put everything on a single file.)
The moment any of the HTML you’re trying to remove changes, the script won’t work as expected. If you’re feeling adventurous, you can update the scripts to be more robust by using commands/utilities that understand HTML.
After fixing all the HTML-files, you can refresh the browser, and you now have a pre-rendered Blazor WASM application that is still interactive!
Here’s a script that puts together the publish, pre-rendering, and HTML fixing:
PowerShell:
Bash:
Build and publish to GitHub Pages using GitHub Actions #
There’s a prerequisite blog post which shows you how to build and deploy Blazor WASM to GitHub Pages which goes into more details. This tutorial will focus on integrating react-snap and the script to fix the HTML into GitHub Actions.
Push Blazor project to GitHub #
To make sure you only track the relevant files, you can use the following command to create a .NET specific .gitignore file:
You need to create a local Git repository and commit your source code to the repository using these commands:
Create a new GitHub repository (instructions) and copy the commands to "push an existing repository from the command line" from the empty GitHub repository page, here's what it should looks like but with a different URL:
Create GitHub Action #
Create a new file at .github/workflows/ named main.yml:
PowerShell:
Bash:
Copy the following content to main.yml:
Let’s dissect the adjustments made to integrate pre-rendering:
Just as you published the project locally, GitHub Actions will publish the Blazor WASM project to Prerender/output.
NodeJS is pre-installed on the 'ubuntu-latest' images, so you can simply run the npx react-snap command. The command will be run with 'Prerender' as the CWD.
Just like the bash script locally, GitHub Actions will go through all HTML-files to fix the script-tag issues. In addition to that, the base-tag will be updated to reflect the GitHub Pages subdirectory it will be hosted in. For more details on the base-tag issue, refer to the prerequisite tutorial.
The rest of the workflow YAML file is exactly the same as the prerequisite article. In addition to the Blazor project being pre-rendered to optimize for search engines, GitHub Pages will now not return 404 status codes anymore when requesting child subpaths in the application.
Summary #
SPA frameworks like Blazor require the execution of JavaScript to function. Most search engine crawler’s don’t execute JavaScript which means JavaScript generated content won’t be indexed. Google’s crawlers do execute JavaScript and they can successfully index Blazor WASM apps. To optimize for other search engines, you can pre-render Blazor WASM on the server, but that requires server side code execution.
For static site hosts like GitHub Actions and Azure Static Web Apps, server side code execution is not available. Instead of pre-rendering in response to HTTP requests, you can move the pre-rendering to the build pipeline.
Using pre-rendering tools like react-snap, you can pre-render Blazor WASM. Additionally, you can integrate these pre-rendering tools inside of your continuous integration and continuous deployment pipelines. Using GitHub Actions, you can pre-render your Blazor application and deploy it to GitHub Pages. Deploying pre-rendered applications to GitHub Pages will increase SEO but also resolve those pesky HTTP 404 errors.