Sean G. Wright
Posted on September 9, 2019
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:
Kentico 12: Design Patterns Part 12 - Database Query Caching Patterns
Sean G. Wright ใป Aug 26 '19 ใป 13 min read
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">
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">
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;
}
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
After
/css/v-637034589309537495/styles.css
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" />
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>
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:
Or my Kentico blog series:
Posted on September 9, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 4, 2019