Living in a Multi-World - Sites, Queries, Standard Values, and Personalization

|
Comments
(0)

Introduction

As a Sitecore developer, we always want to keep personalization in mind.   Which means, we also want to keep the idea of "datasources" in mind.    

On a standard website, headers and footers with datasources seem easy enough.    Just set the datasources for header and footer on every page's standard values, and call it done.   Publish to live.    If personalization is needed, we can just go to the standard values again and set the personalization there.   It would, of course, carry over to the other pages that the template is based off of.   

But, imagine a world where a client wants something more than one website.    They want a "multi"-site application.  

This idea of multisite actually isn't that uncommon.   Sitecore is a pretty robust software, and this can be done by the proper implementation company.   But things become a bit confusing and convoluted when you throw the ideas of multisite, datasources, personalization, and standard values into a pot and expect perfect results.  Even taking "standard values" out of this mix, there are still custom Sitecore pipelines that actually need to be done to completely support multisite and datasources.  

We actually ran into this when implementing our product SAFIC.  And is now a built in feature of SAFIC.  

This blog will focus on the quartet mentioned above.   And will list off the customizations & changes required to an OOB (Out of the Box) Sitecore solution.  Without these customizations, each datasource & personalization would actually have to be set on a per-page basis, which would be an incredible amount of work. 

Initial Setup

So, lets say we already have pages and a multisite application.    And the pages are based off of three templates: Homepage Template, Site Section Template, and Generic Page Template.   These standard values have personalization set on them already for the header: one for Logged in View, one for Unauthenticated User.   Right now, without customizations, personalization would be a ton of work for each multisite.  We are talking hours of content authoring....

Below is what it looks like initially.   These are SAFIC screenshots, but they work well for this example because SAFIC already has this implemented.   (In SAFIC, the other page templates are SAFIC Product Template and SAFIC Product Classification) 

      

 

Step #1: Custom Pipeline for Datasource Query

The customization starts in a usual Sitecore way of customizing the application: a Pipeline.  

We created a "QueryableDatasourceProcessor" that renders after the "EnterRenderingContext" processor.   This is to properly resolve the sitecore item when a Sitecore query is used in the rendering datasource of the presentation details.  

Below is what the config patch file looked like, followed by the QueryableDatasourceProcessor.cs file.   There is an additional "SetFallbackSiteNodeDatasourceLocations" included in the config below, which relates to Step #2.    

<?xml version="1.0"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">   <sitecore>     <pipelines>
      <!-- Fix for properly resolving renderings when a Sitecore query is used for a rendering datasource -->       <mvc.renderRendering>         <processor patch:after="*[@type='Sitecore.Mvc.Pipelines.Response.RenderRendering.EnterRenderingContext, Sitecore.Mvc']" type="Safic.Web.Core.Pipelines.QueryableDatasourceProcessor, Safic.Web.Core" />       </mvc.renderRendering>
      <!-- Fix for properly creating new content on category and product items -->       <getRenderingDatasource>         <processor patch:before="*[@type='Sitecore.Pipelines.GetRenderingDatasource.GetDatasourceLocation, Sitecore.Kernel']" type="Safic.Web.Core.Pipelines.SetFallbackSiteNodeDatasourceLocations, Safic.Web.Core" />       </getRenderingDatasource>
    </pipelines>   </sitecore> </configuration>

 

Here is the QueryableDatasourceProcessor.cs.   There is an added "bonus" of the first "else", which is for when the page data item in question cannot resolve who its "Site Node" is (which wouldn't happen in this current example outlined, but WOULD happen if you are dealing with a Product Page that is inside a Product Repository).  

    public class QueryableDatasourceProcessor : RenderRenderingProcessor
    {
        public override void Process(RenderRenderingArgs args)
        {
            var dataSource = args.Rendering.DataSource;
            if (!dataSource.StartsWith("query:")) return;
            var query = dataSource.Substring("query:".Length);
            var queryItem = args.PageContext.Item.Axes.SelectSingleItem(query);
            if (queryItem != null)
            {
                args.Rendering.DataSource = queryItem.Paths.FullPath;
            }
            else
            {
                //If Ancestor doesn't work, get current Site's Start Path + append the rest of the query to it....
                var currentWebsiteRoot = Sitecore.Context.Data.Site.ContentStartPath;
                var newQuery = query.Replace("./ancestor-or-self::*[@@templateid='" + websiteNoteTemplateId + "']", currentWebsiteRoot);

                var newQueryItem = args.PageContext.Item.Axes.SelectSingleItem(newQuery);
                if (newQueryItem != null)
                {
                    args.Rendering.DataSource = newQueryItem.Paths.FullPath;
                }
            }
        }
    }

  

Step #2: Update Rendering Queries for Datasource

Now that a query can be manually set in the presentation details, what about when new items are created?    For example, you are on Site C of the multisite and want to create a banner, how does the application know exactly where to put the new Data item when it is created in the Experience Editor?   Out of the Box, you would be using a hard coded link to Site A most likely, and anything created in the other websites would go to Site A's Site Data folder.  

But it doesn't have to be this way.   You can actually use queries in the datasource location too.   As detailed at Fire Breaks Ice: Sitecore Rendering Datasource Locations and as seen below.

But, what about when the particular "Site Data" item you are wanting to create is on a page that is outside of the Site Node....  For example a Product Page in the Product Repository?   Below is an added bonus of "setting the fallback site node datasource location for when the site node is not found", which is conspicuously named SetFallbackSiteNodeDatasourceLocations.cs mentioned above in Step #1.   

SetFallbackSiteNodeDatasoureceLocation.cs:

        public void Process(GetRenderingDatasourceArgs args)
        {
            Assert.IsNotNull(args, "args");

            var productTemplateId = "INSERT-GUID-HERE";
            var categoryTemplateId = "INSERT-GUID-HERE";
            var contextItem = args.ContentDatabase.GetItem(args.ContextItemPath);

            if (contextItem == null || !contextItem.TemplateID.ToString().ToLower().Equals(productTemplateId.ToLower()) && !contextItem.TemplateID.ToString().ToLower().Equals(categoryTemplateId.ToLower()))
                return;

            if (args.DatasourceRoots.Count > 0)
                return;
            foreach (var location in new ListString(args.RenderingItem["Datasource Location"]))
            {
                if (location.StartsWith("query:", StringComparison.InvariantCulture))
                {
                    AddRootsFromQuery(location.Substring("query:".Length), args);
                }
            }
        }

        private static void AddRootsFromQuery(string query, GetRenderingDatasourceArgs args)
        {
            Assert.ArgumentNotNullOrEmpty(query, "query");
            Assert.ArgumentNotNull(args, "args");

            var objArray = (Item[]) null;
            var site = Sitecore.Sites.SiteManager.GetSites().FirstOrDefault(s => s.Properties["hostName"] == HttpContext.Current.Request.Url.Host);

            if (site != null)
            {
                var currentWebsiteRoot = site.Properties["rootPath"];
                var newQuery = query.Replace("./ancestor-or-self::*[@@templateid='" + websiteNoteTemplateId + "']", currentWebsiteRoot);
                var obj = args.ContentDatabase.GetItem(args.ContextItemPath);

                if (obj != null)
                    objArray = obj.Axes.SelectItems(newQuery);
            }

            if (objArray == null)
                return;

            foreach (var obj in objArray)
                args.DatasourceRoots.Add(obj);
        }
    } 

 

Step #3: Same-Name Conventions for Queried Datasources

Before we continue, we will have to decide on exact naming conventions for the datasources that will be used in this query.   Which means, each site will be expected to have the same-named item in the other's Site Data folders.   Example can be seen below, each website will have an item named "Authenticated Menu" or "General Footer"

 

Step #4: Update Datasource Queries for Standard Values

Now that the customizations are laid out, and we have the same-named conventions, we can set the header, logo, footer, and any other datasource's queries in the standard values, as seen below.   This step will help Site A, Site B, and Site C find it's proper headers.  

  

For the items that have personalization, things get a bit complicated, as you can see in Step #5.  

Step #5: Update Datasource Queries for Personalized Items on Standard Values

Up until now, we were able to use Sitecore's features without doing a "developer-esque" thing like turning on Raw Values of fields.    But in this step, we will need to look at those raw values and disect them.    If using the trio of multisite, queries, and standard values is good enough and you do not need the personalization mixed in, please do not continue.  

If you are sure, please continue.  

If you want to make the personalization rules set on standard values choose the correct site's header based on the Same Name conventions, please you will first have to go to Standard Values of the pages and turn on the "Raw Values."  

Below is what you see in the Renders field, after it has been formatted to read easier.  

<?xml version="1.0" encoding="UTF-8"?>
<r xmlns:xsd="http://www.w3.org/2001/XMLSchema">
   <d id="{FE5D7FDF-89C0-4D99-9AA3-B5FBD009C9F3}" l="{D6B5FA54-08EC-4221-8F5A-2C3A7EAE285F}">
      <r ds="" id="{24C2DC01-B8BE-4C0B-AA59-EB6AFE5FE1E3}" par="" ph="/main" uid="{2DC56632-CE26-4921-9EBB-65C32CD714D0}" />
      <r id="{BFE6E6C5-9483-4CC3-A4CF-FC5D1A47F51B}" ph="/main/page-frame-container-content" uid="{C92049FD-6745-4474-B8C2-8EE7E2FBA5EA}" />
      <r ds="" id="{7C83466A-5866-4F8A-8B91-D85A36109805}" par="" ph="notification-area" uid="{43FC41F8-E50B-4601-B6AC-09D9C971F78C}" />
      <r id="{A155D53C-6066-40C6-A207-DA5A380FC0F3}" ph="main" uid="{EC239762-5E0C-4EAC-A797-6AC7FB7CB016}" />
      <r ds="query:./ancestor-or-self::*[@@templateid='{3578A109-09C3-4F70-A54B-8C611390E639}']/Site Data/Shared Controls/Search/Default Search" id="{52E53AA7-EF34-4E99-B106-BF3D63EB0796}" par="" ph="/header/header-right-content" uid="{5B045816-E71B-4895-8CDD-DD385A19302F}" />
      <r ds="query:./ancestor-or-self::*[@@templateid='{3578A109-09C3-4F70-A54B-8C611390E639}']/Site Data/Shared Controls/Logos/Default Logo" id="{5FF1156C-697A-42AC-B34C-ECC4521A7BD5}" par="" ph="/header/header-left-content" uid="{06A3BBBC-8C33-455B-B80A-5E3865F03D08}" />
      <r ds="{D943A16F-E4B2-431D-AA1D-1E014F4B28E3}" id="{ED50BD53-4B5D-4AC0-895F-700564830944}" par="" ph="header" uid="{8308B126-7561-4ACF-A8AF-A7F27C77C395}" />
      <r ds="query:./ancestor-or-self::*[@@templateid='{3578A109-09C3-4F70-A54B-8C611390E639}']/Site Data/Shared Controls/Main Navigations/Unauthenticated Menu" id="{47F7A7A7-2F3C-4BFE-8FA6-6B51B147CACD}" par="" ph="header" uid="{8A2037D6-5CE5-45EA-AAD4-5685178BD669}">
         <rls>
            <ruleset>
               <rule name="Guest Checkout Only" uid="{C3FC0B6D-4363-4AC8-879A-9D99D09FC362}">
                  <actions>
                     <action id="{0F3C6BEC-E56B-4875-93D7-2846A75881D2}" uid="379C40C7603C48CAAACBCC4EE03B797F" DataSource="{B98E5C7B-CCA1-47F7-8673-10F99D3A6285}" />
                  </actions>
                  <conditions>
                     <condition id="{D687DB1C-F2EB-425B-A7CD-1D379EC59167}" uid="3930E0334833420CA3A403476F1A323D" />
                  </conditions>
               </rule>
               <rule name="Authenticated Customer" uid="{D80CE89F-90A9-4D81-8721-BE631BD6F820}">
                  <actions>
                     <action id="{0F3C6BEC-E56B-4875-93D7-2846A75881D2}" uid="EE9D54B5443448BAA56AAA91452375DD" DataSource="{B98E5C7B-CCA1-47F7-8673-10F99D3A6285}" />
                  </actions>
                  <conditions>
                     <condition id="{59179A5F-4857-470B-AE95-9DAD71848F18}" uid="5760B7D370F14F46B2682CA62150257B" />
                  </conditions>
               </rule>
               <rule name="Guest Checkout" uid="{BA798B02-4370-411E-9D6A-127A88E5DA1B}">
                  <actions>
                     <action id="{0F3C6BEC-E56B-4875-93D7-2846A75881D2}" uid="743B3ACB9BF34D77A4422315ACE12445" DataSource="{B98E5C7B-CCA1-47F7-8673-10F99D3A6285}" />
                  </actions>
                  <conditions>
                     <condition id="{356B2F4F-9C10-45ED-81F3-5E7FBE8DE534}" uid="10845CB0CEBF45C6947FBD83D37593E0" />
                  </conditions>
               </rule>
               <rule uid="{00000000-0000-0000-0000-000000000000}" name="Default">
                  <conditions>
                     <condition id="{4888ABBB-F17D-4485-B14B-842413F88732}" uid="A38E38F8AA204EDFB1CCA117B42059C6" />
                  </conditions>
                  <actions>
                     <action id="{0F3C6BEC-E56B-4875-93D7-2846A75881D2}" uid="5EE3874C82C34E81BC11929603F8DD9F" DataSource="{B98E5C7B-CCA1-47F7-8673-10F99D3A6285}" />
                  </actions>
               </rule>
            </ruleset>
         </rls>
      </r>
      <r ds="query:./ancestor-or-self::*[@@templateid='{3578A109-09C3-4F70-A54B-8C611390E639}']/Site Data/Shared Controls/Footers/Default Footer" id="{11240494-2FC7-4277-92C3-26F504298C43}" par="" ph="" uid="{0694C5ED-0803-4EBB-B21F-4D18EE34F14B}" />
      <r ds="query:./ancestor-or-self::*[@@templateid='{3578A109-09C3-4F70-A54B-8C611390E639}']/Site Data/Shared Controls/Email Subscriptions/Default Email Subscription" id="{AA78B211-1A77-406E-98C7-6D955D44E59E}" par="" ph="/footer/footer-columnc-content" uid="{85D9C993-506B-4EFE-8637-4C9A3B0D4743}" />
   </d>
</r>

If you look at the rendering that has "<r ds="query:./ancestor-or-self::*[@@templateid='{3578A109-09C3-4F70-A54B-8C611390E639}']/Site Data/Shared Controls/Main Navigations/Unauthenticated Menu" id="{47F7A7A7-2F3C-4BFE-8FA6-6B51B147CACD}"", you will see the personalization rules attached to it does NOT have the query.    It has the GUID ID of the Sitecore item, because the Sitecore GUI when selecting an item through personalization sets the datasource to that item's id.    

 But it doesn't have to be this way....   You can manually change it via Raw Values to use a query, as you can see below

<r ds="query:./ancestor-or-self::*[@@templateid='{3578A109-09C3-4F70-A54B-8C611390E639}']/Site Data/Shared Controls/Main Navigations/Unauthenticated Menu" id="{47F7A7A7-2F3C-4BFE-8FA6-6B51B147CACD}" par="" ph="header" uid="{8A2037D6-5CE5-45EA-AAD4-5685178BD669}">
   <rls>
      <ruleset>
         <rule name="Guest Checkout Only" uid="{C3FC0B6D-4363-4AC8-879A-9D99D09FC362}">
            <actions>
               <action id="{0F3C6BEC-E56B-4875-93D7-2846A75881D2}" uid="379C40C7603C48CAAACBCC4EE03B797F" DataSource="query:./ancestor-or-self::*[@@templateid='{3578A109-09C3-4F70-A54B-8C611390E639}']/Site Data/Shared Controls/Main Navigations/Guest Checkout Only Menu" />
            </actions>
            <conditions>
               <condition id="{D687DB1C-F2EB-425B-A7CD-1D379EC59167}" uid="3930E0334833420CA3A403476F1A323D" />
            </conditions>
         </rule>
         <rule name="Authenticated Customer" uid="{D80CE89F-90A9-4D81-8721-BE631BD6F820}">
            <actions>
               <action id="{0F3C6BEC-E56B-4875-93D7-2846A75881D2}" uid="EE9D54B5443448BAA56AAA91452375DD" DataSource="query:./ancestor-or-self::*[@@templateid='{3578A109-09C3-4F70-A54B-8C611390E639}']/Site Data/Shared Controls/Main Navigations/Authenticated Menu" />
            </actions>
            <conditions>
               <condition id="{59179A5F-4857-470B-AE95-9DAD71848F18}" uid="5760B7D370F14F46B2682CA62150257B" />
            </conditions>
         </rule>
         <rule name="Guest Checkout" uid="{BA798B02-4370-411E-9D6A-127A88E5DA1B}">
            <actions>
               <action id="{0F3C6BEC-E56B-4875-93D7-2846A75881D2}" uid="743B3ACB9BF34D77A4422315ACE12445" DataSource="query:./ancestor-or-self::*[@@templateid='{3578A109-09C3-4F70-A54B-8C611390E639}']/Site Data/Shared Controls/Main Navigations/Guest Checkout Menu" />
            </actions>
            <conditions>
               <condition id="{356B2F4F-9C10-45ED-81F3-5E7FBE8DE534}" uid="10845CB0CEBF45C6947FBD83D37593E0" />
            </conditions>
         </rule>
         <rule uid="{00000000-0000-0000-0000-000000000000}" name="Default">
            <conditions>
               <condition id="{4888ABBB-F17D-4485-B14B-842413F88732}" uid="A38E38F8AA204EDFB1CCA117B42059C6" />
            </conditions>
            <actions>
               <action id="{0F3C6BEC-E56B-4875-93D7-2846A75881D2}" uid="5EE3874C82C34E81BC11929603F8DD9F" DataSource="query:./ancestor-or-self::*[@@templateid='{3578A109-09C3-4F70-A54B-8C611390E639}']/Site Data/Shared Controls/Main Navigations/Unauthenticated Menu" />
            </actions>
         </rule>
      </ruleset>
   </rls>
</r>

The "Datasource" in the action of the rules were changed from the ID to the needed query.  

With this final step, you can be sure that personalization will in fact pick up the correct Site Data item based on the Same-Name conventions.   

Seems like a lot of thinking.   And a lot of moving pieces.   But, following all the steps above, you can help provide a better solution where the content author doesn't need to spend hours and hours (or even days) of manual changes.   If a handful of hours of setup from a develop saves days of confusions for the content authors, it is worth it.