Multi-site Episerver Solutions using MVC Areas

March 2022 Update: This post was originally written for EPiServer version 11. In 2021, Episerver (now known as Optimizely) released version 12. No content in this post has been changed for the current version.

In my last blog post from many years ago, I provided various ways that a multi-site Episerver solution could be architected. If you haven't read that post yet, you should do so before continuing with this one.

One of the ways I mentioned is by using MVC Areas, which Episerver does support with a little bit of work. In the time that post was written, I've gotten to build a couple multi-site Episerver solutions using MVC Areas, and I'd like to share some of my approaches to make the development and content editing process smoother.

This post is the first in a series of posts related to architecting and developing multi-site Episerver solutions using MVC Areas. In this post, I'll cover the high-level architecture decisions we've made, and what was involved in creating that base framework.

Decisions to be made

For the context of this architecture, we're working on a solution that serves up three different sites. Each site's design is different, and doesn't share any creative elements between the sites. Because of this, we've completely separated the content types and templates for each site into its own MVC Area. In a perfect world, a content editor for one site wouldn't see anything related to the other sites while in Episerver's editor UI, but we know the page tree and the asset panel prevents that.

You may choose to not fully separate each site, and maybe share some content types between the sites to reduce redundancy. If that's the case, you might expand upon this architecture to have code live in a shared MVC Area, or completely outside of the Areas in a more global scope.

I'll also mention the we're working on solution isn't terribly complex. All sites are CMS only, with just a handful of content editors. The sites are bigger than your standard marketing landing page, but nothing involving third-party integrations or pages behind login. The development team is small, but big enough where we could put a front-end developer and a couple back-end developers on each site. The team only needs to concern themselves with the code in their MVC Area. So for this particular solution, MVC Areas makes sense.

Pieces to the puzzle

We chose to architect this solution in a way that relies on conventions and standards to ensure each site is properly separated from each another. This requires some overhead and a bit more foundational code, but in the end, we hope that content editors only see the content types that's meant for the site they are working on.

Naming conventions

We created a set of naming conventions for all classes that control the foundational architecture of the solution:

  • If the class is meant for all sites, the class name starts with the word "global", or if it's specific to a site, the class name starts with the site's Area name. Global files live outside of the MVC Areas.
  • If the class is abstract and not meant to be used directly, the class name includes the word "base".
  • The remainder of the class name is specific to it's usage.

You'll see this naming convention in the example code below.

Constants

We have a constants file for each site. For the purpose of the following examples, we just need one constant: the folder name of the MVC Area.

namespace MySolution.Areas.SiteA.Business
{
    public static class SiteAConstants
    {
        public const string AreaName = "SiteA";
    }
}

We'll use this constant in many places throughout the foundational architecture, as well as for functionality in future blog posts.

Base classes

We use base content type models for each site in the solution:

namespace MySolution.Areas.SiteA.Models.Pages.Base
{
    [AvailableContentTypes(Include = new[] { typeof(SiteABasePageData) })]
    public abstract class SiteABasePageData : GlobalBasePageData
    {
        ...
    }
}

namespace MySolution.Areas.SiteA.Models.Blocks.Base
{
    public abstract class SiteABaseBlockData : GlobalBaseBlockData
    {
        ...
    }
}

There's nothing fancy in the GlobalBasePageData and GlobalBaseBlockData classes; they just inherit PageData and BlockData, respectively.

The main reason for this is the AvailableContentTypes class decoration, which limits the page types an editor can choose in the "New Page" UI screen. For limiting block types, this will be discussed in a future blog post.

We also have base controller classes for each site, which sets us up for actually making Episerver support MVC Areas:

namespace MySolution.Areas.SiteA.Controllers.Base
{
    public abstract class SiteABasePageController<T> : GlobalBasePageController<T>
        where T : SiteABasePageData
    {
        ...
    }
}

namespace MySolution.Areas.SiteA.Controllers.Base
{
    public abstract class SiteABaseBlockController<T> : GlobalBaseBlockController<T>
        where T : SiteABaseBlockData
    {
        ...
    }
}

Once again, nothing fancy in GlobalBasePageController and GlobalBaseBlockController; they just inherit PageController and BlockController, respectively.

GroupName

When we define our content types, we provide a GroupName in the ContentType attribute:

namespace MySolution.Areas.SiteA.Models.Pages
{
    [ContentType(DisplayName = "Standard Page", GUID = "00000000-0000-0000-0000-000000000000",
        GroupName = SiteAConstants.AreaName)]
    public class StandardPage : SiteABasePageData
    {
        ...
    }
}

This helps to have a bit more organization in both the editor UI and the administrator UI.

One thing to note here: We're not uniquely naming our content types, as so far we haven't had an issue with multiple sites using the same content type name.

RouteData

The key to making all of this work is setting the Area name in the RouteData. This is done in both of our base controller classes using the OnActionExecuting method:

protected override void OnActionExecuting(ActionExecutingContext filterContext)
{
    base.OnActionExecuting(filterContext);

    filterContext.RouteData.DataTokens["area"] = SiteAConstants.AreaName;
}

Doing this also allows us to clean up the default AreaRegistration class that's generated when you create a new MVC Area:

namespace MySolution.Areas.SiteA
{
    public class SiteAAreaRegistration : AreaRegistration
    {
        public override string AreaName => SiteAConstants.AreaName;

        public override void RegisterArea(AreaRegistrationContext context)
        {
        }
    }
}

What's Next?

While this certainly isn't the full picture on creating multi-site Episerver solutions using MVC Areas, it gets us started down that path. There's more to consider when it comes to things like block preview, dependency injection, and default controllers, which I'll cover in future posts.

Until then, if you'd like to dig more into how you can use MVC Areas for a multi-site Episerver solution, check out a couple posts by Episerver MVP and Microsoft MVP, Valdis Iljuconoks:

  • https://blog.tech-fellow.net/2015/01/21/full-support-for-asp-net-mvc-areas-in-episerver-7-5/
  • https://blog.tech-fellow.net/2015/08/10/asp-net-mvc-areas-in-episerver-part-2/

His posts are much more in-depth about this topic, and may provide additional details and context on how all of this works.

More posts in this series: