In this blog post, we will walk through the process of making a Blazor WebAssembly application both SEO-friendly and flicker-free.
Steps
Create a Blazor App WebAssembly Interactive (Global): Start by creating a new Blazor WebAssembly application. Make sure to select the "Interactive (Global)" option.
Figure 1: Selecting the Blazor WebAssembly App project type.
Figure 2: Naming the new Blazor WebAssembly project.
Figure 3: Selecting the 'Interactive WebAssembly (Global)' option.
Detect Bots and Crawlers: Implement a mechanism in your application to detect if a request is coming from a bot or a crawler. This is important for SEO purposes, as it allows search engine bots to crawl your site more effectively.
Update App.razor
in Client project with the following new changes:
@using Microsoft.Net.Http.Headers
@using System.Text.RegularExpressions
@{
Boolean isBot = false;
@inject Microsoft.AspNetCore.Http.IHttpContextAccessor HttpContextAccessor;
var userAgent = HttpContextAccessor?.HttpContext?.Request.Headers[HeaderNames.UserAgent];
if (userAgent.HasValue)
{
var agent = userAgent.ToString();
if (!string.IsNullOrWhiteSpace(agent))
{
isBot = Regex.IsMatch(
agent,
@"bot|crawler|baiduspider|80legs|ia_archiver|voyager|curl|wget|yahoo! slurp|mediapartners-google",
RegexOptions.IgnoreCase
);
}
else
{
isBot = false;
}
}
}
- Adjust Pre-Render Settings Based on Bot Detection: Once you have implemented bot detection, you can use this information to adjust your pre-render settings. Specifically, you should make pre-rendering
true
orfalse
based on whether the request is coming from abot
or not. This can help improve the performance of your site for real users.
Before closing the head tag, replace <HeadOutlet/>
with the following code, so that it will pre-render or not based on the isBot variable.
@if (isBot)
{
<HeadOutlet @rendermode="new InteractiveWebAssemblyRenderMode(prerender: true)" />
}
else
{
<HeadOutlet @rendermode="new InteractiveWebAssemblyRenderMode(prerender: false)" />
}
In the body section, like we did in the heading section, render routes based on the isBot
flag and pass isBot
to the route as CascadingValue.
<CascadingValue Value="isBot">
@if (isBot)
{
<Routes @rendermode="new InteractiveWebAssemblyRenderMode(prerender: true)" />
}
else
{
<Routes @rendermode="new InteractiveWebAssemblyRenderMode(prerender: false)" />
}
</CascadingValue>
@if (!isBot)
{
<script src="_framework/blazor.web.js"></script>
}
Now we have the isBot
value based on that, if the code is running on the client side we can show a loading string. For that, we can update client-side rendering with one div
after Routes
gets rendered.
<Routes @rendermode="new InteractiveWebAssemblyRenderMode(prerender: false)" />
<div id="webassembly-loading-spinner">
<p>Loading...</p>
</div>
Once client-side rendering is complete, we need to hide loading. For that, we can use this JavaScript function in the head section of App.razor.
<script>
(function () {
window.deleteElementById = function (id) {
var element = document.getElementById(id);
element.parentNode.removeChild(element);
}
})();
</script>
This is the final App.razor
page.
@using Microsoft.Net.Http.Headers
@using System.Text.RegularExpressions
@{
Boolean isBot = false;
@inject Microsoft.AspNetCore.Http.IHttpContextAccessor HttpContextAccessor;
var userAgent = HttpContextAccessor?.HttpContext?.Request.Headers[HeaderNames.UserAgent];
if (userAgent.HasValue)
{
var agent = userAgent.ToString();
if (!string.IsNullOrWhiteSpace(agent))
{
isBot = Regex.IsMatch(
agent,
@"bot|crawler|baiduspider|80legs|ia_archiver|voyager|curl|wget|yahoo! slurp|mediapartners-google",
RegexOptions.IgnoreCase
);
}
else
{
isBot = false;
}
}
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<base href="/" />
<link rel="stylesheet" href="bootstrap/bootstrap.min.css" />
<link rel="stylesheet" href="app.css" />
<link rel="stylesheet" href="Blazor.Seo.Friendly.FlickerFree.styles.css" />
<link rel="icon" type="image/png" href="favicon.png" />
<script>
(function () {
window.deleteElementById = function (id) {
var element = document.getElementById(id);
element.parentNode.removeChild(element);
}
})();
</script>
@if (isBot)
{
<HeadOutlet @rendermode="new InteractiveWebAssemblyRenderMode(prerender: true)" />
}
else
{
<HeadOutlet @rendermode="new InteractiveWebAssemblyRenderMode(prerender: false)" />
}
</head>
<body>
<CascadingValue Value="isBot">
@if (isBot)
{
<Routes @rendermode="new InteractiveWebAssemblyRenderMode(prerender: true)" />
}
else
{
<Routes @rendermode="new InteractiveWebAssemblyRenderMode(prerender: false)" />
<div id="webassembly-loading-spinner">
<p>Loading...</p>
</div>
}
</CascadingValue>
@if (!isBot)
{
<script src="_framework/blazor.web.js"></script>
}
</body>
</html>
Now we can update our <Route>
component to hide the loading bar by invoking the JavaScript function to hide the loading. So, the final Routes.razor
will look like this.
@* for some reason we need space here otherwise builds fails with error 'The 'inject` directive must appear at the start of the line', so added this comment for make build works *@
@inject IJSRuntime jsRuntime;
<Router AppAssembly="typeof(Program).Assembly">
<Found Context="routeData">
<RouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)" />
<FocusOnNavigate RouteData="routeData" Selector="h1" />
</Found>
</Router>
@code {
[CascadingParameter]
public bool isBot { get; set; }
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender && isBot == false)
{
await jsRuntime.InvokeVoidAsync("deleteElementById", "webassembly-loading-spinner");
}
}
}
Now update MainLayout.razor
page to hide blazor error ui
if loading on the server. So, the final MainLayout.razor
will look like this.
@inherits LayoutComponentBase
<div class="page">
<div class="sidebar">
<NavMenu />
</div>
<main>
<div class="top-row px-4">
<a href="https://learn.microsoft.com/aspnet/core/" target="_blank">About</a>
</div>
<article class="content px-4">
@Body
</article>
</main>
</div>
@if (!isBot)
{
<div id="blazor-error-ui">
An unhandled error has occurred.
<a href="" class="reload">Reload</a>
<a class="dismiss">🗙</a>
</div>
}
@code {
[CascadingParameter]
public bool isBot { get; set; }
}
- Add Missing AddHttpContextAccessor Service Registration:Don't forget to register the
HttpContextAccessor
service in your application. This service is necessary for accessing the HTTP context of a request. The finalProgram.cs
will look like this.
using Blazor.Seo.Friendly.FlickerFree.Client.Pages;
using Blazor.Seo.Friendly.FlickerFree.Components;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddRazorComponents()
.AddInteractiveWebAssemblyComponents();
builder.Services.AddHttpContextAccessor();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseWebAssemblyDebugging();
}
else
{
app.UseExceptionHandler("/Error", createScopeForErrors: true);
// 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.UseAntiforgery();
app.MapRazorComponents<App>()
.AddInteractiveWebAssemblyRenderMode()
.AddAdditionalAssemblies(typeof(Blazor.Seo.Friendly.FlickerFree.Client._Imports).Assembly);
app.Run();
Demo
The following GIF demonstrates the final outcome of the steps described in this guide:
Source Code: GitHub - AditiKraft/Blazor.Seo.Friendly.FlickerFree