Multiple Sites with One Authoring Domain in Sitecore


Sitecore allows you to manage multiple sites by binding a hostname to a root content item via the webconfig. This works fine in the delivery enviornment but for authoring you would also now need a seperate entry for each site if your site is dependent on the Sitecore.Context.Site at all.

Adding the configs settings isn’t a huge deal but if you have 50 sites you now need 50 config settings for the authoring urls, 50 for the editing urls, 100 dns entries and 50 authoring bookmarks for your authors.

I came up with the following approach to resolving the site in page editor and preview modes to help alleviate some of this pain. I can now use one authoring domain and when I push the Page Editor or Preview buttons Sitecore will resolve the site for the item.

To achieve this I did four things on top of the normal setup :

  • Added SiteResolver to resolve site based on Item if sc_itemid is in the Query String and we are in Page Editor or Preview mode.
  • Override the LinkProvider to include the sc_site in the Query String when in Page Editor or Preview Mode.
  • Turn Off Rendering.SiteResolving
  • Add A Site Entry for the “Authroing” site with the root of the home node.

Normal Setup
Root Content Items

IIS Site Bindings

Added Sites to web.config (notice host names and start items)


      <site name="website" hostName="training65" virtualFolder="/" physicalFolder="/" rootPath="/sitecore/content" startItem="/home" database="web" domain="extranet" allowDebug="true" cacheHtml="true" htmlCacheSize="10MB" registryCacheSize="0" viewStateCacheSize="0" xslCacheSize="5MB" filteredItemsCacheSize="2MB" enablePreview="true" enableWebEdit="true" enableDebugger="true" disableClientData="false" />
      <site name="site2"  hostName="site2.training" virtualFolder="/" physicalFolder="/" rootPath="/sitecore/content" startItem="/Site2" database="web" domain="extranet" allowDebug="true" cacheHtml="true" htmlCacheSize="10MB" registryCacheSize="0" viewStateCacheSize="0" xslCacheSize="5MB" filteredItemsCacheSize="2MB" enablePreview="true" enableWebEdit="true" enableDebugger="true" disableClientData="false" />
      <site name="site3"  hostName="site3.training" virtualFolder="/" physicalFolder="/" rootPath="/sitecore/content" startItem="/site3" database="web" domain="extranet" allowDebug="true" cacheHtml="true" htmlCacheSize="10MB" registryCacheSize="0" viewStateCacheSize="0" xslCacheSize="5MB" filteredItemsCacheSize="2MB" enablePreview="true" enableWebEdit="true" enableDebugger="true" disableClientData="false" />

With the setup above and I log into each domain and author the sites but I can’t author from one site and have the Site resolve correctly.
Authoring Multiple Sites from One Domain
First I add my new authoring site below my last site because I want the others to resolve first on the delivery side.

      <site name="website" hostName="training65" virtualFolder="/" physicalFolder="/" rootPath="/sitecore/content" startItem="/home" database="web" domain="extranet" allowDebug="true" cacheHtml="true" htmlCacheSize="10MB" registryCacheSize="0" viewStateCacheSize="0" xslCacheSize="5MB" filteredItemsCacheSize="2MB" enablePreview="true" enableWebEdit="true" enableDebugger="true" disableClientData="false" />
      <site name="site2"  hostName="site2.training" virtualFolder="/" physicalFolder="/" rootPath="/sitecore/content" startItem="/Site2" database="web" domain="extranet" allowDebug="true" cacheHtml="true" htmlCacheSize="10MB" registryCacheSize="0" viewStateCacheSize="0" xslCacheSize="5MB" filteredItemsCacheSize="2MB" enablePreview="true" enableWebEdit="true" enableDebugger="true" disableClientData="false" />
      <site name="site3"  hostName="site3.training" virtualFolder="/" physicalFolder="/" rootPath="/sitecore/content" startItem="/site3" database="web" domain="extranet" allowDebug="true" cacheHtml="true" htmlCacheSize="10MB" registryCacheSize="0" viewStateCacheSize="0" xslCacheSize="5MB" filteredItemsCacheSize="2MB" enablePreview="true" enableWebEdit="true" enableDebugger="true" disableClientData="false" />
      <site name="authoring" hostName="" virtualFolder="/" physicalFolder="/" rootPath="/sitecore/content" startItem="/home" database="web" domain="extranet" allowDebug="true" cacheHtml="true" htmlCacheSize="10MB" registryCacheSize="0" viewStateCacheSize="0" xslCacheSize="5MB" filteredItemsCacheSize="2MB" enablePreview="true" enableWebEdit="true" enableDebugger="true" disableClientData="false" />

Now if I login to editing.training domain it resolves to this authoring site that doesn’t contain a domain name. I can log in and Edit all three of my sites but if the sites are relying on anything contained in the Sitecore.Context.Site this information will be pointing to the Authoring Site and it will be wrong.

I remembered how the cross site links resolve to the correct domain when Rendering.SiteResolving is set to true so I reflected on that code and came up with a class (mostly copy pasta’d….including the code with the Go To Label) to resolve the Site for an Item without looking at the host name.

This first class is the Link Helper Class and it’s ResolveTargetSite method is what we need.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Sitecore.Web;
using Sitecore.IO;
using Sitecore.Sites;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;

namespace Sitecore.Prototypes.Links
{
    public static class LinkHelper
    {
        private static Dictionary<string, SiteInfo> _siteResolvingTable;
        private static List<SiteInfo> _sites;
        private static readonly object _syncRoot = new object();
        private delegate string KeyGetter(SiteInfo siteInfo);

        public static SiteInfo ResolveTargetSite(Item item)
        {
            SiteContext site = Context.Site;
            SiteContext context2 = site;
            SiteInfo info = (context2 != null) ? context2.SiteInfo : null;
            if ((context2 != null) && item.Paths.FullPath.StartsWith(context2.StartPath))
            {
                return info;
            }
            Dictionary<string, SiteInfo> siteResolvingTable = GetSiteResolvingTable();
            string key = item.Paths.FullPath.ToLowerInvariant();
        Label_00AE:
            if (siteResolvingTable.ContainsKey(key))
            {
                return siteResolvingTable[key];
            }
            int length = key.LastIndexOf("/");
            if (length > 1)
            {
                key = key.Substring(0, length);
                goto Label_00AE;
            }
            return info;
        }

        private static Dictionary<string, SiteInfo> GetSiteResolvingTable()
        {
            List<SiteInfo> sites = SiteContextFactory.Sites;
            if (!object.ReferenceEquals(_sites, sites))
            {
                lock (_syncRoot)
                {
                    if (!object.ReferenceEquals(_sites, sites))
                    {
                        _sites = sites;
                        _siteResolvingTable = null;
                    }
                }
            }
            if (_siteResolvingTable == null)
            {
                lock (_syncRoot)
                {
                    if (_siteResolvingTable == null)
                    {
                        _siteResolvingTable = BuildSiteResolvingTable(_sites);
                    }
                }
            }
            return _siteResolvingTable;
        }

        private static Dictionary<string, SiteInfo> BuildSiteResolvingTable(IEnumerable<SiteInfo> sites)
        {
            Dictionary<string, SiteInfo> dictionary = new Dictionary<string, SiteInfo>();
            List<string> list = new List<string>();
            KeyGetter[] getterArray = new KeyGetter[] { info => FileUtil.MakePath(info.RootPath, info.StartItem).ToLowerInvariant() };
            foreach (KeyGetter getter in getterArray)
            {
                foreach (SiteInfo info in sites)
                {
                    if (!string.IsNullOrEmpty(info.HostName) && string.IsNullOrEmpty(GetTargetHostName(info)))
                    {
                        Log.Warn(string.Format("LinkBuilder. Site '{0}' should have defined 'targethostname' property in order to take participation in site resolving process.", info.Name), typeof(LinkProvider.LinkBuilder));
                    }
                    else if ((!string.IsNullOrEmpty(GetTargetHostName(info)) && !string.IsNullOrEmpty(info.RootPath)) && !string.IsNullOrEmpty(info.StartItem))
                    {
                        string key = getter(info);
                        if (!list.Exists(new Predicate<string>(key.StartsWith)))
                        {
                            dictionary.Add(key, info);
                            list.Add(key);
                        }
                    }
                }
            }
            return dictionary;
        }

        private static string GetTargetHostName(SiteInfo siteInfo)
        {
            if (!string.IsNullOrEmpty(siteInfo.TargetHostName))
            {
                return siteInfo.TargetHostName;
            }
            string hostName = siteInfo.HostName;
            if (hostName.IndexOfAny(new char[] { '*', '|' }) < 0)
            {
                return hostName;
            }
            return string.Empty;
        }

    }
}

Next I create a custom SiteResolver and place it after the normal SiteResolver in the begin request pipeline.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web;
using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Web;
using Sitecore.Sites;

namespace Sitecore.Prototypes.Links
{
    public class SiteResolver : Sitecore.Pipelines.HttpRequest.SiteResolver
    {

        public override void Process(Pipelines.HttpRequest.HttpRequestArgs args)
        {
            if ((Sitecore.Context.PageMode.IsPreview || Sitecore.Context.PageMode.IsPageEditor) 
                && HttpContext.Current.Request.QueryString["sc_itemid"] != null)
            {
                Database db = Sitecore.Context.Database;
                if (db != null)
                {
                    Item item = db.SelectSingleItem(HttpContext.Current.Request.QueryString["sc_itemid"]);
                    if (item != null)
                    {
                        SiteInfo site = LinkHelper.ResolveTargetSite(item);
                        SiteContext siteContext = SiteContextFactory.GetSiteContext(site.Name);
                        UpdatePaths(args, siteContext);
                        Sitecore.Context.Site = siteContext;
                    }
                }
            }          
        }
    }
}

Add that to the httpRequestBeing pipeline right after the normal site resolver

        <processor type="Sitecore.Pipelines.HttpRequest.SiteResolver, Sitecore.Kernel" />
        <processor type="Sitecore.Prototypes.Links.SiteResolver, Sitecore.Prototypes.Links"/>

Now when I hit the Page Editor or Preview buttons the site is displayed using the correct Sitecore.Context.Site, but if I click on a link to another page it loses the context….

To fix this I added a custom link provider that appends the sc_site to the query string if we are in preview or pageditor mode.

Here is the class

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Sitecore.Sites;

namespace Sitecore.Prototypes.Links
{
    public class LinkProvider : Sitecore.Links.LinkProvider
    {

        public override string GetItemUrl(Data.Items.Item item, Sitecore.Links.UrlOptions options)
        {
            string url = base.GetItemUrl(item, options);
            if (Sitecore.Context.PageMode.IsPageEditor || Sitecore.Context.PageMode.IsPreview)
            {
                url = AppendOption(url, "&sc_site=" + LinkHelper.ResolveTargetSite(item).Name);
            }
            return url;
        }

        public override string GetDynamicUrl(Data.Items.Item item, Sitecore.Links.LinkUrlOptions options)
        {
            string url  = base.GetDynamicUrl(item, options);
            if (Sitecore.Context.PageMode.IsPageEditor || Sitecore.Context.PageMode.IsPreview)
            {
                url = AppendOption(url, "&sc_site=" + LinkHelper.ResolveTargetSite(item).Name);
            }
            return url;
        }

        public string AppendOption(string url, string option)
        {
            return url.Contains("?") ? url + option : url + "?" + option;
        }
    }
}

And here is where I overrode it in the config section

		<linkManager defaultProvider="authoring">
			<providers>
				<clear />
				<add name="sitecore" type="Sitecore.Links.LinkProvider, Sitecore.Kernel" addAspxExtension="false" alwaysIncludeServerUrl="true" encodeNames="true" languageEmbedding="asNeeded" languageLocation="filePath" shortenUrls="false" useDisplayName="false" />
        <add name="authoring" type="Sitecore.Prototypes.Links.LinkProvider, Sitecore.Prototypes.Links" addAspxExtension="false" alwaysIncludeServerUrl="true" encodeNames="true" languageEmbedding="asNeeded" languageLocation="filePath" shortenUrls="false" useDisplayName="false"  />
			</providers>
		</linkManager>

Now my last problem is that because Rendering.SiteResolving is set to true so my domain name is switching so we need to set that to false in the Authoring Environment while we probably want it true on the delivery side.

       <setting name="Rendering.SiteResolving" value="false" />

Notes

  • I wouldn’t expect this to work with the Shared Source DB Site Resolver and I’m not sure about the other Shared Source Multi-Site piece.
  • Posting code in wordpress is painfull.
  • So is copying from it so I’ll try to get a download up
About these ads

About Kevin Buckley
.Net web developer with a lot of experience in CMS. Currently working at Sitecore as Solutions Engineer.

2 Responses to Multiple Sites with One Authoring Domain in Sitecore

  1. John West says:

    Just noting that Sitecore’s source code does not necessarily contain the gotos and labels – the compiler optimized the code and the decompiler interpreted those optimizations, which leaves only a functional approximation of the original source.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

%d bloggers like this: