Static website with Umbraco Heartcore
Heads Up!
This article is several years old now, and much has happened since then, so please keep that in mind while reading it.
JAMstack brings with it speed, stability, scalability, security, serviceability and simplicity. Adam Griffith, Luminary's Managing Director, has written an article on the benefits of the JAMstack which is a good read if you need more information.
Speedy Gonzales
The speed and stability of JAMstack comes from the static prebuilt markup which is stored on the server.
An easy way to describe this is as follows:
With the razor file, the webserver needs to access a database or an API or even a cache store to get the Title. In the illustration above, the razor file relies on the Controller to communicate with Umbraco Heartcore and retrieve content. This requires the webserver to work a bit harder to render the content.
With a static file, the webserver just needs to serve index.html. No extra work is needed to generate the content as it is already in the markup.
Generating static files
I feel we have come the full circle with developing websites. Back in college, I used Dreamweaver to create and update static HTML files. Then FTP it to a shared webserver. Rinse and repeat.
The problem with the above workflow is that my customer could not edit any content on their own accord and had to rely on a developer (or school-kid in my instance). So content management systems such as Umbraco CMS came into being. While a CMS allows editors to do what they want, it added in a few extra requirements such as executable code and databases.
With JAMstack, the idea was to get web servers to serve static pages rather than execute code. So platforms such as Gatsby Cloud and Netlify are utilized to pull in content from a CMS, generate a static HTML file and push it to a web server or to a storage resource.
Heartcore and JAMstack
While a traditional CMS could be plugged into generating static pages, a headless CMS is best suited to do this. Its RESTful API architecture, Image delivery service, Content Delivery Network (CDN), Webhooks and GraphQL API makes Heartcore an ideal fit for building a static website.
A static website generator such as Gatsby or Nuxt could be used to pull in content from Heartcore and generate static HTML files. Mix in the GraphQL API and GraphQL filtering capabilities of Heartcore to the recipe. Voila! You have got a JAMstack-ready tech-stack rearing to go!
Sticky JAM
JAMstack works beautifully when it does. But as in any scenario, there would be tradeoffs. With static pages, you get speed, stability, scalability, security, serviceability and simplicity. But on the negatives, you lose the ability for real-time updates, quick previews, and custom functionality such as forms/workflows. You also rely heavily on third party systems to do most of the heavy lifting for generating static pages.
What happens when a single update to a content item triggers the need for a thousand pages to be generated in one-go? Or when your build minutes are exhausted? These are real-world scenarios we have encountered. While there are indisputable benefits of using JAMstack, I wanted a way to work with razor views and avoid some of these pitfalls which JAMstack introduces.
Also, what if you are a die-hard .NET fan? What if you want to build a website with .NET 5.0 but still want the benefits of a static site? What if you are more comfortable writing C# and Razor markup than Javascript and Handlebars?
Stazor Pages
This is where the concept of Stazor pages (or Static Razor pages) come to the rescue. In it’s simplest form, Stazor pages generate static HTML files on the first request and deletes the same file on a webhook call.
The server would still need to execute some code rather than only serve static pages. Let’s see how this works.
Stazor Pages Middleware
Stazor Pages Middleware has three distinctive actions it takes in response to page requests and webhook calls.
First Request
When receiving the first request for a given URL, the content will be retrieved from Umbraco Heartcore, passed to an MVC Controller and will be rendered out as a page. The usual ASP.NET MVC concept of views, shared layouts, _ViewImports, _ViewStart, ViewComponents, DisplayTemplates all work without any modification. In short, if you have written an ASP.NET MVC Web Application, Stazor Pages Middleware can be easily plugged in with very little modification.
Once the page is rendered, the middleware saves the page as an HTML file to `AppData\Pages` folder. This location is used by the StaticFileMiddleware to serve the static file on subsequent requests.
Subsequent Requests
On subsequent requests to the same URL, the Stazor Pages Middleware uses the ASP.NET StaticFileMiddleware to serve the previously generated static HTML file. No calls to Heartcore, no processing in Controllers. Just serving some static HTML goodness.
Webhook Call
Whenever content is updated, we have setup Heartcore to send a webhook request to our web application. Once again the Stazor Middleware takes over and deletes the previously generated content.
Stazor Page Lifecycle
This diagram outlines those three distinct actions which make up the Stazor Page Lifecycle.
Lord Lamington in Action
As with all things new, shiny and performant, Lord Lamington (our world-famous fictional client) wants to get in on the action. Lord Lamington wants to rebuild his website with Stazor Pages on .NET 5. He is also very pleased that he can use most of the Controllers and Views from his previous .NET Core 3.1 web application.
This is the boring part where I show the developers how we coded this new web application. Feel free to skip to where I show you a Stazor Pages sample site.
New ASP.NET Core Web Application Project
The code I explain below is available on GitHub. Feel free to clone the repo and make your own changes.
First off, make sure you have got Visual Studio 2019 version 16.8.2 or higher. Then create a new ASP.NET Core Web Application (Model-View-Controller) project with ASP.NET Core 5.0. I have called mine TwentyFourDaysIn.StazorPages.Heartcore. Add in references to the following NuGet packages using the Package Manager.
- StazorPages - PM> Install-Package StazorPages -Prerelease
- StazorPages.Heartcore - PM> Install-Package StazorPages.Heartcore -Prerelease
Cross-Platform Bonus
Running your code on macOS or Ubuntu? No Problem! Your cloned repo could run using Visual Studio Code or the .NET Core CLI. More instructions can be found in the README file on the repo.
Add in your configuration
Modify your appsetting.config to add configuration to point to your Umbraco Heartcore project. For this example, feel free to use my personal Heartcore project to start off.
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*",
"UmbracoConfig": {
"ProjectAlias": "damazingnut",
"ApiKey": ""
}
}
Content Structure on my Umbraco Heartcore instance
The following screen-grab will give you an idea of how I have structured the content in my Heartcore instance.
The Cafe Model
In this article, I will go through the steps to render out a static page which renders a cafe location. In order to do that, we need a model for Cafe which is derived from HeartcorePage and implements the IRetrievedContent interface. The class also needs to be decorated with the ModelBinder attribute with the type of StazorPages.Routing.RouteDataModelBinder. The MapToType() method also uses StazorPages.Heartcore.Models.HeartcorePage.MapCommonProperties() to map fields such as Id, Url, Name and IsVisible.
using Microsoft.AspNetCore.Mvc;
using StazorPages.Heartcore.Extensions;
using StazorPages.Heartcore.Models;
using StazorPages.Models;
using StazorPages.Routing;
using Umbraco.Headless.Client.Net.Delivery.Models;
namespace TwentyFourDaysIn.StazorPages.Heartcore.Models
{
[ModelBinder(typeof(RouteDataModelBinder))]
public class Cafe : HeartcorePage, IRetrievedContent
{
public const string ContentTypeAlias = "cafe";
public string BuildingNumber { get; set; }
public string AddressLine1 { get; set; }
public string AddressLine2 { get; set; }
public string Suburb { get; set; }
public string Postcode { get; set; }
public string State { get; set; }
public string Title { get; set; }
public string GetContentTypeAlias() => ContentTypeAlias;
public static Cafe MapToType(Content content)
{
var model = new Cafe
{
Title = content.Name,
BuildingNumber = content.Value<string>("buildingNumber"),
AddressLine1 = content.Value<string>("addressLine1"),
AddressLine2 = content.Value<string>("addressLine2"),
Suburb = content.Value<string>("suburb"),
Postcode = content.Value<string>("postcode"),
State = content.Value<string>("state")
};
model.MapCommonProperties(content);
return model;
}
}
}
Cafe Document Type
The model above corresponds with this document type on my Heartcore instance.
The Cafe Controller
We are going to add a controller to render out a cafe page for us. Nothing special at all here.
using Microsoft.AspNetCore.Mvc;
using TwentyFourDaysIn.StazorPages.Heartcore.Models;
namespace TwentyFourDaysIn.StazorPages.Heartcore.Controllers
{
public class CafeController : Controller
{
public IActionResult Index(Cafe model)
{
return View(model);
}
}
}
The View
A view file for the cafe uses a simple bootstrap layout. Again, no special markup or skulduggery here.
@model Cafe
@{
Layout = "_SimpleLayout";
}
<div class="container">
<div class="jumbotron mt-3">
<h3>A little bit of indulgence at our cafe in</h3>
<h1>@Model.Title</h1>
<p class="lead">@Model.BuildingNumber @Model.AddressLine1 @Model.AddressLine2</p>
<p class="lead">@Model.Suburb @Model.State @Model.Postcode</p>
<a class="btn btn-lg btn-primary" href="@Model.Url" role="button">URL to self</a>
</div>
</div>
The _SimpleLayout.cshtml file can be found here.
TypeResolver
An implementation of ITypeResolver is needed so that the StazorPages Middleware could map a ContentTypeAlias to a strongly typed model. A simple implementation is given below.
using System;
using StazorPages.Heartcore.Resolvers;
using StazorPages.Models;
using TwentyFourDaysIn.StazorPages.Heartcore.Models;
using Umbraco.Headless.Client.Net.Delivery.Models;
namespace TwentyFourDaysIn.StazorPages.Heartcore.Resolvers
{
public class TypeResolver : ITypeResolver
{
public IRetrievedContent GetTypedContent(Content content)
{
return content.ContentTypeAlias switch
{
Cafe.ContentTypeAlias => Cafe.MapToType(content),
// truncated for brevity
_ => throw new TypeLoadException($"Unknown type {content.ContentTypeAlias} encountered.")
};
}
}
}
Wire up the Startup file
In ConfigureServices() make a call to services.AddStazorPagesHeartcore() and in Configure(), call app.UseStazorPages(env). You also need to map an endpoint for StazorPages using endpoints.MapStazorPages(). The full Startup.cs file is given below for your reference.
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using StazorPages.Heartcore.Config;
using StazorPages.Heartcore.Extensions;
using StazorPages.Routing;
using TwentyFourDaysIn.StazorPages.Heartcore.Resolvers;
namespace TwentyFourDaysIn.StazorPages.Heartcore
{
public class Startup
{
private readonly IWebHostEnvironment _webHostEnvironment;
public Startup(IConfiguration configuration, IWebHostEnvironment env)
{
Configuration = configuration;
_webHostEnvironment = env;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
var umbracoConfig = new UmbracoConfig();
Configuration.GetSection(nameof(UmbracoConfig)).Bind(umbracoConfig);
services.AddStazorPagesHeartcore(_webHostEnvironment, umbracoConfig, new TypeResolver());
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.UseStazorPages(env);
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapStazorPages();
});
}
}
}
Heartcore Content
The content for the Melbourne CBD cafe on my Heartcore instance is filled in like this.
If you visit https://localhost:44340/cafes/melbourne-cbd on your development machine, you should get this simple page with data retrieved from Umbraco Heartcore.
Voila! Static File
On the first request to the URL, content is retrieved from Heartcore and then saved to this static HTML file.
<!doctype html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
<title>24 Days In Umbraco - Melbourne CBD</title>
</head>
<body>
<div class="container">
<div class="jumbotron mt-3">
<h3>A little bit of indulgence at our cafe in</h3>
<h1>Melbourne CBD</h1>
<p class="lead">Level 1 195 Little Collins Street </p>
<p class="lead">Melbourne VIC 3000</p>
<a class="btn btn-lg btn-primary" href="/cafes/melbourne-cbd/" role="button">URL to self</a>
</div>
</div>
<div class="container">
<small>Stazor Page generated at 29-Nov-2020 09:20:21 UTC</small>
</div>
</body>
</html>
Any subsequent requests to the same cafe page would result in this static file been served.
Pro Dev Tip
Want to debug some tricky code? You can turn off the generation of static files on your development environment by amending the app.UseStazorPages(env) in the Startup.cs file to app.UseStazorPages(env, !env.IsDevelopment()).
Content Update process
Creating a webhook in Umbraco Heartcore and pointing to the `/api/contentupdate` endpoint deletes the page whenever a Publish content event occurs. This endpoint is defined in the StazorPages.Heartcore library. Essentially, Heartcore webhook calls are handled automagically. 💫
Similarly, other events such as Unpublish content, Delete content should be configured to the same endpoint.
Sample Stazor Pages Site
Lord Lamington has got a StazorPages.Heartcore sample site at https://lordlamingtonheartcore.azurewebsites.net/.
You can see a screengrab below which highlights the generated time of the static file. For example, you might see Stazor Page generated at 25-Nov-2020 01:12:03 UTC
Speedy Gonzales Again
Time to check some metrics regarding speed. I don't claim this to be a proper benchmark, it's anecdotal.
I tested loading https://lordlamingtonheartcore.azurewebsites.net/cafes/ on my local machine running IIS Express with the following setup. Each time, I restarted the application and loaded the home page prior to loading the cafes page using Postman. All durations are in milliseconds.
First Run | Second Run | Third Run | |
Stazor Pages disabled | 99 | 465 | 870 |
First request with Stazor Pages | 179 | 2374 | 254 |
Second request with Stazor Pages | 65 | 78 | 86 |
Speedy's verdict - Overall, the first request with Stazor Pages enabled was a bit slower than with Stazor Pages disabled. But with the second and subsequent requests, hands down, Stazor Pages wins on speed.
In its infancy
At the time of writing this article, Stazor Pages is in its infancy. There are many improvements which are needed both to the codebase and to the routing mechanism. As I plan on dogfooding this for my wife's business site, follow me on @dAmazingNut for future updates.
Open Source Code
With the disclaimer above, the source code is shared on GitHub. It is open for Pull Requests and Suggestions.
- StazorPages - Source code for the NuGet package - https://github.com/emmanueltissera/stazorpages
- StazorPages.Heartcore - Source code for the NuGet package - https://github.com/emmanueltissera/stazorpages.heartcore
- LordLamington.Heartcore - Source code for Lord Lamington's website running on Azure - https://github.com/emmanueltissera/lordlamington.heartcore
Happy Servers and Happier Developers
“In a nutshell, Heartcore means: happy marketers who reduce their time to market, happy editors who create content early-on, and happy developers who get to use the latest tech.”
Okay! So that's my own quote. But now Heartcore + Stazor Pages allow for Happy servers. Servers which do not need to work as hard. They just need to work hard once and serve static content on subsequent requests.
Developers who are happy with Heartcore, now have more reasons to be happier. They (I, nerd 🤓) get to use ASP.NET Core 5 and still build a static website for optimum performance.💥
Anyone for some Jam and Vanilla Lamingtons?
Emmanuel Tissera
Emmanuel is on Twitter as @dAmazingNut