Sitecore URL and domain setup options

In Sitecore solutions there are often requirements around the URL structure or domain on which the site will live. This is often driven by SEO and perhaps the Sitecore site even needs to share the same domain with a non-Sitecore site. This article will look at several common options and will explain when they should be used.

Below flowchart shows which option to choose depending on your needs. None of the options are mutually exclusive and they can be combined to meet multiple requirements:

Option 1: Reverse proxy/application gateway

In some cases requirements are more complex, for example your company’s Sitecore site needs to be hosted at mycompany.com but complete different servers needs to host mycompany.com/careers. This might not even be an IIS site but a site hosted on Apache or Nginx.

There are several ways to do this depending on the infrastructure which is in place. If Sitecore is hosted in the cloud here are some options which can route the traffic accordingly to prevent it from even hitting IIS

In an on-premise setup there might be similar infrastructure which can do this routing. IIS can also do this routing through the Application Request Routing (ARR) extension. In general routing this traffic away before it hits IIS is the preferred solution as it reduces the load on IIS.

Below is a sample reverse proxy rule in ARR which will route all traffic under /Careers to http://internalcareersite. This is all achieved through the routing and the site will be exposed over mycompany.com/careers:

<rewrite>
    <rules>
        <rule name="Reverse Proxy to Careers" stopProcessing="true">
            <match url="^Careers/(.*)" />
            <action type="Rewrite" url="http://internalcareersite/{R:1}" />
        </rule>        
    </rules>
</rewrite>

Option 2: IgnoreUrlPrefixes setting

The IgnoreUrlPrefixes setting tells Sitecore which Paths to exclude from Sitecore. This setting comes in handy if there is another site which needs to be hosted on the same domain, and the site can be hosted in the same IIS site as Sitecore. This is often an easy solution which can be helpful in many circumstances.

It can be challenging to update this setting in a clean way as there are already a number of entries of Sitecore in this setting. This article provides a good solution to only patch in your solution specific values.

URL Rewrite/Redirect

Before diving into the next few options let’s be clear on the difference between a redirect and a rewrite:

  • Redirect: a redirect happens when the servers responds to the client/browser and tells it to load a page or resource from a different location. The client can then retrieve the resource from the location provided by the server. For example, you can have a vanity URL at mycompany.com/mycampaign which redirects to mycompany.com/some/long/path/mycampaign
  • Rewrite: a rewrite is when IIS modifies an incoming request before it hands it off to its handlers (inbound rewrite) or modifies an outgoing response before it sends it to the client (outbound rewrite). In this case there are no additional request/responses like there are in case of a redirect.
    An example of this could be to rewrite all the incoming request starting with mycompany.com/english to mycompany.com/en-us. In this case the URL in the browser would be mycompany.com/english, but on Sitecore it would appear as if it came at mycompany.com/en-us as IIS modified it before handing off to Sitecore. These techniques can be powerful in a variety of situations, for example to have different URL structure then what Sitecore OOTB supports with language and sites.

There are a variety of Sitecore modules which can help with redirects which are managed in Sitecore, SXA also has a redirect module. Also IIS has a redirect/rewrite extension which is already a prerequisite for Sitecore installations.

3. Rewrite through IIS rewrite module

Sometimes it can be challenging to meet certain URL requirements with Sitecore without making any customizations, for example there might be a need to have the language in the URL as /english or /spanish etc. Implementing this in Sitecore can be complex as it requires customizations to processor(s) in the httpRequestBegin pipeline as well as the logic to generate links correctly. Below inbound and outbound rewrite rule will have the URL as /english but to Sitecore it appears as /en-us.

An important consideration when doing this is that in Sitecore the links will still show as /en-us, for example in the analytics.

Inbound rule: to make sure /english and anything after it gets send to /en-us on the same path, e.g. /english/products will go to /en-us/products:

<rewrite>
	<rules>
		<rule name="Englist to en-us">
			<match url="^english/(.*)" />
			<action type="Rewrite" url="en-us/{R:1}" />
		</rule>
	</rules>
</rewrite>

Outbound rule: to make sure any link in an anchor text gets “en-us” replaced with “english”. Depending on your setup more rules might be required to update the links in other places:

<rewrite>            
	<outboundRules>
		<rule name="Update en-us to english in anchor">
			<match filterByTags="A" pattern="^/en-us/(.*)" />
			<action type="Rewrite" value="/english/{R:1}" />
		</rule>
	</outboundRules>
</rewrite>

Option 4: Redirect through IIS Rewrite module

Some redirects rarely change and therefore are not managed by content authors, for example a redirect from http to https, to lowercase URLs or to enforce a trailing slash. In such cases it makes sense to have the redirects in IIS as it is acceptable to update these only with a deployment. Another advantage of doing this through IIS is that the request never hits Sitecore, it gets redirected straight from IIS which is more efficient. Below is a sample IIS redirect which redirect from http to https:

<rule name="Redirect to HTTPS" patternSyntax="Wildcard" stopProcessing="true">
	<match url="*" ignoreCase="true" negate="false" />
	<conditions logicalGrouping="MatchAny" trackAllCaptures="false">
	  <add input="{HTTPS}" ignoreCase="true" matchType="Pattern" negate="false" pattern="OFF" />
	</conditions>
	<action type="Redirect" url="https://{HTTP_HOST}{REQUEST_URI}" appendQueryString="false" redirectType="Permanent" />
</rule>

Option 5: Custom Sitecore processors

Sometimes there is a need to do something similar to a rewrite (option 3), but it cannot be done with the rewrite module because it requires information from the Sitecore context. In this case perhaps the OOTB item resolution logic does not resolve the desired item or a different context language or site needs to be set. Before getting into more detail here are some common use cases:

  • Sitecore’s language or site resolution logic does not meet the requirements. For example the language does not need to come from the URL but from the Sitecore IP Geolocation service.
  • A Single Page Application (SPA) is used which uses push-state. In this case it is common to want to deep link into these push state route. For example the SPA could live in mycompany.com/myspa, with myspa being the Sitecore item. There could be a route like /myspa/filter/category1. Sitecore would try to resolve an item under that path but it is not there so it would return a 404. In this case the Sitecore ItemResolver can be customized to still resolve the correct item. An more detailed approach for this can be found here

In these cases the correct Sitecore processor needs to be updated. Below are the 3 most common processors which need to be updated. They are all part of the HttpRequestBegin pipeline:

  • Sitecore.Pipelines.HttpRequest.SiteResolver: as the name suggests this processor resolves the site from the context. This processor serves as a good starting point to adjust the resolution of the context site.
  • Sitecore.Pipelines.HttpRequest.LanguageResolver: the language resolver determines the correct language based on the context. The default Sitecore language resolution can put some constraints on the URL structure as the language needs to match a culture. Often business wants user friendly language in the URL like English, Spanish, French etc. In these cases customizing the language resolver can be a good option.
  • Sitecore.Pipelines.HttpRequest.ItemResolver: this resolves the context item in Sitecore and might be the most customized processor in Sitecore to solve a variety of different issues. There are even some popular modules that customize this like the wildcard module

Updating Sitecore processors can be tricky as they can also run outside the scope of your solution, for example when a user logs into Sitecore CMS (/sitecore/login). If possible it is a good idea to keep the existing logic as is, but run additional code in case the existing resolution fails because the custom solution requirements are different.

Updating these processors will ensure Sitecore handles incoming requests correctly, however the links Sitecore generates to other pages will not take this logic into account. To ensure these links work correctly there are 2 options:

  • Put in a redirect to send the links to the correct location
  • Override Sitecore’s logic to generate links by putting in a custom link provider or a custom renderfield pipeline

Option 6: Sitecore Redirect Module

In many cases the content authors need to manage redirects along with other content updates they make in Sitecore. In these cases it is recommended to manage the redirects in Sitecore content as well. There are a variety of Sitecore modules which can help with redirects and SXA also has a redirect module.

Turn off Session State locking in Sitecore MVC pages

The default implementation of the ASP .NET Session State Module uses exclusive locking for each request from the same session. This means ASP .NET will only execute one request at a time from the same browser. Any other request will be locked by the Session State Module and will not be executed until the previous request is complete and it can obtain the exclusive lock. This can cause performance issues in many real-world scenarios.

Below screenshot from IIS shows 6 concurrent request to the homepage from the same browser. Sitecore is only executing the bottom request, which is in the ExecuteRequestHandler state. All other 5 requests are in the RequestAcquireState state and will only be fulfilled one at a time after the bottom request is complete. Each of the requests in RequestAcquireState state will check the session store every 0.5 seconds to see if it can obtain a lock.

This can cause pressure on the session state store in case many requests take some time to execute. Depending on the session store it is common to see messages like below in log:

Common errors with session state in Redis:

Exception type: TimeoutException
Exception message: Timeout performing EVAL, inst: ....
at StackExchange.Redis.ConnectionMultiplexer.ExecuteSyncImpl[T](Message message, ResultProcessor1 processor, ServerEndPoint server) at StackExchange.Redis.RedisBase.ExecuteSync[T](Message message, ResultProcessor1 processor, ServerEndPoint server)
at StackExchange.Redis.RedisDatabase.ScriptEvaluate(String script, RedisKey[] keys, RedisValue[] values, CommandFlags flags)
at Sitecore.SessionProvider.Redis.StackExchangeClientConnection.<>c__DisplayClass7.b__6()
at Sitecore.SessionProvider.Redis.StackExchangeClientConnection.RetryForScriptNotFound(Func1 redisOperation) at Sitecore.SessionProvider.Redis.StackExchangeClientConnection.RetryLogic(Func1 redisOperation)
at Sitecore.SessionProvider.Redis.StackExchangeClientConnection.Eval(String script, String[] keyArgs, Object[] valueArgs)
at Sitecore.SessionProvider.Redis.RedisConnectionWrapper.TryTakeWriteLockAndGetData(String sessionId, DateTime lockTime, Object& lockId, ISessionStateItemCollection& data, Int32& sessionTimeout)
at Sitecore.SessionProvider.Redis.RedisSessionStateProvider.GetItemFromSessionStore(Boolean isWriteLockRequired, HttpContext context, String id, Boolean& locked, TimeSpan& lockAge, Object& lockId, SessionStateActions& actions)
at Sitecore.SessionProvider.Redis.RedisSessionStateProvider.GetItemExclusive(HttpContext context, String id, Boolean& locked, TimeSpan& lockAge, Object& lockId, SessionStateActions& actions)
at System.Web.SessionState.SessionStateModule.GetSessionStateItem()

Common errors with session state in SQL:

Message: Timeout expired. The timeout period elapsed prior to obtaining a connection from the pool. This may have occurred because all pooled connections were in use and max pool size was reached.
Source: System.Data
   at System.Data.ProviderBase.DbConnectionFactory.TryGetConnection(DbConnection owningConnection, TaskCompletionSource`1 retry, DbConnectionOptions userOptions, DbConnectionInternal oldConnection, DbConnectionInternal& connection)
   ... 
   at System.Web.SessionState.SqlSessionStateStore.SqlStateConnection..ctor(SqlPartitionInfo sqlPartitionInfo, TimeSpan retryInterval)
Message: Timeout expired. The timeout period elapsed prior to obtaining a connection from the pool. This may have occurred because all pooled connections were in use and max pool size was reached.
Source: System.Data
   at System.Data.ProviderBase.DbConnectionFactory.TryGetConnection(DbConnection owningConnection, TaskCompletionSource`1 retry, DbConnectionOptions userOptions, DbConnectionInternal oldConnection, DbConnectionInternal& connection)
   ... 
   at System.Web.SessionState.SqlSessionStateStore.SqlStateConnection..ctor(SqlPartitionInfo sqlPartitionInfo, TimeSpan retryInterval)

Common errors with session state in Mongo:

ERROR Application error.
Exception: System.TimeoutException
Message: Timeout waiting for a MongoConnection.
Source: MongoDB.Driver
   at MongoDB.Driver.Internal.MongoConnectionPool.AcquireConnection(AcquireConnectionOptions options)
   ...
   at Sitecore.SessionProvider.MongoDB.MongoSessionStateProvider.GetItemExclusive(HttpContext context, String id, Boolean& locked, TimeSpan& lockAge, Object& lockId, SessionStateActions& actions)
   at System.Web.SessionState.SessionStateModule.GetSessionStateItem()
   at System.Web.SessionState.SessionStateModule.BeginAcquireState(Object source, EventArgs e, AsyncCallback cb, Object extraData)
   at System.Web.HttpApplication.AsyncEventExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute()
   at System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously)

Too many locked requests from a single session

ERROR Application error.
Exception: System.Web.HttpException
Message: The request queue limit of the session is exceeded.
Source: System.Web
   at System.Web.SessionState.SessionStateModule.QueueRef()
   at System.Web.SessionState.SessionStateModule.PollLockedSession()
   at System.Web.SessionState.SessionStateModule.GetSessionStateItem()
   at System.Web.SessionState.SessionStateModule.BeginAcquireState(Object source, EventArgs e, AsyncCallback cb, Object extraData)
   at System.Web.HttpApplication.AsyncEventExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute()
   at System.Web.HttpApplication.ExecuteStepImpl(IExecutionStep step)
   at System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously)

Sitecore has a good KB article which describes this in more detail which can be found here. This article mentions to set session state to readonly and describes how to do this for 2 scenarios:

  • Custom MVC Routes: Set the session state to readonly on the controller. This can be done by decorating the controller with this attribute: [SessionState(SessionStateBehavior.ReadOnly)]
  • ASP.NET Web Forms pages: Set the EnableSessionState=”Readonly” on the pages directive

This article does not mention how to fix this for Sitecore MVC pages. The solution provided below describes how to address this for Sitecore MVC pages.

Solution

Sitecore sets this to the Default Session state behavior in the SitecoreControllerFactory for Sitecore MVC pages. This is a virtual method so this can be overridden to change the session state behavior:

using Sitecore.Diagnostics;
using Sitecore.Mvc.Controllers;
using Sitecore.Mvc.Extensions;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
using System.Web.SessionState;

namespace Foundation.Extensions.Factory
{
    public class ReadOnlySessionStateSitecoreControllerFactory : SitecoreControllerFactory
    {
        public ReadOnlySessionStateSitecoreControllerFactory(IControllerFactory innerFactory) : base(innerFactory)
        {
        }

        public override SessionStateBehavior GetControllerSessionBehavior(RequestContext requestContext, string controllerName)
        {
            Assert.ArgumentNotNull(requestContext, "requestContext");
            Assert.ArgumentNotNull(controllerName, "controllerName");

            if (controllerName.EqualsText(SitecoreControllerName))
            {
                return SessionStateBehavior.ReadOnly;
            }

            return InnerFactory.GetControllerSessionBehavior(requestContext, controllerName);
        }
    }
}

An initialize pipeline processor needs to be created to set our new controller factory:

using Foundation.SitecoreExtensions.Factory;
using Sitecore.Mvc.Controllers;
using Sitecore.Mvc.Pipelines.Loader;
using Sitecore.Pipelines;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

namespace Foundation.Extensions.Processors.Initialize
{
    public class InitializeReadOnlySessionStateSitecoreControllerFactory : InitializeControllerFactory
    {
        protected Func<System.Web.Mvc.ControllerBuilder> ControllerBuilder = () => System.Web.Mvc.ControllerBuilder.Current;

        protected override void SetControllerFactory(PipelineArgs args)
        {
            System.Web.Mvc.ControllerBuilder controllerBuilder = ControllerBuilder();
            var controllerFactory = new ReadOnlySessionStateSitecoreControllerFactory(controllerBuilder.GetControllerFactory());
            controllerBuilder.SetControllerFactory(controllerFactory);
        }
    }
}

Below XML file can be used to patch in this new pipeline processor

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <initialize>
        <processor type="Foundation.Extensions.Processors.Initialize.InitializeReadOnlySessionStateSitecoreControllerFactory, Foundation.Extensions" patch:instead="*[@type='Sitecore.Mvc.Pipelines.Loader.InitializeControllerFactory, Sitecore.Mvc']"/>
      </initialize>
    </pipelines>
  </sitecore>
</configuration>

Below screenshot shows the same scenario as in the beginning of this post, but now all 8 requests are getting executed at the same time.

Setting the session state to readonly for Sitecore MVC pages can cause significant performance improvements and will help reduce the load on the session store as described in Sitecore’s KB article. Before doing this it is important to understand below considerations:

  • Multiple requests from the same browser will execute at the same time. Your application should be able to handle this without causing any unintended issues by multiple threads modifying shared objects at the same time.
  • Custom objects cannot be stored in the session state anymore when it is set to ReadOnly, except when the session state is in process. Using a custom cache as already suggested in Sitecore’s article is a good solution.
  • This issue might not occur when a site is running smoothly, but can turn a small issue into an overall site stability issue. The session store can get under a lot of load for example if some pages in your site start being slow or in case of an app pool recycle. This can impact the overall stability of the site as it can overload SQL, Redis or Mongo.

Solve caching issues when rendering is on page multiple times

HTML caching is arguably the best way to improve Sitecore performance. Sometimes you can run into issues when you enable HTML cache on a rendering and the rendering has been added to the same page multiple times. This will only happen if the renderings do not have a datasource or share the same datasource, but will still render different content. This could happen for example when the renderings have different rendering parameters or have some custom logic which changes the content.

This can be fixed in a generic way by overriding the GenerateKey method of the GenerateCacheKey RenderRendering Processor. Below code will add the UniqueId of each rendering to the cachekey which will ensure the cached output is unique for each rendering.

using Sitecore.Mvc.Pipelines.Response.RenderRendering;
using Sitecore.Mvc.Presentation;

namespace Foundation.Pipelines.RenderRendering
{
    public class GenerateCustomCacheKey : GenerateCacheKey
    {
        protected override string GenerateKey(Rendering rendering, RenderRenderingArgs args)
        {
            var cacheKey = base.GenerateKey(rendering, args);

            cacheKey += rendering.UniqueId;

            return cacheKey;
        }
    }
}

 

Sitecore 9 fix heartbeat.aspx

The heartbeat page is a useful page in Sitecore as it shows if Sitecore can connect to it’s databases. If so it will return a 200 status. It can be found at /sitecore/service/heartbeat.aspx and it can be a good practice to point the load balancer’s health check to this page. This will avoid that any traffic is send to a server which cannot connect to its backend database.

Sitecore 9 has introduced a number of new connectionstrings with xConnect and the heartbeat page will fail on these. This can be avoided by adding the new connectionstrings to the excluded connections so the heartbeat page will not return an error while Sitecore’s databases are online. Below is the value which can be used to get the heartbeat page to work in Sitecore 9.

<setting name=”Sitecore.Services.Heartbeat.ExcludeConnection” value=”LocalSqlServer| xconnect.collection| xconnect.collection.certificate| xdb.referencedata.client| xdb.referencedata.client.certificate| xdb.marketingautomation.reporting.client| xdb.marketingautomation.reporting.client.certificate| xdb.marketingautomation.operations.client| xdb.marketingautomation.operations.client.certificate|  EXM.CryptographicKey| EXM.AuthenticationKey| Session| sharedSession” />

How to fix “RedisConnectionException: No connection is available to service this operation” in Sitecore

Redis is a great choice for Sitecore’s shared session database. Sitecore has a good article which describes how to set this up, and links to this article to explain all options. I was running into issues when setting this up with my Azure Redis Cache which is using an access key. The “accessKey” attribute in the provider node was populated with the access key form the Azure portal. Initially i was seeing something like this in the log:

INFO  redisname.redis.cache.windows.net:6380,abortConnect=False
INFO
INFO  Connecting redisname.redis.cache.windows.net:6380/Interactive...
...
INFO  redisname.redis.cache.windows.net:6380 faulted: UnableToResolvePhysicalConnection on PING

Then the log would be full of errors like below:

ERROR GetItemFromSessionStore => StackExchange.Redis.RedisConnectionException: No connection is available to service this operation: EVAL
   at StackExchange.Redis.ConnectionMultiplexer.ExecuteSyncImpl[T](Message message, ResultProcessor`1 processor, ServerEndPoint server)
   at StackExchange.Redis.RedisBase.ExecuteSync[T](Message message, ResultProcessor`1 processor, ServerEndPoint server)
   at StackExchange.Redis.RedisDatabase.ScriptEvaluate(String script, RedisKey[] keys, RedisValue[] values, CommandFlags flags)
   at Sitecore.SessionProvider.Redis.StackExchangeClientConnection.c__DisplayClass12_0.b__0()
   at Sitecore.SessionProvider.Redis.StackExchangeClientConnection.RetryForScriptNotFound(Func`1 redisOperation)
   at Sitecore.SessionProvider.Redis.StackExchangeClientConnection.RetryLogic(Func`1 redisOperation)
   at Sitecore.SessionProvider.Redis.StackExchangeClientConnection.Eval(String script, String[] keyArgs, Object[] valueArgs)
   at Sitecore.SessionProvider.Redis.RedisConnectionWrapper.TryTakeWriteLockAndGetData(String sessionId, DateTime lockTime, Object& lockId, ISessionStateItemCollection& data, Int32& sessionTimeout)
   at Sitecore.SessionProvider.Redis.RedisSessionStateProvider.GetItemFromSessionStore(Boolean isWriteLockRequired, HttpContext context, String id, Boolean& locked, TimeSpan& lockAge, Object& lockId, SessionStateActions& actions)

The only way I was able to get this to work was by not putting the access key in the provider but instead specifying it in the connection string (in ConnectionStrings.config):

<add name="sharedSession" connectionString="redisname.redis.cache.windows.net:6380,password=rediskey,ssl=True,abortConnect=False" />

Sitecore is now able to connect to Redis and all errors are gone from the log. Below lines from log file show the successful connect:

INFO  redisname.redis.cache.windows.net:6380,password=rediskey,ssl=True,abortConnect=False
INFO  Connecting redisname.redis.cache.windows.net:6380/Interactive...
....
INFO  Connect complete: redisname.redis.cache.windows.net:6380

Sitecore namespace issues

Since I ran into this silly issue a couple of times I decided to dig into it a little bit and write a post about it. As developers we like to structure our solutions and projects by applying consistent naming conventions. Most Sitecore projects use a naming convention like this prefix.Sitecore.description. Usually the prefix is something like a company name or a brand name, and the description is something that describes the project’s role in the solution. There can also be multiple prefixes and descriptions separated by a period. Looking at Habitat an example of description could be Feature.Accounts.

A popular approach is to keep project names, assembly names, folder structure and namespaces all in sync for clarity. If you go with this approach you will end up with a namespace like MyBrand.Sitecore.Features.Accounts. This is where things can get a little messy because “Sitecore” is in the namespace and this can conflict with references to Sitecore DLL’s. Consider below piece of code:

namespace MyBrand.Sitecore.Features.Accounts
{
    using Sitecore;

    public class Helper
    {
        public string ReturnCurrentItemName()
        {
            var item = Sitecore.Context.Item;

            return item.Name;
        }
    }
}

This code will create below compile error:

Error CS0234 The type or namespace name ‘Context’ does not exist in the namespace ‘MyBrand.Sitecore’ (are you missing an assembly reference?)

Problem

The problem here is how namespace resolution works for nested namespaces, more details can be found in the C# language specification here. As the compile error suggests it is looking for the Context object in our class instead of in Sitecore in the root scope. The good news is that there are at least 3 different ways to solve this:

1. Avoid using anything before “Sitecore” in namespace

The same code will compile just fine when “Sitecore” is the first part of the namespace, see line 1. This is the easiest solution if changing the namespace is not a problem.

namespace Sitecore.Features.Accounts
{
    using Sitecore;

    public class Helper
    {
        public string ReturnCurrentItemName()
        {
            var item = Sitecore.Context.Item;

            return item.Name;
        }
    }
}

2. Use namespace alias

This is my least favorite way to solve this but it also seems to be the most common solution. The Sitecore namespace from the Sitecore DLL can be declared as an alias and then used to reference:

using RealSitecore = Sitecore;

namespace MyBrand.Sitecore.Features.Accounts
{
    public class Helper
    {
        public string ReturnCurrentItemName()
        {
            var item = RealSitecore.Context.Item;

            return item.Name;
        }
    }
}

3. Use global namespace alias

Instead of making up a random name for Sitecore as in the last solution, it feels a lot cleaner to use the global namespace alias to tell the compiler to look in the global namespace instead:

namespace MyBrand.Sitecore.Features.Accounts
{
    using Sitecore;

    public class Helper
    {
        public string ReturnCurrentItemName()
        {
            var item = global::Sitecore.Context.Item;

            return item.Name;
        }
    }
}

Summary

As long as there is no problem changing the namespace to start with “Sitecore” then option 1 is the best choice. If for whatever reason that is not an option then go with using the global namespace alias as described in option 3.

Angular 2 and Sitecore Best Practices

Angular 2 is out of beta and I have been using this for a while now to write single page applications in Sitecore. There are a couple of good blog posts around that describes how to get Angular 2 and Sitecore working. This post will focus on some touch points between Sitecore and Angular 2. The official Angular 2 site has a good article which describes setting up Angular 2 in Visual Studio 2015.

Retrieving data from Sitecore

Angular 2 has a HTTP library which makes it easy to work with JSON data returned from the server. Sitecore makes it easy to return JSON data especially from a controller action when using MVC.

However it seems like Sitecore.Services.Client is often overlooked when a restful API is required. Sitecore.Services.Client allows you to fetch Sitecore items in JSON format without writing any code, this link shows several ways to retrieve data this way. The Sitecore.Services.Client is also extensible just like almost everything else in Sitecore. The developer guide can be found here

PushState Routing

Angular 2 uses pushState routing by default, which means that the URL of the Angular route gets appended to the regular URL using a slash (/) as a separator just like Sitecore does. This is the preferred style and is good for SEO as well. An example of this would be an Angular 2 application that is hosted on mysite.com home page that has a route at mysite.com/details which will be displayed by Angular’s routing from the app on home page.

There will be an issue when someone tries to deep link into a mysite.com/details as Sitecore will try to resolve this item but cannot since this is only an Angular route not a Sitecore item. Perhaps the easiest way to solve this is by creating rewrite rules in IIS to resolve these links to the homepage. However I do not like this approach as it is not very maintainable. Creating the same app on a different page would require deploying additional rewrites. Instead I’m using below pipeline to detect this scenario and assign the correct item. Now the content author can add additional instances of the Angular 2 application without involvement from the development team:

using Sitecore;
using Sitecore.Diagnostics;
using Sitecore.Links;
using Sitecore.Pipelines.HttpRequest;
using System;

namespace Jeroen.Sitecore
{
    public class Angular2PushStateItemResolver : HttpRequestProcessor
    {
        public override void Process(HttpRequestArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            if (Context.Item == null && Context.Database != null && Context.Database.Name != "core")
            {
                //find all items that have the Angular app
                var query = string.Format("{0}//*[@@templateid='{1}']", args.StartPath, "<Guid of template with Angular 2 app>");
                var items = Context.Database.SelectItems(query);

                foreach (var item in items)
                {
                    var options = LinkManager.GetDefaultUrlOptions();
                    options.LanguageEmbedding = LanguageEmbedding.Never;

                    //check if this is an Angular component inside the current item and if so assign context item
                    if (args.LocalPath.StartsWith(LinkManager.GetItemUrl(item, options), StringComparison.OrdinalIgnoreCase))
                    {
                        Context.Item = item;
                        break;
                    }
                }
            }
        }
    }
}

Here is the code to include the pipeline:

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <httpRequestBegin>
        <processor patch:after="*[@type='Sitecore.Pipelines.HttpRequest.ItemResolver, Sitecore.Kernel']" type="Jeroen.Sitecore.Angular2PushStateItemResolver, Jeroen.Sitecore" />
      </httpRequestBegin>
    </pipelines>
  </sitecore>
</configuration>

What about SEO, Social and performance?

Angular2 is rendering the page client side which can adversely affect a number of key areas:

  • Page performance as the browser needs to download and run a good amount of javascript to run the application. This can get especially troublesome in mobile and larger more complicated apps
  • SEO as search engine bots are not able to crawl the page
  • Social as Twitter, Facebook and other social media are not able to correctly display a preview

Angular Universal will address these issues by sending the rendered output to the client. This is currently only supported in node.js and ASP .NET core but should hopefully be available for ‘regular’ ASP .NET soon. Therefore I recommend keeping the best practices from the universal team in mind to ensure the application will work in Angular universal when this is ready. Maybe Angular Universal can even get the experience editor working?!

Sitecore vary output cache by context item and datasource

Sitecore’s output cache is an excellent solution to improve the performance of a Sitecore solution. The output cache can be configured to be different for a variety of criteria as explained by John West in this post.

The most common option is the VaryByData option. I don’t think this option is documented very well, but it will vary the cache based on the datasource if it is set. The context item will be used instead if no datasource is set.

VaryByData will vary the cache based on Datasource if set, otherwise it will use the context item.

This will work well in most cases, but there are valid scenarios where the cache should vary based on the datasource and the context item. This can be avoided by adding below pipeline. This will use Sitecore’s logic to determine the cachekey, but will add the item path. The if statement can be adjusted depending on the need. Examples include setting it to the ID of the rendering that requires it or checking if rendering’s Datasource property is set if this is required for all renderings that have a datasource.

public class GenerateCacheKeyCustomized : GenerateCacheKey
{
    protected override string GenerateKey(Rendering rendering, RenderRenderingArgs args)
    {
        var cacheKey = base.GenerateKey(rendering, args);

        if (rendering.RenderingItemPath == "<your ID>")
        {
            cacheKey += string.Format("_{0}", Sitecore.Context.Item.Paths.Path);
        }

        return cacheKey;
    }
}

Sitecore WFFM 8.1 incorrectly detects forged request

Sitecore Web Forms For Marketers 8.1 rev.151217 (Update-1) has recently been released, see release notes here As mentioned in the release notes this includes a new feature to prevent forging request.

For some reason I’m seeing valid request (the initial form was served by the Sitecore server and is submitted back to the same server) incorrectly being detected as forged request which causes the form the error. I’m currently working with Sitecore to try to understand why this is happening and understand what the root cause is. I will let you know if i figure out this issue.

Please let me know if anyone else is running into the same issue hopefully it can help troubleshoot this. Also Sitecore can provide a custom DLL which avoids the page crashing in this scenario.

Filter items without language version in Sitecore Item Web API

The Sitecore Item Web API is a great tool to query Sitecore client side and retrieve JSON data which can be shown through a JavaScript library like Angular or Knockout. The Item Web API will return all items that match a query even if there is no version of an item in the current language. This can lead to unnecessarily large HTTP responses which will slow down the page, especially in multilingual sites with many languages with different content per language.

One nice feature of Item Web API is the ability to easily customize it. Below simple pipeline can be added to filter out any item that matches a query but does not have a version in the current language:

using Sitecore.Diagnostics;
using Sitecore.ItemWebApi.Pipelines.Read;
using Sitecore.Web;
using System.Linq;

namespace Jeroen.Pipelines.WebAPI
{
    public class RemoveItemsWithoutLanguageVersion : ReadProcessor
    {
        public override void Process(ReadArgs arguments)
        {
            Assert.ArgumentNotNull(arguments, "arguments");

            if (WebUtil.GetQueryString("language-filter", null) == "1")
            {
                arguments.Scope = arguments.Scope.Where(i => i.Versions.Count > 0).ToArray();
            }
        }
    }
}

I’m only doing this filtering when a query string of language-filter=1 is passed. I add this to every query that needs it. This avoids any conflicts with existing functionality which uses Item Web API like the Sitecore Desktop. Above pipeline is enabled in Sitecore.ItemWebApi.config as follows:

<itemWebApiRead>
   <processor type="Jeroen.Pipelines.WebAPI.RemoveItemsWithoutLanguageVersion, Jeroen.Pipelines" />
 </itemWebApiRead>