Cache Busting Kentico

When developing a website that is quick to load on all devices, caching from both a data and asset perspective is very important. Luckily for us, Kentico provides a comprehensive approach to caching data in order to minimise round-trips to the database. But what about asset caching, such as images, CSS and JavaScript files?

At Syndicut, we ensure all our site assets are cached by adding the following to our web.config file:

<!-- Cache resources directory BEGIN -->
<location path="resources">
<system.webServer>
<httpProtocol>
<customHeaders>
<add name="Cache-Control" value="public, max-age=604800" />
</customHeaders>
</httpProtocol>
</system.webServer>
</location>
<!-- Cache resources directory END -->

As great as this approach is, how do we alleviate the issue of ensuring updates to our assets are taken effect straight away in the user’s browser without having to go down the process of telling the client they have to clear their browser cache to see new front-end changes?

I’m aware of different approaches developers have taken to deal with cache busting their own web applications each varying in complexity. Whether in it’s basic form where the link to a file is manually updated using the following methods:

  • Filename — site.v3.css
  • File path — /v3/site.css
  • Query string — site.css?version=3

Or, a more automated approach using Octopus deploy or the approach I will be detailing later on. At the end of the day, as long as a different link to a file is served to the browser, a new version of the file will be requested.

In this post, I will be detailing how we cache bust our portal page Kentico sites. I have taken inspiration from a blog post written a few years ago by Mads Kristensen. His elegant solution will form the underlying framework for adapting our Kentico sites.

So let’s get to it!

Step 1: CacheEngine Class

Create a new class called CacheEngine that will carry out the main functionality of transforming our local asset URL and append a timestamp based on the last modified date of the file.

The StaticFileCacheBuster method will only append the cache busting timestamp if the site is not in debug mode. This is a personal choice since I don’t require the cache busting element whilst in development mode due to the nature of how our development environment is set up. However, once the site is on a production environment we disable debug mode as a best practice.

public class CacheEngine
{
/// <summary>
/// Caches static resources, such as CSS and JavaScript.
/// </summary>
/// <param name="rootRelativePath"></param>
/// <returns></returns>
public static string StaticFileCacheBuster(string rootRelativePath)
{
if (!HttpContext.Current.IsDebuggingEnabled)
{
if (HttpRuntime.Cache[rootRelativePath] == null)
{
string fileAbsolutePath = HostingEnvironment.MapPath($"~{rootRelativePath}");
DateTime date = File.GetLastWriteTime(fileAbsolutePath);
int index = rootRelativePath.LastIndexOf('/');
string result = rootRelativePath.Insert(index, $"/v{date.Ticks}");
HttpRuntime.Cache.Insert(rootRelativePath, result, new CacheDependency(fileAbsolutePath));
}
return HttpRuntime.Cache[rootRelativePath].ToString();
}
else
{
return rootRelativePath;
}
}
}

Step 2: Apply ClientCache and Rewrite Rules

The web.config file will need to be updated to include the following:

<system.webServer>
<rewrite>
<rules>
<rule name="Cache Buster">
<match url="([\S]+)(/v[0-9]+/)([\S]+)" />
<action type="Rewrite" url="{R:1}/{R:3}" />
</rule>
</rules>
</rewrite>
<staticContent>
<clientCache cacheControlMode="UseMaxAge" cacheControlMaxAge="365.00:00:00" /> <!-- This needs to be the top entry in the staticContent block. -->
</staticContent>
</system.webServer>

Step 3: Kentico Cache Macro

The code presented in Step 1 as it stands will allow us to cache assets directly in code. However, for portal templates we need to create a quick macro called CacheBuster:

using CMS;
using CMS.DocumentEngine;
using CMS.Helpers;
using CMS.MacroEngine;
using CMS.SiteProvider;
using System;
using System.Text;
using System.Web;
[assembly: RegisterExtension(typeof(StringMacroMethods), typeof(string))]
public class StringMacroMethods : MacroMethodContainer
{
[MacroMethod(typeof(string), "Caches static resources, such as CSS and JavaScript", 0)]
[MacroMethodParam(0, "param1", typeof(string), "Relative file path.")]
public static object CacheBuster(EvaluationContext context, params object[] parameters)
{
string filePath = parameters[0].ToString();
return CacheEngine.StaticFileCacheBuster(filePath);
}
}

Usage Examples

If all the above steps have been carried out correctly, we should be able to cache bust any local asset URL at template level and code.

Portal Template

The following example demonstrates using the macro we created in Step 3 to cache bust our stylesheet and a touch icon.

<link rel="stylesheet" type="text/css" href="{%"/resources/css/site.min.css".CacheBuster() #%}" />
<link rel="apple-touch-icon" href="{%"/resources/images/icons/apple-touch-icon.png".CacheBuster()#%}" />

Outcome:

<link rel="stylesheet" type="text/css" href="/resources/css/v636619761520000000/site.min.css" />
<link rel="apple-touch-icon" href="/resources/images/icons/v636528193180000000/apple-touch-icon.png" />

In Code

Sometimes we need to declare JavaScript to be added to a page at Web Part level and these JavaScript references can be cache busted by calling the CacheEngine.StaticFileCacheBuster() from Step 1 directly.

...
...
...
if (!Page.ClientScript.IsClientScriptBlockRegistered(this.GetType(), "ArticleCarousel"))
{
string jsScripts = $@"$LAB
.script(""{CacheEngine.StaticFileCacheBuster("/resources/js/plugins/slick.min.js")}"")
.script(""{CacheEngine.StaticFileCacheBuster("/resources/js/carousel.min.js")}"").wait(function() {{
BECarousel.Init();
FECarousel.Init();
}});";
ScriptManager.RegisterStartupScript(this, typeof(string), "ArticleCarousel", jsScripts, true);
}
...
...
...

Outcome:

<script type = "text/javascript">
$LAB
.script("/resources/js/plugins/v636528193160000000/slick.min.js")
.script("/resources/js/v636530800300000000/carousel.min.js").wait(function() {
BECarousel.Init();
FECarousel.Init();
});
</script>

With minimal setup time and the benefit of automatic updates to the links to your client side resources, I hope you find this approach useful.