Block elements are Umbraco's new favorite thing, so I thought it would be good to have a post about how we worked with them while creating version 2.0 of our Umazel Starter Kit.
If you're hearing about this Starter Kit for the first time, here is what you need to know: It's free, and there's a version for Umbraco v8 released. (You can also find a demo here). However, the first version of the Starter Kit was for Umbraco v7. Long before Blocks were introduced, we wanted to be able to create a page by putting together things like Lego blocks. We called them "sections" and they were, essentially, document nodes under one grouping node (the "sections folder") that was automatically created under each document. We didn't choose to go with Nested Content with this because sections had a lot of properties and Nested Content was not that flexible, fearing that content editors would have a hard time with it.
When Blocks were released, we noticed two things that made us decide to refactor the whole section approach in order to use them: The double-document approach (content and settings) and the option to either have a block editor render inline or open in a new window. This was enough to ensure that Blocks could provide a good experience to content editors.
So here's how we handled this.
First, we created a document with which we would compose all our sections (block elements). In the starter kit, we have two types of block elements (sections and intros) so we created two corresponding composition docs. This might seem a bit redundant, but we're used to grouping documents using a composition, even if the composition doesn't add any properties to them. Through Models Builder, this provides an interface (e.g. IAbstractSectionBlock), and that's good enough for us to be able to check whether a document belongs to a specific "group" of document types.
So "section" block elements are composed with the Abstract Section Block, while "intro" block elements are composed with BOTH the Abstract Section Block and the Abstract Intro Section Block. That is because we treat intro blocks like normal sections anyway, but we need a way to tell them apart from other blocks (and give them an additional set of properties - a single one in this case).
We also created a composition used on all documents that utilize content blocks (some may not) and provides the Block List for both intros and sections:
Afterwards, we ditched the default block element rendering and created this partial view:
Let's get into some detail here.
We always call the view providing the current page as a model. So our first check is if our models follow the composition described above, i.e. use block elements. This is our early-exit condition to make things faster if this view is called needlessly.
Afterward, we go into two loops: One for the intro blocks and one for the section blocks. Both loops (however differently coded, oops!) check for a property named "Disabled" and skip the blocks that have it set to true. This property is part of the Abstract Section Block composition (so it goes for both intros and sections), and setting it to true makes Umbraco essentially ignore this one. This was needed because we didn't want editors to have to delete any block element they wouldn't use.
Moreover, we've set up some code so when you check this box the entry in the Block List goes grey, as it happens on normal "unpublished" nodes.
After we've found the sections we have to render, comes the second part that is called from within the first one:
This one renders a single block. Ignore the multiCol reference at the start, it's for a special section that contains other sections and puts them in columns. Also, ignore the switch statement - it's only there for future expansion and special handling of specific sections.
What this view does is essentially taking the Block element and:
- Find out whether there's a complimentary partial to load section-specific scripts with Client Dependency
- Decide whether to cache the section or not depending on where it's stored
- Handle things a bit differently if it's a multi-column section (a section that contains other sections).
Ignoring the third bullet, as mentioned above, since it's just very specific handling, let's take the easiest of the remaining two first: Decide whether to cache the section or not.
This is achieved very easily by having the section stored in one of those two locations:
So if you have a section that has problems with it being cached, all you have to do is move it to the "Uncached" folder. Umbraco will find it and render it uncached.
Cached sections are a tough one as well - we can't just use a "cache per page" flag since we may have the same section added several times on a single page - e.g. text sections, plus we're multilingual, so a contextual key with something like the page id wouldn't cut it. So, to cover all cases, we're using a contextual key that is comprised of the section's key (block elements don't have IDs, but they have unique keys) and the culture we're rendering for. This should be unique enough for a cache key.
To render a section, we're using a convention: The partial view for the section should have the exact same name as the block element's alias. So a block element with an "accordionList" allias should have a corresponding "accordionList.cshtml" partial view. We use Model. Content as the partial's model (block element objects have Content and Settings properties corresponding to their two documents).
We haven't talked about the Settings documents yet - these are supposed to contain settings (as their name implies), so what we've done is compose them (yes, again!) with various documents that provide properties for animations, margins, and paddings, etc. that are common in most sections. Each "section" block has its corresponding "settings" document.
Here's how the Text Section's "Content" document looks like in the Umbraco back office:
And here's how the Text Section's "Settings" document looks like:
Here's the partial view that handles the "Text Section" block:
This, as all partials for sections, are strongly-typed (don't forget we're using strongly-typed models all the way). We won't go into details here, since there are some extension methods involved that our out of the scope of this article. Just pay attention to how we get properties from both the Content and Settings documents to render our block element here. (Please don't yell at us for the double conversion at line 46 though :) )
Our second convention on partial views used with blocks is how we load section-specific scripts. Even though we're using Client Dependency, it makes no sense to clutter all pages with scripts that are being used in only a subset of blocks (especially when those blocks may not be used at all in some setups).
So what we did was to have this folder under which we put partials that are named just as each block's document type alias, suffixed with "Cdf". Let's see one of those:
But wait, that's not everything! We will probably need some more scripts for some sections that have to use specific section data to work, won't we? Sometimes! Yet another convention for this is to have those scripts put into yet more partial views in yet another special folder:
The same convention with before, only now partial views are suffixed with "Scripts".
There's actually another partial to render those, similar to the "section renderer" partial we presented earlier. This one gets called at the bottom of the page on our main template so that scripts are executed last. Let's see the renderer partial and one example view for adding scripts to a section:
The PageSectionScriptsRenderer view is the one that is being initially called. This does the usual checks, respecting the "disabled" property for any sections that have it on, taking care of multi-column blocks (out of our current scope, as mentioned earlier), and calls the _RenderSectionScripts view which does the actual work. The unifySectionScripts flag is to actually replace all the <script>tags found in individual section scripts with one big enclosing <script> tag, and it's enabled or disabled via a web.config setting.
The _RenderSectionScripts view, in turn, follows the convention to find a view having the block element's document type alias suffixed with "Scripts" and loads that view.
Finally, the actual view (in our example, it's the one that renders the scripts for the Client Logos section) does the work. Having a reference to the actual block element data allows it to use inside the script.
As expected, those scripts cannot be registered in Client Dependency and come up uncompressed and unbundled inside the main markup, so they need caution so that the main markup doesn't get filled with stuff. On the other hand, having such specific scripts lessens the time needed for script processing, making pages achieve higher ratings in evaluations as Lighthouse and Google Page Speed do.
That's all for now! I'm aware there are a zillion more things I could write, but I believe you've got a good view of the underlying architecture (if you've made it that far!). Comments welcome, as always!