Cache rules everything around me!

In today’s world seconds can determine whether a customer stays on a site or goes elsewhere to find what they are looking for. To decrease page load times, developers are using techniques like optimizing images, minifying JavaScript & CSS and caching pages. Let us focus on caching. Sitecore provides Html caching but how does one take advantage of CDN caching. This is done with cache headers. These headers tell a browser what to cache and for how long. If the CDN honors these headers, pages become very fast because they rarely make it to origin because the CDN holds the cached pages. This also makes it very easy to scale. Here is an example.

This approach adds a processor to the httpRequestBegin pipeline. The processor is configured to run only on the public sites against the web database. In the Includes folder, add a configuration file:

<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <httpRequestBegin>
        <processor type="Foundation.Pipelines.HttpRequest.SetCacheHeaders, Foundation.Sample"
          patch:before="processor[@type='Sitecore.Pipelines.HttpRequest.DeviceSimulatorResolver, Sitecore.Kernel']" >
          <databasesToIgnore>core, master</databasesToIgnore>
    <sitesToIgnore>shell,login,admin,service,modules_shell,modules_website,scheduler,system,publisher</sitesToIgnore>
          <pathsToIgnore>/sitecore,/trace,/layouts,/_dev,/_DEV,/api</pathsToIgnore>
        </processor>

      </httpRequestBegin>
    </pipelines>
  </sitecore>
</configuration>

In order to ignore certain pages, an ExcludeFromCache base template is added to any page that is not cached. For example, any page with a Sitecore Form must go to origin so that submit actions and other server side code can fire. With this in mind, a processor is implemented as follows:

namespace Foundation.Pipelines.HttpRequest
{
    public class SetCacheHeaders : HttpRequestProcessor
    {
        private static readonly ID ExcludeFromCacheTemplateId = new ID("ITEM_GUID}");
        private static string CacheDuration = NUMBER_OF_DAYS;
        private static string ProxyCacheDuration = NUMBER_OF_DAYS;
        public override void Process(HttpRequestArgs args)
        {
            if (!Context.Item.IsDerivedFrom(ExcludeFromCacheTemplateId))
            {
                //Cache page not being excluded from caching
                var cacheDuration = CacheDuration;
                var proxyCacheDuration = ProxyCacheDuration;
                HttpContext.Current.Response.Cache.
                    SetCacheability((HttpCacheability.Public));
                HttpContext.Current.Response.Cache.
                    SetMaxAge(DateTime.Now.
                    AddDays(int.Parse(cacheDuration)) - DateTime.Now);
                HttpContext.Current.Response.Cache.
                    SetProxyMaxAge(DateTime.Now.
                    AddDays(int.Parse(proxyCacheDuration)) - DateTime.Now);
                HttpContext.Current.Response.Cache.
                    SetExpires(DateTime.Now.
                    AddDays(int.Parse(cacheDuration)));
                HttpContext.Current.Response.Cache.SetValidUntilExpires(true);
                var etag = Context.Item.Statistics.Revision.Replace("-", "");
                HttpContext.Current.Response.Cache.
                    SetLastModified(Context.Item.Statistics.Updated);
                HttpContext.Current.Response.Cache.SetETag(etag);
            }
            else
            {
                //Exclude page from caching
                HttpContext.Current.Response.
                     AppendHeader("Cache-Control", "no-cache, no-store, must-revalidate"); // HTTP 1.1
                HttpContext.Current.Response.
                     AppendHeader("Pragma", "no-cache"); // HTTP 1.0
                HttpContext.Current.Response.
                     AppendHeader("Expires", "0"); // Proxies
            }
        }
    }
}

Now you have cache headers! Read here to better understand the cache derivatives. I hope someone finds this tidbit useful.

NOTES: Use either Last-Modified or Etag to identify specific version of the page. This will be determined by how the CDN is configured. Also, IsDerivedFrom is an extension method supplied by Constellation for Sitecore. Basically, the idea is to determine if the page contains the base template. Finally, multiple no cache headers were supplied for backwards compatible but Cache-Control is the current header expected to control caching in browsers and shared caches (CDNs).