Turn your ASP.NET MVC/Razor Website into a SPA Without Blazor

Jeffrey Rennie
7 min readSep 16, 2023

--

Should you rewrite your ASP.NET MVC/Razor website with Blazor? No. Blazor solves one problem and introduces three more. Blazor punishes your users with large, slow web-assembly packages and punishes users who have JavaScript disabled because your website becomes unusable. Blazor punishes you by requiring you to squander precious time rewriting your website and by making it much harder to debug production issues, because blazor websites rely on websockets or web assembly.

Instead of Blazor, save yourself time, money, and stress by using HTMX to transform your website into single-page application (SPA). With HTMX, you and your users will suffer none of the punishments of Blazor listed above.

In this article, I’ll demonstrate how to use HTMX to transform a legacy ASP.NET website into an interactive Single-Page Application by changing 10 lines of code, none of them JavaScript. I’ll describe the advantages of HTMX along the way.

This technique works for ASP.NET websites that follow the standard _Layout.cshtml file pattern, which looks something like this:

<!DOCTYPE html>
<html lang="en">
<head>
<!-- meta tags -->
<title>@ViewData["Title"] - Web Application</title>
<!-- scripts -->
</head>
<body>
<!-- navigation bar -->
<div class="container">
<main role="main" class="pb-3">
@RenderBody()
</main>
</div>
<!-- footer and more scripts -->
</body>
</html>

Sample Application

To generate a sample ASP.NET website for this demonstration, I randotnet new webappwith the fictional business name Wayoprise. I used ASP.NET 6, but this technique works with older and newer versions of ASP.NET too. The source code is on GitHub and the website is running on Google Cloud Run.

When I visited the Wayoprise home page and clicked Privacy, the browser briefly blanked the screen and then loaded the Privacy page. It looked mostly the same as the previous Home page. The only thing that changed was about 100 bytes inside the <main> tag. However, I saw this in the browser’s network trace:

This is a lot of wasted effort for a 100-byte change. For starters, 2.63kB was transferred, more than 10 times the size of the actual change. The browser didn’t download the JavaScript files because they were cached, but it still had to re-evaluate them in the context in new of the new page. This wasted time and energy on the server, and time and energy in the browser.

My goal was to avoid all the wasted effort. When I clicked the Privacy link, I wanted to see the following network trace:

Ideally, I wanted the bytes transferred to be only the new content inside the <main> tag:

<h1>Privacy Policy</h1>

<p>Use this page to detail your site's privacy policy.</p>

A Brief Introduction to HTMX

HTMX is a lightweight (44k, 14k gzipped) JavaScript library that simplifies web development by enabling server-side features to be added to HTML elements with minimal code. It allows you to enhance traditional server-rendered web applications by adding client-side interactivity without the need for complex JavaScript frameworks. HTMX can progressively enhance existing HTML by specifying behavior via attributes, such as hx-get, hx-post, or hx-trigger, to fetch or send data to the server, update the DOM, and trigger events.

Adding HTMX to the Sample Application

To add HTMX to the application, I downloaded the HTMX Javascript library with the following command:

curl -L "https://unpkg.com/htmx.org@1.9.5" > wwwroot/lib/htmx.js 

And I added one line of code to _Layout.cshtml’s scripts section:

  <script src="~/lib/htmx.js"></script>

And then HTMX was fully installed into my ASP.NET project. The changes were recorded in this git diff.

Updating the Sample Application to Use HTMX

With HTMX installed, it was time to put it to work.

First, I wanted to tell HTMX, “when a user clicks a link in the body, don’t fetch a whole page. Instead, fetch a fragment of HTML and replace the contents of the <main> tag with the response.” Here’s how I did that.

I edited _Layout.cshtml and added hx-boost and hx-target to the body tag:

<body hx-boost=true hx-target=main>  

The HTMX website explains hx-boost:

The hx-boost attribute allows you to “boost” normal anchors and form tags to use AJAX instead. This has the nice fallback that, if the user does not have javascript enabled, the site will continue to work.

For anchor tags, clicking on the anchor will issue a GET request to the url specified in the href and will push the url so that a history entry is created.

In the code sample above, hx-boost=true tells HTMX to boost all the links in the body. When a user clicks a boosted link, HTMX uses AJAX to fetch the URL. hx-target’s argument is any CSS selector. hx-target=main tells HTMX to replace the <main> tag with the response. Effectively, I told HTMX to replace the content returned by RenderBody() with the content returned by a new call to RenderBody().

Although I had told HTMX which element to fetch and replace, I hadn’t updated the application to return a fragment of HTML code. A fetch to /Privacy would still return a whole page.

So, I updated the top of _Layout.cshtml to return a fragment of HTML when the request was coming from an HTMX boosted link:

@if (ViewContext.HttpContext.Request.Headers["HX-Boosted"].Contains("true")) {
<text>@RenderBody()</text>
} else {
<!DOCTYPE html>
...

HTMX adds an HX-Boosted: true header to its fetches. That made it easy to detect whether a request was coming from an HTMX boosted link or a regular link. With this code change, _Layout.cshtml responded with only the fragment of HTML returned byRenderBody()for HTMX-boosted links.

There was still one potential pitfall lurking in this code. A browser could cache the response of a request with HX-Boosted: true, and serve it from the cache for a request lacking HX-Boosted: true. That would result in a broken page. To tell the browser not to make that mistake, I added one more line of code:

 @{ ViewContext.HttpContext.Response.Headers.Add("Vary", "HX-Boosted"); }

All these changes were recorded in this git diff.

I compiled and ran the code, and the network trace looked exactly like what I wanted:

No JavaScript or CSS was refetched or retrieved from the cache. Only the new HTML fragment was transferred.

However, I found a bug. After I clicked the privacy link, the url in the browser’s address bar was updated with the /Privacy path, but the title in the browser tab still said “Home page — Wayoprise” when it should have been updated to say “Privacy Policy — Wayoprise.”

Fortunately, HTMX provides a way to update the title at the same time it updates the body, by using the hx-swap-oob attribute. The HTMX website describes hx-swap-oob:

The hx-swap-oob attribute allows you to specify that some content in a response should be swapped into the DOM somewhere other than the target, that is “Out of Band”. This allows you to piggy back updates to other element updates on a response.

To update the title, I inserted a new <title> tag into the fragment of code returned for HTMX boosted links:

@if (ViewContext.HttpContext.Request.Headers["HX-Boosted"].Contains("true")) {
<title hx-swap-oob=title>@ViewData["Title"] - Wayoprise</title>
<text>@RenderBody()</text>
} else {
...

All these changes were recorded in this git diff.

I recompiled and ran the website, and the HTMX-boosted web site rendered exactly like the original website. The response to a request for /Privacy looked like this:

    <title hx-swap-oob=title>Privacy Policy - Wayoprise</title>
<h1>Privacy Policy</h1>

<p>Use this page to detail your site's privacy policy.</p>

The final result is running in Google Cloud.

Success!

I converted this ASP.NET web site into a Single-Page application without writing a single line of JavaScript! Users now enjoy a faster, seamless website on every device, I’ve reduced my web hosting bills because my web servers handle fewer requests and transfer fewer bytes, and I have a code base that’s easier to understand than one built with Blazor or a JavaScript front-end framework. The source code for the sample is posted to GitHub.

HTMX is very powerful, and this article only began to explore the ways it can be applied to ASP.NET applications. Please clap if you found this article useful. Please clap twice if you’d like to see more examples of integrating HTMX with ASP.NET.

--

--