Issues

Hotwiring Umbraco with Turbo

Hi there — I wanted to share a tip for making a site (or a subsection of a site) faster and smoother without having to dig yourself into deep caching strategy rabbit-holes or restructuring the entire stack for asynchronous JSON-payloads (whatever that means?).

If you already know how to build a website, no matter if it's static, CMS-driven or a little bit of both, you'll be able to add this on top, and see the results immediately.

It all revolves around using a framework called Turbolinks — our company website's Cases section have used this for quite some time now, and when Turbolinks was recently rebranded as Turbo (part of the new Hotwire toolset) I decided to upgrade and maybe write a little something about it as well.

I'll take you through a stripped-down version to see what it takes.

What are we building?

The setup here is a page for showcasing various cases made in a couple of different categories (e.g. webdesign, graphics design and more).

There's three document types at play: CasesCategory and Case, set up with permissions to allow building a tree like this (Website is our "Home" node):

Website
└── Cases                        [Cases]
    ├── Iconography              [Category]
    │   ├── History Museum       [Case]
    │   └── McDonald's           [Case]
    └── Webdesign                [Category]
        └── LEGO                 [Case]

This gets us friendly step-wise URLs like /cases/ for a list of featured cases, /cases/webdesign/ for all our webdesign cases and finally /cases/webdesign/lego/ for the specific LEGO webdesign case.

Setting this up should be straightforward with Umbraco's built-in tools. If you let Umbraco create the Templates along with the Document Types (the default when you create a new Document Type), you'll only need to do a couple of things:

  1. Set a common Master Template for them
  2. Define the rules for building the hierarchy depicted above

Both of these can be accomplished within Umbraco with a couple of clicks.

Experience has taught me to build something like this "backwards", i.e. starting from the bottom of the tree (to prevent having to "circle back" to a previously created document type to allow a new one as child):

  1. Create the Master Template
  2. Create the Case document type - then edit its template and set its Master to the one I just created
  3. Create the Category document type, and set the Case document type as an allowed child on the Permissions tab - then set its master
  4. Finally, create the Cases document type, and set the Category as an allowed child - then set its master - you get it now, right?

Rendering the individual cases can be done in whichever way we prefer - but rendering the category pages and the overall cases page can be done with the same partial, if we think ahead a little bit.

On the cases page we chose to create a picker and select 10-15 cases we want to feature, while on any category page we want to show all cases below. I'll use the same partial (CasesView.cshtml) for the logic of which cases to render, using the variable casesToShow for the collection of cases and then actually rendering the cases with a doctype-specific partial (CaseTile.cshtml).

CasesView.cshtml

@inherits Umbraco.Web.Mvc.UmbracoViewPage
@using ContentModels = Umbraco.Web.PublishedContentModels;
@{
	var currentPage = Model;
	
	// Create an empty collection
	var casesToShow = Enumerable.Empty<ContentModels.Case>();
	
	if (currentPage is Cases) {
	  casesToShow = currentPage.FeaturedCases.OfType<ContentModels.Case>();
	} else if (currentPage is Category) {
	  casesToShow = currentPage.Children<ContentModels.Case>().Where(c => c.IsVisible());
	}
}
@if (casesToShow.Any()) {
	<div class="cases">
		@foreach (var tile in casesToShow) {
			@Html.Partial("Blocks/CaseTile", tile)
		}
	</div>
}

CaseTile.cshtml

@inherits Umbraco.Web.Mvc.UmbracoViewPage<ContentModels.Case>
@using ContentModels = Umbraco.Web.PublishedContentModels;
@{
	var block = Model;
}
<div class="casetile">
	<h2>@(block.Name)</h2>
	<a href="@(block.Url)">See the full case</a>
</div>

Also, we'll need a navigation partial for the categories:

CategoryFilter.cshtml

@inherits Umbraco.Web.Mvc.UmbracoViewPage
@using ContentModels = Umbraco.Web.PublishedContentModels;
@{
	var currentPage = Model;
	
	var projectsRoot = currentPage.AncestorOrSelf<ContentModels.Cases>();
	var categories = projectsRoot.Children<ContentModels.Category>().Where(c => c.IsVisible());
	
	var selectedClassName = "selected";
	var selectedClass = "";
}	
<div class="categoryfilter">
	<ul>
		<li class="@(currentPage.Id == projectsRoot.Id ? selectedClassName : null)">
			<a href="@(projectsRoot.Url)" class="categorylink">Featured</a>
		</li>
		@foreach (var category in categories) {
			selectedClass = category.Id == currentPage.Id ? selectedClassName : null;
			<li class="@(selectedClass)">
				<a href="@(category.Url)" class="categorylink">@(category.Name)</a>
			</li>
		}
	</ul>
</div>

It's a working system

So if everything's been done right, we should now have something that works, i.e., we can build a cases section, we can add some categories below it and we're able to create the individual case pages. Even better — we can browse the pages and navigate between them.

The important part here is that the site works; that each page shows the expected contents even if we hit its URL directly. Then we can take it a step further.


Why am I making such a strong point about this? Because that's what probably around 95% of the audience would navigate to this section for: To see a list of cases (perhaps in a specific category) and definitely to see individual cases as well. By making sure this is doable on even the simplest device with no CSS or JavaScript available (could be a user preference, could be by design - anything really), I have accomplished the main goal.

Another way I tend to think of it is that doing this forces me to build the Umbraco pieces in a way that makes sense and makes it easy for the editors to add new cases and/or categories. If I'd made a one-page Ajax thing right away, I could have easily fallen into the trap of using e.g. Nested Content or the Block List for building the hierarchy, and subsequently had to jump hoops (i.e. resort to hash-bang URLs) and maybe even server-side rendering just to satisfy one of the main goals; that a case should obviously be available for search engines with a sensible URL.


 

Now push the Turbo button

As the site is working, we can now look at enhancements. The main one being the ability to navigate the categories without a full page refresh, which is exactly what Turbo will allow us to do (among other things, of course).

To install Turbo we'll need the compiled JavaScript, which we can get from unpkg.com - we'll use the UMD version, so downloading that and adding the following script tag to the Master Template's head section enables Turbo for our website:

<script src="turbo.es2017-umd.js"></script>

There isn't even a step 2...

Now, when browsing the site, Turbo does all the heavy-lifting behind the scenes - i.e. when clicking a link to a page within the site:

  1. Fetch the page in the background
  2. Replace the <body> tag with the new page's <body>
  3. Merge any new content in the <head> section
  4. Update the URL to reflect the new location

Meaning:

  • The browser does not need to load or parse the JavaScript bundle(s) on every page load.
  • The JavaScript state is kept between pages

And most importantly: If the browser doesn't load the JavaScript, or if it's too old or if JavaScript is turned off - the site still works perfectly fine!

Let's say for the sake of argument, that we wanted to not include a JavaScript library and "just do it ourselves with a simple fetch + JSON solution" - clearly that's a lot cheaper than including a JavaScript library/framework, no?

After all, we "just" need to hijack the links and send an Ajax request to the server which then returns only the JSON needed to to render the new cases, right? Not quite...

  • We need a partial (or a controller of some sort) on the server to render the JSON
  • We need a template on the client to take the JSON and render the HTML
  • We need the glue to hijack the category links, fetch the JSON and insert the rendered HTML into the document

Yeah, but then that's all it is, right?

(Hollow sounding "No"...)

  • We should change the URL when "navigating" to a new category...
  • ...which also means changing the <title>

Finally. git commit -m "Done. It works now."


INT. BEDROOM - NIGHT

A phone is ringing

DEVELOPER
Hello?

CLIENT
Hi there - so I’m trying the new faster Cases section
but the menu no longer seem to highlight the category
I click? Could you fix this ASAP, please?

DEVELOPER
Oh, of course...
  • Turns out we need to keep track of state in the category navigation too; who would have thought?

So unless you've been doing this a lot, I hope this illustrates the many small steps to making this work "just right"; Turbo allows us to skip so many of them and focus on the stuff that matters most.

Additional features

One of the reasons for rebranding Turbolinks was that Turbo is now capable of a few other things - what we've been using here is now referred to as Turbo Drive - there is also Turbo Frames and Turbo Streams which I'll let you have a look at on your own, if so inclined.

Another very useful feature is that anytime a page takes more than 500 milliseconds to load, Turbo shows a progressbar (fully style-able of course) which does two awesome things:

  1. Lets the user know that something's taking a bit longer
  2. Keeps the current page fully working up until the server responds

By default, Turbo hijacks all internal links but you can disable this behavior for a single link or multiple, by adding a data-turbo="false" attribute on an element, which will disable Turbo for any descendant link.

Turbo also hijacks form submissions, so you need to add the data-turbo="false" on <form>s that post to external sites (e.g. newsletter signups).

You can also add a turbo-root meta tag to your master template, to confine Turbo to only activate for links in this section of your site, e.g.:

<meta name="turbo-root" content="/cases/">

(In fact, this is exactly what we're doing at the moment because of some clashes between the existing "framework" for handling some URLs.)

Finally, Turbo caches the pages you visit, and displays the cached version while loading the fresh one which makes for some impressiveness in the "perceived speed" department.

Parting words

I know this is just one way of tackling a solution like this, but the approach is so perfect for me, because I always strive to build something that works before adding the bells and whistles. Sometimes that even means that there's no time (or money) left for the bells and whistles but that's so much better than the alternative, where we built the fancy thing and then miss some of the very basics.

Anyway - feel free to throw any comments/questions etc. you have my way - I'll be happy to discuss!

Chriztian Steinmeier

Chriztian is a frontend developer at Vokseværk in Denmark - he’s a three time Umbraco MVP and spent most of his teens practicing guitar and coding BASIC and Assembler on his ZX Spectrum 48K. These days he's juggling everything from HTML, CSS & JavaScript to Partial Views, Document Types and Config Transforms on numerous Umbraco Cloud projects.

comments powered by Disqus