DEV Community

Sean G. Wright for WiredViews

Posted on • Edited on

Kentico CMS Quick Tip: Automatic Static File Fingerprinting

Wooden Finger

Photo by Charles ๐Ÿ‡ต๐Ÿ‡ญ on Unsplash

Static Content and URLs

Whether we are working on a Kentico CMS Portal Engine or Kentico MVC site, we probably have some static content being served by IIS.

This is the content that isn't stored in a Media Library or as a page Attachment - those files are served by the running ASP.NET application (either CMS or MVC).

The files I'm primarily talking about here are the CSS and JavaScript files that are requested every time a browser loads a page from our site.

While these files could be served from a Media Library or a page Attachment, it's much more common to deploy them directly to the web server file system along with your compiled application code.

We then let IIS's Static File handling serve them up ๐Ÿ˜.

What do the URLs for these files look like ๐Ÿค”?

They could be as simple as a path to the file:

  • /css/styles.css

We could also include build information in the path:

  • /dist/css/styles.min.css

Or maybe we include the version number in the path or query string:

  • /css/1.3.2/styles.css
  • /css/styles.css?v=1.3.2

The first option seems the easiest, so why not use that and move on to solving real problems within our applications ๐Ÿคทโ€โ™€๏ธ?


Client-side Caching

If we are trying to have our site perform its best, with top tier SEO rankings, and a great UX on both desktop and mobile, then we probably want to leverage caching ๐Ÿ˜‰.

In previous posts I've written about caching within the application for faster data access:

However, the caching I'm referencing now is client-side caching.

Each time the browser requests a page from our site and that page has a <link> tag pointing to /css/styles.css, the browser also requests that stylesheet.

By using the correct cache headers in the HTTP response from the server, when /css/styles.css is requested, we can instruct the browser to cache the result of the request which saves it the time and bandwidth the next time the file is needed ๐Ÿ‘.

So how does this all tie into the URLs of our static files?

Handling Client-side Caches

If we tell the client (browser) to cache the results of a request for /css/styles.css then no additional requests will be made for this file from that browser, until the cache expires - the browser already has it.

This cache expiration time is going to be based on the HTTP headers in the response that our server sends for the requested file.

What happens then if we update /css/styles.css on the server because we realized there was an error in our CSS rules or design?

No one is going to be happy ๐Ÿ˜‘ if we tell them the fix for the problem won't show up in the browsers of the site's visitors until their local cache expires.

So, this is where the different URL patterns, mentioned above, come into play.

By including something like the version of file in the URL, we can force the file to be downloaded by the browsers as soon as our fix is uploaded to the server.

Why would this work?

Using Versioning

Assume the browser has /css/1.3.2/styles.css cached and our current page has a <link> tag like the following:

<link rel="stylesheet" href="/css/1.3.2/styles.css">
Enter fullscreen mode Exit fullscreen mode

Now, we update the file, with some new CSS, and deploy it to /css/1.4.0/styles.css on the server.

We also need to update the <link> tag, probably in our Master Page or _Layout.cshtml, to the following:

<link rel="stylesheet" href="/css/1.4.0/styles.css">
Enter fullscreen mode Exit fullscreen mode

The next time the browser requests a page, it will see a reference to the new CSS file path (/css/1.4.0/styles.css), find that this file is not in the local cache, and then download it and add it to the cache ๐Ÿ˜‰.

The problem is that this requires several moving parts to get right:

  • โŒ Our CSS files need to be deployed to a new path on the file system each time we update them, which either complicates version control or CI/CD.
  • โŒ Our <link> tags all need to be updated to the correct path otherwise they will serve an outdated or non-existent file.
  • โŒ We need to do this not just for all of our CSS files but also all of our JavaScript files.

Wouldn't it be great if there was an automated way to do all of this? ๐Ÿคจ


Automatic URL Versioning (Fingerprinting)

Mads Kristensen wrote a blog post several years ago detailing an ingenious method for inserting version information into static file URLs.

This approach is simple, efficient, and safe for distributed (read: Web Farm) environments.

We have 3 parts we need to add to our applications for this to work:

  • โœ… Create a Helper method to generate and cache static file URLs.
  • โœ… Use this helper method in all of our markup referencing static files.
  • โœ… Add URL re-writing to our web.config so IIS can serve the correct file.

Let's go over each step below.

Fingerprint Helper Method

We can write this helper method either in a custom static class (Web Forms) or as an extension method for UrlHelper (MVC). I'm going to assume MVC going forward (see Mads blog for Web Forms scenarios).

/// <summary>
/// Inserts a fingerprint path for cache creation/breaking based on the file write time
/// </summary>
/// <param name="_"></param>
/// <param name="rootRelativePath"></param>
/// <returns></returns>
public static string Fingerprint(this UrlHelper _, string rootRelativePath)
{
    if (HttpContext.Current.Kentico().Preview().Enabled)
    {
        return rootRelativePath;
    }

    if (HttpRuntime.Cache[rootRelativePath] is null)
    {
        string absolute = HostingEnvironment.MapPath($"~{rootRelativePath}");

        var date = File.GetLastWriteTime(absolute);
        int index = rootRelativePath.LastIndexOf('/');

        string result = rootRelativePath.Insert(index, $"/v-{date.Ticks}");

        HttpRuntime
            .Cache
            .Insert(rootRelativePath, result, new CacheDependency(absolute));
    }

    return HttpRuntime.Cache[rootRelativePath] as string;
}
Enter fullscreen mode Exit fullscreen mode

There are a few things to note about the code above.

First we accept as a parameter the root relative path of a static file (ex: /css/styles.css). This will be the normal URL we would have rendered out in our Razor file.

We then check to see if the request context is in Kentico's MVC Preview Mode, and if so, we short-circuit to disable this URL versioning which would break the Preview Mode / PageBuilder functionality within the CMS ๐Ÿ˜ฎ.

If we can't find an item in the application cache, using the provided URL as a key, we create an absolute path to the file on the file system, find out its last modification date, and insert that before the last path segment.

This would result in the following URL change:

Before

/css/styles.css
Enter fullscreen mode Exit fullscreen mode

After

/css/v-637034589309537495/styles.css
Enter fullscreen mode Exit fullscreen mode

We then store this new URL in the cache, so we don't have to re-calculate it on the next page request, and return the cached value ๐Ÿ˜Ž.

We also take a dependency on the absolute file path.

If that file is changed, the cached URL is evicted and we generate a new URL with a new modification date value in it.

Generating URLs in our Razor Views

We can leverage the above method in our views as follows:

<link href="@Url.Fingerprint("/css/styles.css")" rel="stylesheet" />
Enter fullscreen mode Exit fullscreen mode

Pretty simple ๐Ÿ˜‹!

We, of course, can use @Url.Fingerprint() for any static file on our site, not just JavaScript and CSS.

Web.config URL Re-write

Finally, in our web.config, we add the following re-write rule

<system.webServer>
  <rewrite>
    <rules>
      <rule name="Fingerprint-Rewrite">
        <match url="([\S]+)(/v-[0-9]+/)([\S]+)"/>
        <action type="Rewrite" url="{R:1}/{R:3}"/>
      </rule>
  </rewrite>
</system.webServer>
Enter fullscreen mode Exit fullscreen mode

This rule says that anytime a request arrives for a URL that looks like .../v-<some numbers>/..., serve that file from the file system path that matches the URL but with the /v-<some numbers> part removed.

This re-written path is the location of our file so the file ends up being served correctly every time ๐Ÿค“!

Note: It doesn't matter what the v-<some numbers> value is - this doesn't need to be accurate, it just needs to be unique every time we make a change to the file to force the browsers to request the new version of the file.

A request to /css/v-1234/styles.css would result in the same file being served as a request to /css/v-9876/styles.css.


Final Thoughts

If needed, we could use this Fingerprint() method to also change the domain of the generated URL so that our file could be served from a separate CDN domain.

We could also toggle this re-writing and fingerprinting per environment - disable it in LOCAL / DEV but enable it in STAGING / PRODUCTION.

Remember, to really take advantage of the benefits this approach brings us, we need to ensure we have the correct cache headers specified for these files ๐Ÿ™‚.

Client-side caching is a solution to a performance problem and fingerprinting is a solution to a cache validity problem.

Check out the MDN article on cache headers to ensure you have your application configured correctly.

Thanks for reading and I hope you found this post helpful ๐Ÿ™.


If you are looking for additional Kentico content, checkout the Kentico tag here on DEV:

#kentico

Or my Kentico blog series:

Top comments (0)