Episerver multi-site best practices/approaches?


Currently, our single-project Episerver solution serves up a single website and has been working great. Soon we'll be developing an additional, separate Episerver site that will need to live within the same solution but I would love for the two codebases to remain separate while still sharing the same Episerver DB. For the site that currently exists, we have a lot of page/block models, so I expect any refactoring will be no light lift.

Is there any documentation/guidance, aside from this, that details out best practices on how to separate out the models, controllers, views, etc for each site into their own project? 

Is this better achieved through the use of Areas instead of separate projects? Some other approach I'm not considering?

Any help and/or discussion around this is greatly appreciated. 

Edited, Jan 11, 2022 8:34


Jan 11, 2022 8:50

Hi! I want to start by saying that you should prepare for a significant amount of work if you want to accomplish this. Since the sites share the same DB, all content types from all projects will of course be synchronized. You will have to create custom implementations for a few services to be able to use different pages/blocks per site, one of them is ContentTypeAvailabilityService. If you need any shared pages/blocks you might have to create some feature switching between the sites, and a ViewEngine that prioritize views from the executing site etc.

I have personally implemented a multi site solution successfully which prioritizes controllers and views from within a site folder by matching folder name with executing site name. I used one codebase for this though. This was quite complex and took me a while to achieve, and it involved creating a custom ViewEngine, ITemplateResolver, ContentTypeAvailabilityService etc. The documentation was quite poor at that time so I had to do a lot of digging and trial and error.

Here is one link, quite old, but should still be relevant and of help if you need to create a custom ContentTypeAvailabilityService:


Edited, Jan 11, 2022 9:43
Mark Stott - Jan 11, 2022 10:16
I had a read through of your blog, I especially like the custom attribute to hide fields from shared content types based on the site you're on.

I'm involved in a similar undertaking right now.  Some of the strategies we're using include:

  • Using interfaces to decorate which sites a page or block can be used on.
    • e.g. ISiteOnePage, ISiteTwoPage, ISiteOneBlock, ISiteTwoBlock
    • You'll need a UIDescriptor to ensure that the CMS can recognise the interfaces when restricting which content types can be placed where based on AvailableContentTypes and AllowedTypes attributes.
    • This also allows you to use the same content type on either site by applying both interfaces.
  • Using a different content types for each start page.
    • This along with SiteDefinition.Current can be used to determine which core styles or parent layout razor file to use.
  • Refactoring existing code into a feature folder structure with roots for each site and a common route.
    • e.g. /features/siteone/featureone/ and /features/sitetwo/featuretwo/ and /features/common/
    • You could go as far as to make these separate projects for a clearer definition.
Jan 11, 2022 9:49

For a new CMS 12 solution we are going to separate code by using Razor Class Libraries (RCL) (how you should do add-ons now as well) and have a layered structure. In razor class libs views and static files are out-of-the-box compiled as well (with some caveats regarding location of views).

Our website project only has add-on packages, appsettings and the startup.cs logic and references to the rest.
We have a foundation RCL project that is shared code (alloy/foundation infrastructure like functionality, base models etc.)
Sites or/and feature are then spiit into separate feature RCL
Then we also have planned some core libraries (Clean/Onion Architecture) that have no deps to episerver etc.

It looks promising at this point :)

I am guessing it will only work for CMS12+ and will of course require some refactoring if you have a solution already.

Jan 12, 2022 14:33
Mark Stott - Jan 12, 2022 15:19

I've just built a module using Razor Class Libraries and they are rather nice to use: https://nuget.optimizely.com/package/?id=Stott.Optimizely.RobotsHandler #ShamelessPlug

I've also been talking about using these within the agency I work in as an approach to separation of functionality as reusable libraries for multiple builds.  I'm a big fan of Feature Slices as apposed to N-Tier onions. I like how feature slicing is coupling within a feature and decoupling across features rather than coupling across layers but decoupling between layers that comes with a typical N-Tier / Onion approach.  The feature slicing approach does reduce risk and testing requirements for rapid changes.

It would be interesting to see how your approach works, though we can obviously talk theoretically only :D
Rajveer Singh - Jan 12, 2022 15:43
Thanks for the solution, but i am on EPi 11.

To add to this, this template selection approach might also be useful for start pages in a multi-site solution:


This could also be slightly modified so a site specific template is automatically selected based on current site context, maybe something like this:

public class TemplateForSitesAttribute : Attribute
    public TemplateForSitesAttribute(params string[] siteNames)
        SiteNames = siteNames;

    public readonly string[] SiteNames { get; }

[TemplateDescriptor(Name = "My first start page template", Default = true)]
[TemplateForSites("SiteName1", "SiteName2")]
public class StartController : PageController<StartPage>

[ContentType(DisplayName = "Start page")]
public class StartPage : PageData, IDynamicTemplateContent
    public void SetDynamicTemplate(TemplateResolverEventArgs args)
        if (SelectedTemplateIsPreviewController(args.SelectedTemplate))

        TemplateModel selectedTemplate = args
            .FirstOrDefault(tmpl => IsForCurrentSite(tmpl));

        if (selectedTemplate != null)
            // A template targeting current site was found. Switch to it.
            args.SelectedTemplate = selectedTemplate;

    private bool SelectedTemplateIsPreviewController(TemplateModel selectedTemplate)
        if (selectedTemplate == null || selectedTemplate.Tags == null)
            return false;

        return Array.IndexOf(selectedTemplate.Tags, RenderingTags.Preview) > -1;

    private bool IsForCurrentSite(TemplateModel templateModel)
        var templateForSites = templateModel.TemplateType.GetCustomAttribute<TemplateForSitesAttribute>();

        if (templateForSites != null)
            return templateForSites.SiteNames.Contains(SiteDefinition.Current.Name);

        return false;

You could of course also use interfaces to decorate your controllers to be able to select the correct one for executing site.

Edited, Jan 13, 2022 10:33

Lot of good ideas above. Just wanted to share a few more blog posts talking about different approaches:

Jan 13, 2022 18:41
* You are NOT allowed to include any hyperlinks in the post because your account hasn't associated to your company. User profile should be updated.