Developing and building web applications with Webpack is a relatively straight-forward process. There are many great tools such as create-react-app and many great resources for long-term caching such as https://developers.google.com/web/fundamentals/performance/webpack/use-long-term-caching. What is not so clear is how to long-term cache the index.html while still having new builds show up seamlessly to the user. This is fairly easy to handle when not in a static context, like .Net or Node.js, where you can easily intercept the call to index.html and invalidate the cache if something has changed; however, this is not so easy to handle when hosting a webapp as a static site. When creating a single page app, hosted as a static site, most web servers are going to aggressively cache the index.html. This is not desirable since anytime bundled assets change, within the index.html, the user would need to hard refresh the browser to pull those in. What we really want is for the new index.html to show up on any normal page load. The user should never need to hard refresh the browser tab with a ctrl+r. This is a bad user experience and can lead to confusion with users seeing old content.

The solution is to hash the index.html and include that hash in the filename itself, so that it’s built as index.[hash].html, then point the webserver to this specific hashed named index file.

Hashing the index.html requires one additional option in the HtmlWebpackPlugin:

new HtmlWebpackPlugin({

filename: ‘index.[hash].html’,

template: paths.appHtml,

})

Your HtmlWebpackPlugin will likely contain many more options than this, but note the [hash] in the bolded text — this is what controls the output. The resulting build file may be something like: index.340e916f34c4b56b9d73.html

Having the index.[hash].html means that anytime the bundles change, which are included in the index.html by default, then the hash part of the index.[hash].html will change. This means between production pushes all calls to that index will be cached with a 304. The challenge now is pointing IIS to a dynamically generated defaultDocument file. Since IIS DefaultDocument doesn’t allow wildcards we’ll have to use another approach.

When creating a new Azure static site, you’ll either need to check the option to generate a web.config file for you, or you can create one separately. There are two main ideas here. First is that IIS needs a defaultDocument; second, Webpack needs to update the local web.config with the generated index.[hash].html name.

In the web.config define a defaultDocument with a standard index.html.

Now tell Webpack to rewrite the index.html to the generated index.html with the hash. This is a bit tricky since the index.html is written by webpack near the end of the executed plugins, and it’s difficult to determine the order of plugins executed. Thankfully, “There’s a plugin for that.” Meet the EventHooksPlugin. This plugin allows you to hook into the various events that happen in Webpack. This way we can easily hook into the afterEmit event and rewrite the index.html to the generated index.[hash].html. The afterEmit event is executed after all assets have been emitted to the build directory. This looks like the following:

To break down the logic here, after the index.[hash].html has been emitted, the EventHooksPlugin kicks in, the custom code reads the html file, extracts out the hash, and rewrites the index.html text in web.config with the index.[hash].html text. In Azure, there are a list of default documents for a static site. Given you can’t add a wild-carded one, and you wouldn’t want to have to manually add the generated index.[hash].html every time you do a production push, this plugin modifies the web.config so it is added automatically for each new build.

Given there will be no strict index.html file in the build directory, the only remaining defaultDocument will be the dynamic one that the web.config now points to. Now when you load your webapp, successive reloads will be cached until you push a new build. A new build will result in a new hashed index, invalidating the cache on the next page load causing a 200 response. Subsequent requests will now be cached until the next index hash change. This should all be transparent to the user with no additional user intervention or hard refreshing required.

It is also worth noting the rewrite in the web.config. This forces all requests to the root context. With a single page app hosted as a static site, this is most likely what you’ll want to do to allow your routing library to intercept these calls, and also to allow the defaultDocument to take effect for the default context of /.

Edit on 07/05/2018:

If you are deploying to Azure via VSTS you may notice that the browser may still be caching old artifacts. This is because, by default, build artifacts are appended to the Kudu site/wwwroot instead of replaced. See the following forum link for the VSTS option for ensuring only the latest build artifacts are in the wwwroot in Kudu: https://social.msdn.microsoft.com/Forums/azure/en-US/0c5e85b6-806f-43a9-af6b-3a2a6f6634e6/how-to-delete-all-the-files-on-the-site?forum=windowsazurewebsitespreview