Proper use of browser cache and CDN can simultaneously improve performance and decrease costs. A CDN often becomes essential at enterprise scale. However, a CDN is not a one-size-fits-all solution; getting the most out of your CDN calls for some care in configuration.
This page discusses some key issues you should consider when implementing our dotCDN service, or another CDN service, to best meet your site's needs.
Essentials
Cache Control
The HTTP Cache-Control
header is used by both CDNs and browsers to control how long content is cached. Setting proper cache-control headers is crucial not only for a smooth CDN implementation, but a healthy site in general.
There is no way to clear the browser cache of a user of your site. Setting a cache TTL too high could mean that users cache a version of your site you later determine to be unsatisfactory, and continue to see that version long after you publish corrections. (Path-based asset invalidation, as in the section below, can potentially improve this situation.) Setting a cache TTL too low can increase page load times and increase server load. Thus, even in the absence of a CDN, cache control is critical.
The single most important thing you can do to ensure a smooth CDN implementation is to make sure your Cache-Control
header is set properly for your dotCMS pages and content.
When configuring the header manually, the most common directive is max-age
. As such, a cache lifespan of an hour would look like this:
Cache-Control: max-age=3600
Configuring Cache-Control in dotCMS
Content | Default Value | How to Change |
---|---|---|
Images and files (Using /dA and /contentAsset/image pathing) | 1 year (31536000 seconds) | Rules |
dotCMS Pages | 3600 seconds | Cache TTL property or Rules |
External Applications (e.g., SPA) | Varies by app server and configuration | Framework app/server configuration |
Note: Configuring Cache-Control
through a Rule will take precedence over all other methods, such as the Cache TTL property.
Block-Level Caching
A more granular degree of control over how page content is cached can be accessed through dotCMS's #dotcache
directive, which permits block-level caching. This can be especially important in cases where page caches are insufficient due to the presence of dynamic content.
For more information, see Tag-Based Caching.
Cache Invalidation Built Into File Paths
CDNs cache all content — files, images, and pages — based on the path to the asset. So, if you change a file or image, but your pages access that file or image using the same exact path, the CDN or the users' browser may not recognize that the file or image has changed. And it may therefore serve an old, cached version of the file or image to the user.
So, the most reliable way to ensure that new versions of files and images are delivered to users is to change the path to the file or image each time a new version of it is saved. Since the path is new, it's impossible for an old version of the file or image to be served from cache.
There are two ways to ensure that paths change when file versions change:
- SPAs: Usually no action is required.
- Most popular front-end Java frameworks create unique hashes for files automatically.
- Images and Files: Update the URL to include the inode. This can be done in one of two ways:
- When using the file/image URL in your code (such as an
<img>
tag), add a URL parameter that includes the inode to the end of the URL. This example will update the query string each time the contentlet is updated:
Alternately, you can update it when the file itself changes, through the use of<img src="/images/blog/blog-image?inode=${dotContentMap.inode}">
webapi.getAssetInode
:<img src="/images/blog/blog-image?inode=${webapi.getAssetInode('/images/blog/blog-image')}">
- Use
/dA
or/contentAsset/image
pathing based on the inode instead of the identifier (again usingwebapi.getAssetInode
). Example:
Similarly, you could include a group of javascript files in a directory with<script src="dA/${webapi.getAssetInode('/application/themes/dotcms/css/vendor/jquery.fancybox.min.css')}/jquery.fancybox.min.css"></script>
$dotcontent.pull
:
This produces:#set($jsThemePath = ${dotTheme.path} + "js/") #foreach($jsFile in $dotcontent.pull( "+contentType:FileAsset +(conhost:${host.identifier} conhost:SYSTEM_HOST) +FileAsset.fileName_dotraw:*.js +parentpath:${jsThemePath}", 10,"score,modDate desc") ) <script src="${jsFile.shortyUrlInode}"></script> #end
<script src="/dA/7e39a73547/script.js/"></script> <script src="/dA/dd32bda0f9/core.min.js/"></script>
- When using the file/image URL in your code (such as an
Lastly, to change paths on a sitewide basis, use the URL parameter method given above with the host's inode. Then, simply save and activate a new version of your host/site to update the path:
<link rel="stylesheet" href="/application/themes/dotcms/css/main.dotsass?v=$host.inode">
For more information, see the pages on content version or displaying images with binary image fields.
Review Your CDN Cache Settings
With most CDNs, you can configure how different URLs or types of assets are cached. These settings override any settings that come from dotCMS, so it's important that you understand what settings you have, and make sure they don't interfere with the delivery of your site.
- For dotCMS Cloud customers using dotCDN, this configuration is performed by the dotCMS team as part of the free CDN management service.
- If you're using a different CDN and managing it yourself, you should review your CDN cache settings to ensure they don't cache important assets in ways that might interfere with the delivery of your site.
- We strongly recommend that you do not add specific cache settings to your CDN configuration unless you have a specific reason to do so. The less unique cache configuration you have for your CDN, the less likely it is that you will run into caching problems that are difficult to both identify and resolve.
Other Best Practices
Set Up CDN-Bypass Domain/Subdomain
For a user's request to go through the CDN, you must configure your DNS entries to point the user's initial request at the CDN, rather than at dotCMS directly. This also means that you can have a separate DNS entry that allows you to access your site directly from dotCMS, without first hitting the CDN.
Setting up two separate DNS entries in this way allows you to:
- Test the use of the CDN before you switch your main (public) domain to use the CDN.
- Determine if any unexpected behavior is due to issues with the main site content, or caching performed by the CDN.
After you've finished testing the implementation through the CDN, you can swap the DNS entries so your main public domain goes through the CDN, and you can use your test domain to access the site without going through the CDN.
Purging Caches Via Push Publish Listeners
This best practice is already fully implemented for dotCMS Cloud customers using the dotCDN service.
If you're using a different CDN, you may wish to implement a similar dotCMS plugin for your CDN. The plugin uses a Push Publishing listener to perform a CDN cache invalidation for an asset when the dotCMS server receives a new version of said asset via Push Publishing.
Although it is a best practice to ensure the CDN cache is invalidated after a Push Publish, this is not a substitute for the three “essential” items above.
Velocity Scriptlet for Invalidations
The script below is an example script of that shows how you can use Velocity to perform cache invalidations when a piece of content is changed, published or removed. This code is intended as an example and can be encapsulated in a .vtl and called in a workflow task using the Velocity Scripting Actionlet whenever a piece of content is published. It figures out what URLs are assoicated with the piece of content, of whether the content is a page, a file, a URLMap or a dotAsset and invalidates the urls associated with the give piece of content.
Again, this is no substitute for proper header configuration and/or path-based invalidations, as described in the first section of this page.
#set($baseDomain = "https://dotcms.cdn.net")
#set($AccessKey = "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXXXXXX")
#set($urls = [])
#set($id=$webapi.findIdentifierById("$content.identifier"))
#if($!content.contentObject.isFileAsset() || $content.isFileAsset())
$!logger.info("invalidating a fileAsset $id.path")
#set($x = $urls.add("$id.path"))
#elseif($!content.contentObject.isHTMLPage() || $content.isHTMLPage())
$!logger.info("invalidating a page: $id.path")
#set($x = $urls.add("$id.path"))
#elseif($urls.size()==0)
#set($myContent=$dotcontent.find("$content.identifier"))
#if($!content.contentObject.isDotAsset() || $content.isDotAsset())
$!logger.info("invalidating a dotAsset: $myContent.getShortyUrl()")
#set($x = $urls.add("$myContent.getShortyUrl()"))
#set($x = $urls.add("$myContent.getShortyUrlInode()"))
#elseif($!myContent.urlMap)
$!logger.info("invalidating a urlMap: $myContent.urlMap")
#set($x = $urls.add("$myContent.urlMap"))
#end
#end
#set($headers = {})
#set($x = $headers.put("Content-Type", "application/json"))
#set($x = $headers.put("AccessKey", "$AccessKey"))
#foreach($url in $urls)
#set($invadateUrl = "https://mycdn.com/api/purge?url=${baseDomain}${url}")
$!logger.info("invadateUrl: $invadateUrl")
#set($x = $json.post($invadateUrl, $headers, {}))
#end