Sunday, July 19, 2015

Managing Countries and Regions within the same site (Part 2) : The Region Resolver

Standard
With the content in place, the next step was to be able to tie the specific site instances to their respective countries or regions. We wanted to keep things nice and clean, and decided to add 2 additional attributes to the site definitions within our config file:

  1. siteRegion - The item id value of the region content item
    1. 1 These items where located in the global / shared location within our site. See part 1 for more information.

  2. pageNotFound - Redirect the user to a path within our site that contains a "page not found" page if they try and access a page that didn't match their country or region. 
So, our site definition looked something like this:

 <sites>  
    <site name="MySiteUSA" patch:before="site[@name='website']"  
       virtualFolder="/"  
       physicalFolder="/"  
       rootPath="/sitecore/content"  
       startItem="/MySite"  
       database="master"  
       domain="extranet"  
       allowDebug="true"  
       cacheHtml="true"  
       htmlCacheSize="10MB"  
       enablePreview="true"  
       enableWebEdit="true"  
       enableDebugger="true"  
       disableClientData="false"  
       hostName="*MySiteusa.com"  
       siteRegion="{B4F05B9C-1A40-4B95-AC03-C2115E7CA448}"  
       pageNotFound="/Page-Not-Found"  
       />  
    <site name="MySiteCA" patch:before="site[@name='website']"  
       virtualFolder="/"  
       physicalFolder="/"  
       rootPath="/sitecore/content"  
       startItem="/MySite"  
       database="master"  
       domain="extranet"  
       allowDebug="true"  
       cacheHtml="true"  
       htmlCacheSize="10MB"  
       enablePreview="true"  
       enableWebEdit="true"  
       enableDebugger="true"  
       disableClientData="false"  
       hostName="*MySitecda.com"  
       siteRegion="{A71B1C90-107B-4740-9913-8E683B019BF7}"  
       pageNotFound="/Page-Not-Found"  
       />  
   </sites>  

Quick note:
This site definition is not meant for a production instance. You obviously want to increase the cache size and turn off things like debugging in production.

Next, we added a simple class that we would use to retrieve an object that contains our site's context as well as the above-mentioned attributes from the configuration xml via it's properties.

When looking at the code below (starting at line 44), you will notice that we are attempting to pull the region object out of cache if it exists. We added a caching layer, because we wanted to ensure that this processor would be as fast as possible. You will see this being used a lot in our processor code that follows.

1:  public class SiteRegion  
2:    {  
3:      private const string SiteNode = "/sitecore/sites";  
4:      private XmlNode CurrentSiteNode  
5:      {  
6:        get  
7:        {  
8:          XmlNode targetParamsNode = Factory.GetConfigNode(SiteNode);  
9:          var currentSiteContext = Context.Site;  
10:          foreach (XmlNode childNode in targetParamsNode.ChildNodes)  
11:          {  
12:            if (XmlUtil.GetAttribute("name", childNode)  
13:              .Equals(currentSiteContext.Name, StringComparison.InvariantCultureIgnoreCase))  
14:            {  
15:              return childNode;  
16:            }  
17:          }  
18:          return null;  
19:        }  
20:      }  
21:      public string CacheKey  
22:      {  
23:        get  
24:        {  
25:          return Context.Site.Name;  
26:        }  
27:      }  
28:      public SiteContext Site  
29:      {  
30:        get { return Context.Site; }  
31:      }  
32:      public string Region  
33:      {  
34:        get { return XmlUtil.GetAttribute("siteRegion", CurrentSiteNode); }  
35:      }  
36:      public string PageNotFoundUrl  
37:      {  
38:        get { return XmlUtil.GetAttribute("pageNotFound", CurrentSiteNode); }  
39:      }  
40:      public static string CurrentRegion  
41:      {  
42:        get  
43:        {  
44:          var siteRegion = CacheHelper.RegionCache.GetObject(Context.Site.Name) as SiteRegion;  
45:          if (siteRegion == null)  
46:          {  
47:            siteRegion = new SiteRegion();  
48:            CacheHelper.RegionCache.SetObject(Context.Site.Name, siteRegion);  
49:          }  
50:          return siteRegion.Region;  
51:        }  
52:      }  
53:    }  

Region Resolver Processor

Next, we built out the region resolver pipeline processor to check if an item was meant for a specific country / region.

Sitecore provides us with a nice SafeDictionary KeyValuePair object in their PipelineArgs that is useful for adding custom data to pass down the pipeline. This was ideal for us to pass the message along to the Region Page Not Found processor (next up) telling it whether the "page not found" page should be displayed or not (Line 26).

Line's 15 and 16 are performing the check for the context item's region field being set.

1:  public class RegionResolver : HttpRequestProcessor  
2:    {  
3:      public override void Process(HttpRequestArgs args)  
4:      {  
5:        Assert.ArgumentNotNull(args, "args");  
6:        var showRegionPage = true;  
7:        var siteRegion = CacheHelper.RegionCache.GetObject(Context.Site.Name) as SiteRegion;  
8:        if (siteRegion == null)  
9:        {  
10:          siteRegion = new SiteRegion();  
11:          CacheHelper.RegionCache.SetObject(Context.Site.Name, siteRegion);  
12:        }  
13:        if (Context.Item != null && siteRegion.Site.HostName.IsNotEmpty())  
14:        {  
15:          if (Context.Item.Fields["Region"] != null &&  
16:            !Context.Item.Fields["Region"].Value.Contains(siteRegion.Region))  
17:          {  
18:            showRegionPage = false;  
19:          }  
20:        }  
21:        if (showRegionPage)  
22:        {  
23:          return;  
24:        }  
25:        //Add entry to safe dectionary to tell region page not found processor to redirect to page not found  
26:        args.CustomData.Add("hideRegionPage", true);  
27:        var notFoundProcessor = new RegionPageNotFound();  
28:        notFoundProcessor.Process(args);  
29:      }  
30:    }  

Region Page Not Found Processor

One of things that we wanted was to have a "page not found" bit of logic that would be able to handle both our normal page not found / 404's for our sites, as well as those that were not supposed to be displayed for our country / region.

So, we wrote a processor that would be able to catch both.

Before looking at the code, there are a few things to note:
  1. On line 7, we are checking to see if our Region Resolver has told us to hide the context item via the "message" in the safe dictionary object .
  2. If you have defined custom MVC routes, you would need to check for those in the pipeline and allow them to be processed (line 31).
  3. This processor would do the job of redirecting visitors to our page not found path, set in our site definition that was mentioned above (line 47).

1:  public class RegionPageNotFound : HttpRequestProcessor  
2:    {  
3:      public override void Process(HttpRequestArgs args)  
4:      {  
5:        Assert.ArgumentNotNull(args, "args");  
6:        //Check for safe dectionary object indicating that page needs to be "hidden"  
7:        var hideRegionPage = args.CustomData.ContainsKey("hideRegionPage");  
8:        if (!hideRegionPage &&  
9:          (Context.Item != null  
10:          || Context.Site == null  
11:          || Context.Site.Name.Equals("shell", StringComparison.CurrentCultureIgnoreCase)  
12:          || Context.Site.Name.Equals("website", StringComparison.CurrentCultureIgnoreCase)  
13:          || Context.Database == null  
14:          || Context.Database.Name.Equals("core", StringComparison.CurrentCultureIgnoreCase)  
15:          || string.IsNullOrEmpty(Context.Site.VirtualFolder)  
16:          ))  
17:        {  
18:          return;  
19:        }  
20:        // The path in the requested URL.  
21:        var filePath = Context.Request.FilePath.ToLower();  
22:        if (string.IsNullOrEmpty(filePath)  
23:          || WebUtil.IsExternalUrl(filePath)  
24:          || System.IO.File.Exists(HttpContext.Current.Server.MapPath(filePath)))  
25:        {  
26:          return;  
27:        }  
28:        //Api path checks  
29:        var uri = HttpContext.Current.Request.Url.AbsoluteUri;  
30:        if (uri.Contains("sitecore/api")  
31:          || uri.Contains("api/mycustomroute"))  
32:        {  
33:          return;  
34:        }  
35:        var siteRegion = CacheHelper.RegionCache.GetObject(Context.Site.Name) as SiteRegion;  
36:        if (siteRegion == null)  
37:        {  
38:          siteRegion = new SiteRegion();  
39:          CacheHelper.RegionCache.SetObject(Context.Site.Name, siteRegion);  
40:        }  
41:        // Send the NotFound page content to the client with a 404 status code  
42:        if (!string.IsNullOrEmpty(siteRegion.Region) && !string.IsNullOrEmpty(siteRegion.PageNotFoundUrl))  
43:        {  
44:          var ctx = HttpContext.Current;  
45:          ctx.Response.TrySkipIisCustomErrors = true;  
46:          ctx.Response.StatusCode = 404;  
47:          ctx.Response.Redirect(siteRegion.PageNotFoundUrl);  
48:          ctx.ApplicationInstance.CompleteRequest();  
49:        }  
50:      }  
51:    }  

Hooking into the HttpBeginRequest Pipeline

The final piece of this puzzle, was to add our new processors to the HttpBeginRequest Pipeline, after the ItemResolver processor.

Here is what the config file looked like that would make the magic happen:

 <pipelines>  
  <httpRequestBegin>  
   <processor  
        type="MyProject.Library.Pipelines.HttpRequestBegin.RegionResolver, MyProject.Library"  
        patch:after="processor[@type='Sitecore.Pipelines.HttpRequest.ItemResolver, Sitecore.Kernel']"/>  
   <processor  
        type="MyProject.Library.Pipelines.HttpRequestBegin.RegionPageNotFound, MyProject.Library"  
        patch:after="processor[@type='Sitecore.Pipelines.HttpRequest.ItemResolver, Sitecore.Kernel']"/>  
  </httpRequestBegin>  
 </pipelines>  

Order is important here, because as I mentioned, we want the Region Resolver Processor to be able to tell the Region Not Found Processor whether or not the page should be displayed for the context site.

Next Up

In Part 3, I am going to demonstrate how we were able to "regionize" renderings on the sites by building a custom condition for the Sitecore Rules Engine.
 

0 comments:

Post a Comment