Traditional Sitecore asp.net supports multi domain binding and it’s built to render site based on requested domain. It can easily be configured using a patch configuration as shown below,
<?xml version="1.0"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
<sitecore>
<sites>
<site name="mysite" patch:after="site[@name='modules_website']"
targetHostName="rhino.acme.com”
enableTracking="true" virtualFolder="/" physicalFolder="/"
rootPath="/sitecore/content/mysite"
startItem="/home" database="web" domain="extranet"
allowDebug="true" cacheHtml="true" htmlCacheSize="50MB" registryCacheSize="0"
viewStateCacheSize="0" xslCacheSize="25MB" filteredItemsCacheSize="10MB"
enablePreview="true" enableWebEdit="true" enableDebugger="true"
disableClientData="false" cacheRenderingParameters="true"
renderingParametersCacheSize="10MB" />
</sites>
</sitecore>
</configuration>
Unfortunately Rendering Host though built on Asp.net core and can be hosted on IIS - it doesn't support multiple domains. In this blog, we can see how it can be customized to support multi domains or multi sites.
To checkout below steps - please clone this repository https://github.com/nkdram/MVP-Site which is actually a forked version from actual repository.
1. Setup
The primary step is to do a setup with multiple domains. I have used this repository “Sitecore MVP Site” built by Rob Earlam and few other wonderful contributors !! As it’s build using Asp.net core Rendering host and Sitecore Headless service, I’ll be using this solution to demonstrate multi-site support. The repo is well documented and can be easily cloned and run.
Once cloned, let’s make some changes to few files to add another hostname – let’s call it “regional-mvp.sc.localhost”.
- Init.ps1 : add below line just after $renderingHost
$regionalRenderingHost = “regional-mvp.sc.localhost” - .env : Add env setting as below,
RENDERING_HOST_RegionalSite=regional-mvp.sc.localhost - docker-compose.override.yml : Add below traefik labels to route requests from our new host name.
– “traefik.http.routers.regional-secure.entrypoints=websecure”
– “traefik.http.routers.regional-secure.rule=Host(`${RENDERING_HOST_RegionalSite}`)”
– “traefik.http.routers.regional-secure.tls=true”
Sites will automatically load up when the up.ps1 is run and rendering host site should be rendered in browser.
Once backend is loaded, duplicate the MVP site and let’s call it Regional-Mvp site.


Note : Duplicating site will not change datasource for renderings in page - Make sure to update them.

2. Extend Rendering-Host Project layer to pass-in multi-sites
We need to configure project layer to have multi-sites configured, so let’s start from “appsettings.json”. This is where multiple sites are configured – I chose to have it as dictionary entries as shown below, so it is easier to map it based on requested hostname.
{
"Sitecore": {
"InstanceUri": "http://mvp-cd",
"LayoutServicePath": "/sitecore/api/layout/render/jss",
"DefaultSiteName": "mvp-site",
"ApiKey": "{E2F3D43E-B1FD-495E-B4B1-84579892422A}",
"Sites": {
"mvp.sc.localhost": "mvp-site",
"regional-mvp.sc.localhost": "regional-mvp-site"
}
},
"Logging": {
"LogLevel": {
"Default": "Debug",
"Microsoft": "Information",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*"
}
Now we need to pass this value to SitecoreOptions which is nothing but a serialized object based on appsetting configuration. Creating a dictionary and calling it as ‘Sites’. This will hold sites that are configured in appsettings.
public class SitecoreOptions
{
public static readonly string Key = "Sitecore";
public Uri InstanceUri { get; set; }
public string LayoutServicePath { get; set; } = "/sitecore/api/layout/render/jss";
public string DefaultSiteName { get; set; }
public string ApiKey { get; set; }
public Uri RenderingHostUri { get; set; }
public bool EnableExperienceEditor { get; set; }
public Dictionary<string, string> Sites { get; set; }
public Uri LayoutServiceUri
{
get
{
if (InstanceUri == null) return null;
return new Uri(InstanceUri, LayoutServicePath);
}
}
}
3. Adding a MultiSite Foundation Layer to extend Rendering Host
The next step would be to extend SitecoreLayoutRequest and to create a custom Rendering Engine Pipeline to pass on Site information based on requested url.
Let’s look on how to pass sites by extending SitecoreLayoutRequest. As it is nothing but an inherited Dictionary<string,object> object, we could easily store the sites.
public static class LayoutRequestExtensions
{
public static Dictionary<string, string>? Sites(this SitecoreLayoutRequest request)
{
return ReadValue<Dictionary<string, string>?>(request, "sc_sites");
}
public static SitecoreLayoutRequest Sites(this SitecoreLayoutRequest request, Dictionary<string, string>? value)
{
return WriteValue(request, "sc_sites", value);
}
private static T ReadValue<T>(SitecoreLayoutRequest request, string key)
{
if (request.TryReadValue<T>(key, out var value))
{
return value;
}
return default(T);
}
private static SitecoreLayoutRequest WriteValue<T>(SitecoreLayoutRequest request, string key, [AllowNull] T value)
{
request[key] = value;
return request;
}
}
The sites can now be passed from SitecoreOptions to SitecoreLayoutRequest in startup.cs as shown below,
// Register the Sitecore Layout Service Client, which will be invoked by the Sitecore Rendering Engine.
services.AddSitecoreLayoutService()
// Set default parameters for the Layout Service Client from our bound configuration object.
.WithDefaultRequestOptions(request =>
{
request
.SiteName(Configuration.DefaultSiteName) //register default site
.ApiKey(Configuration.ApiKey)
.Sites(Configuration.Sites); // register other sites
})
.AddHttpHandler("default", Configuration.LayoutServiceUri)
.AsDefaultHandler();
As we now have Sites that are passed during App startup, the next step would be to create a custom Rendering engine that would use a custom Middleware and utilize the Sites information and resolve site based on Requested Url.
The CustomRenderingEnginePipeline is inherited from RenderingEnginePipeline as Configure can be overridden as shown below.
public class CustomRenderingEnginePipeline : RenderingEnginePipeline
{
public override void Configure(IApplicationBuilder app)
{
//Register Custom Rendering Engine Middleware
app.UseMiddleware<CustomRenderingEngineMiddelware>(Array.Empty<object>());
}
}
As middleware methods cannot be overridden, I had to completely create a custom one based on RenderingEngineMiddleware and do some tweaks to pass Site information based on requested url as highlighted in the below code.
public class CustomRenderingEngineMiddelware
{
...
private async Task<SitecoreLayoutResponse> GetSitecoreLayoutResponse(HttpContext httpContext)
{
SitecoreLayoutRequest sitecoreLayoutRequest = _requestMapper.Map(httpContext.Request);
//Update LayoutRequest Sitename based on hostname from httpcontext
sitecoreLayoutRequest = ResolveSiteNameBasedOnHost(httpContext, sitecoreLayoutRequest);
return await _layoutService.Request(sitecoreLayoutRequest).ConfigureAwait(continueOnCapturedContext: false);
}
private SitecoreLayoutRequest ResolveSiteNameBasedOnHost(HttpContext httpContext, SitecoreLayoutRequest sitecoreLayoutRequest)
{
Dictionary<string, string>? sites;
string siteName = "", currentSitename = "";
//Getting private _layoutRequestOptions and then accessing SitecoreLayoutRequestOptions.RequestDefaults
FieldInfo field = typeof(DefaultLayoutClient).GetField
("_layoutRequestOptions", BindingFlags.Instance | BindingFlags.NonPublic);
IOptionsSnapshot<SitecoreLayoutRequestOptions> sitecoreLayoutRequestOptions = (IOptionsSnapshot<SitecoreLayoutRequestOptions>)field.GetValue(_layoutService);
if (sitecoreLayoutRequestOptions.Value.RequestDefaults.TryReadValue<Dictionary<string, string>?>("sc_sites", out sites) && sitecoreLayoutRequestOptions.Value.RequestDefaults.TryReadValue<string>("sc_site", out currentSitename))
{
string hostName = httpContext.Request.Host.Value;
siteName = sites.ContainsKey(hostName) ? sites[hostName] : "";
if (currentSitename.ToLower() != siteName.ToLower())
{
sitecoreLayoutRequestOptions.Value.RequestDefaults["sc_site"] = siteName;
//finaly update sitecorelayout request defaults
field.SetValue(_layoutService, sitecoreLayoutRequestOptions);
//also update in layoutrequest
sitecoreLayoutRequest.SiteName(siteName);
}
}
return sitecoreLayoutRequest;
}
}
Now that the RenderingEngine and Middleware is created, next step would be to update DefaultController’s Index action to use it. This can be done using UseSitecoreRenderingAttribute.
// Injecting Custom Middleware that overrides Site based on Requested URL
[UseSitecoreRenderingAttribute(typeof(CustomRenderingEnginePipeline))]
public IActionResult Index(Route route)
{
var request = HttpContext.GetSitecoreRenderingContext();
if (request.Response.HasErrors)
{
foreach (var error in request.Response.Errors)
{
switch (error)
{
case ItemNotFoundSitecoreLayoutServiceClientException notFound:
Response.StatusCode = (int)HttpStatusCode.NotFound;
return View("NotFound", request.Response.Content.Sitecore.Context);
case InvalidRequestSitecoreLayoutServiceClientException badRequest:
case CouldNotContactSitecoreLayoutServiceClientException transportError:
case InvalidResponseSitecoreLayoutServiceClientException serverError:
default:
throw error;
}
}
}
return View(route);
}
4. Add Site and app entry
Now to the final step ! We just need to site and app entry to map with the newly duplicated site.
<sitecore>
<sites>
<site name="mvp-site"
inherits="website"
hostName="mvp-cm.sc.localhost"
rootPath="/sitecore/content/MvpSite"
dictionaryDomain="{DB704D9E-113D-44A1-AA85-2A5D127CD2A3}"
patch:before="site[@name='website']" />
<site name="regional-mvp-site"
inherits="website"
hostName="mvp-cm.sc.localhost"
rootPath="/sitecore/content/Regional-MvpSite"
dictionaryDomain="{DB704D9E-113D-44A1-AA85-2A5D127CD2A3}"
patch:before="site[@name='website']" />
</sites>
<javaScriptServices>
<apps>
<!--
We need to configure an 'app' for the site as well in order to
enable support for Experience Editor. The URL below will be used
by the Experience Editor to render pages for editing.
-->
<app name="mvp-site"
sitecorePath="/sitecore/content/MvpSite"
serverSideRenderingEngine="http"
serverSideRenderingEngineEndpointUrl="http://mvp-rendering/jss-render"
inherits="defaults" />
<app name="regional-mvp-site"
sitecorePath="/sitecore/content/Regional-MvpSite"
serverSideRenderingEngine="http"
serverSideRenderingEngineEndpointUrl="http://mvp-rendering/jss-render"
inherits="defaults" />
</apps>
</javaScriptServices>
</sitecore>
To view the sites, let's run up.ps1 script and test them !! It should now resolve to sites based on requested domain.

Finally it should also work in Experience Editor and making multisite editing feasible using a single Rendering Host !!!!

P.S : Thanks @NickWesselman for showcasing how custom rendering engine can be avoided as there is options.MapToRequest to replace sitename !!!
https://gist.github.com/nickwesselman/f670449840787f471ad6e4da2cc43399.js
1 comments on “Extending Sitecore Rendering Host to support multisite”