Have you ever needed to reuse the same Umbraco project for multiple customers, building a product where all the CMS structure is the same, but the frontend and design are different? How did you manage it? We initially struggled with it and ended up forking the repository and effectively having different projects. But things became unmanageable, with multiple projects that needed to be updated and maintained and that needed bug fixing.
In this article, I will explain how we managed to go back to one single repository used as the source for all the projects, making management much more straightforward.
What differs between customers
In our scenario, we need to build many similar websites, all with the same content types (news, events, “content” pages, who’s who) but with different graphics, configurations and, to a certain extent, even different logic and structure of items listings, search results and homepage.
We managed to consolidate all these different implementations into one codebase by implementing the following “features”:
- We adopted a Page Builder approach to allow different structures in the pages without the need to change properties in the document type.
- We built a theme-based view engine to allow different graphics and layouts.
- We customised the configuration builder of .NET to have overrides specific to a customer.
- We deployed the infrastructure on Azure using ARM templates.
- Finally, we used build events to include or exclude files that cannot be managed via code.
But enough with the introduction: let’s dive into the actual implementation.
Page Builder
Let’s start with how we designed our document types so that they could be easily reused, without any change, in multiple installations.
We adopted two main rules:
- The strict separation between content and metadata.
- Every page is customisable.
Separation between content and metadata
To give full flexibility to editors, all our documents are made of only one “content” property, implemented with the Block List Editor, which contains all the possible “components” editors might need to design their pages as they prefer. This property is all that is needed to render the document in the frontend. However, it is not suitable for referencing the page inside other pages, like in a list of news, in search results, or when the page is included as a highlighted element on the homepage.
For this reason, we also added a few properties, like title, abstract, date, cover image, taxonomies, and similar, which are used when the document is showing inside other pages, like the aforementioned search results page.
Every page is customisable
What we noticed in the first projects, was that every customer wanted something slightly different, even for pages that should be pretty standard, like the listing and search pages. And I’m not talking about the design, but also the logic of the search or listing of documents.
For example, one wanted just the list of news, another wanted some “highlighted” news on top before the list. One wanted the news as a simple list, another wanted to show them grouped by date, and another grouped by topic.
To avoid creating different page controllers per customer, which would have effectively killed the idea of a reusable base project, we implemented also these pages with a Block List Editor.
This way editors could assemble the available components as they wanted and choose between different variations of the same components. We also implemented the settings screen for each component, to allow fine-tuning of the behaviour: for example, to set the page size in the search results or the number of items in the latest news box. And if the “variation” they wanted was not available, we only needed to add a new BLE component, with its related view component for the frontend, and later this would have been available for all future customers.
Themes
To allow different designs for each website, we devised a solution to allow a themes-based approach for the frontend views. With this approach, we can have a generic view reused in all sites that can be replaced by a view specific to an individual website.
This is achieved by creating a ViewLocationExpander and adding it to the current view engine via the RazorViewEngineOptions. The custom expander needs to be added as last on the list to be considered the most "specific" one.
The following code snippet shows the code for the custom ViewLocationExpander, which instructs the view engine to look for files in the /Views/Themes/<ThemeName>/ folder and subfolders instead of just in /Views/.