The main challenge of building an Optimizely CMS website is to think about its multi site capabilities up front. Making adjustment after the fact can be a difficult task and often requires a lot of refactoring.
In this blog post, I'll talk about a bit on how I have found a way to easily isolate a single website, structurally speaking, under a C# solution.
Of course, this implies a couple of modifications and is far from perfect. But I thought sharing my discovery to the community could help a lot.
Of course building the architecture of a solution to something that will easily accommodate future development is always a question of determining whether it's worth the upfront efforts and cost that it entails. If your client is sure to have more than a single site in your Optimizely CMS project, well then the question of correctly structuring your solution up front is the best you can do to reduce the development complexity with a bigger and bigger code base that does a lot in a single place.
This is something that we've come across quite often when clients were requesting us to make a new site under the same umbrella. What to do with the existing code base and avoid any regression while implementing the new website code base?
Well then, here's my solution to it: routing based on the site name configured under Optimizely CMS. This essentially allow you to create a completely separated C# library project and isolate every business logic into that single bucket for that single CMS site.
Obviously there is not only routing magic down there, but all in all, the goal was to modify every core elements of ASP.NET Core and Optimizely CMS that drives where and how certain elements are rendered.
By default, there is no controller for your content, you have to create one for your pages and your blocks. You are not forced to create a controller for your blocks, this is optional and also recommended by Optimizely.
Avoiding to use a controller just to change the location of the partial view requires an adjustment on the ASP.NET razor view engine. This is something that you can personalize by creating a class that implements a IViewLocationExpander
. That location expander needs to do a couple of things for us.
First making sure the order of the locations are to the most precise to the less precised one. In the logic then goes like that:
By the way, more information on the implementation will be shared later on. But you get the idea. This will essentially ask the view engine to look for locations that is only applicable for the site being requested. To give an example, this blog has been designed with that in mind. Here's a visual example of how the view structure looks:
As you can see, every views are inside a folder with the name of the site. None of them are outside of it. This means two things:
_ViewImports
and _ViewStart
file for a site. With this approach, the only thing needed to be done is to create a folder with the name of the site that will be created under Optimizely CMS and voila, you're good to go. The project you see in my previous screenshot doesn't contain a Startup.cs
. This is something only the starting assembly project have. This here is only a C# library project that is completely separated from the rest.
This is a good question. The short answer rely then again on an alteration to do on the ASP.NET Core routing engine.
Optimizely CMS uses what is known as the endpoint middleware. This is the reason why under your Startup.cs
file, you have to do something similar as that:
app.UseEndpoints(endpoints =>
{
endpoints.MapContent();
});
MapContent()
is essentially the method that hooks the entire CMS routing system on Microsoft's. Of course this is an oversimplification, but you get the picture. When the endpoint middleware is called, the matcher policies are called, and eventually the one from Optimizely is too. This consequently is used to load the correct page controller of the current http request.
So, knowing a matcher policy is checking if the content from the CMS can be loaded, I've hooked myself on that same system to add two variables in the route values:
The MultiSiteMatcher
is adding those routing values only after the ContentMatcherPolicy
from Optimizely is running, making sure it is able to grab the correct data while adding them in the routing values.
The other challenge with multi sites are the static assets. The classic setup with Optimizely CMS is to create a folder for a specific site under wwwroot
and then store all your assets in it for that site. All in all, you might want different scripts, styles, fonts, etc. and you don't necessarily want to share them with other sites.
With this setup though, two things are happening:
Of course these problems are mainly cosmetic, but I felt like I needed to close the loop with this whole multi site thing correctly.
There is a way to customize that and still keep a good separation between sites. By implementing a new IFileProvider
. Essentially there it's quite simple, this file provider is trying to find a file directly from a subpath of the requested site and if it cannot be found, fallback to the out of the box file provider.
All these concepts were implemented during the development of my blog. I have recently separated this logic into a NuGet package if you are interested.
Please let me know what you think of! Of course if you'd like to participate and contribute to the project, please let me know via LinkedIn!