Right, let’s talk about making your site a fortress. You’ve gone through the trouble of setting up HTTPS, your headers are tight, and then you go and load a script from some third-party CDN. You’re trusting that CDN to serve the exact code you tested, not something a malicious actor slipped in there. That’s a huge, glaring weak spot. This is where Subresource Integrity (SRI) comes in, and Hugo, being the brilliant but occasionally obtuse tool it is, gives us the integrity attribute in its resources pipeline to handle it.

Think of SRI as a tamper-evident seal on a medicine bottle. Your browser calculates a cryptographic hash (a unique fingerprint) of the file it’s about to execute. It then compares that to the hash you’ve hardcoded in the integrity attribute of your <script> or <link> tag. If they match, all is well. If they don’t—if even a single comma has been changed—the browser refuses to load it. It’s a hard fail. This is the single most effective way to mitigate the risk of a compromised CDN.

Generating the Hash with Hugo Pipes

You don’t have to open a terminal and manually hash files yourself (thankfully). Hugo’s resources namespace will do the heavy lifting for you with the fingerprint function. This function does two things: it generates the hash and it appends it to the filename for cache busting. The integrity part is what we need for SRI.

Here’s how you’d use it on a local script from your assets directory. This is the most common and recommended use case.

{{- with resources.Get "js/main.js" | fingerprint "sha512" -}}
<script src="{{ .RelPermalink }}" integrity="{{ .Data.Integrity }}" crossorigin="anonymous"></script>
{{- end }}

Let’s break this down. We grab the resource, pipe it to fingerprint "sha512", and then inside the with block, we have access to the transformed resource. The .Data.Integrity property contains the complete integrity string, which will look something like sha512-abc123...==. We plop that into the integrity attribute. The crossorigin="anonymous" is required by the spec for resources loaded from a different origin, and it’s just good practice to always include it.

Why You Must Specify The Hash Type (And Which to Choose)

You’ll notice I used "sha512". You can also use "sha256" and "sha384". Why would you choose one over the other? sha256 is perfectly adequate for the foreseeable future in terms of security for this purpose. sha384 and sha512 are, technically, stronger. The web’s security community generally recommends using at least sha384. It’s a “why not?” situation. The performance hit for generating a slightly longer hash is negligible for Hugo at build time. My advice? Use sha512 and forget about it. The browser supports them all, so you’re not penalizing anyone.

The Gotcha: Remote Resources (The Whole Point)

Here’s the part that trips everyone up, and it’s not Hugo’s fault—it’s a fundamental limitation of static site generators. Hugo cannot generate an SRI hash for a remote file at build time. Let that sink in. The entire point of SRI is often to protect against a compromised remote CDN, like when you load jQuery from code.jquery.com.

If you try to do this, it will not work:

{{/* THIS IS A TRAP. IT WILL NOT WORK. */}}
{{- $jquery := resources.GetRemote "https://code.jquery.com/jquery-3.6.0.min.js" -}}
{{- with $jquery | fingerprint "sha512" -}}
<script src="{{ .RelPermalink }}" integrity="{{ .Data.Integrity }}" crossorigin="anonymous"></script>
{{- end }}

Why? Because resources.GetRemote fetches the resource at build time and stores it locally. The integrity hash is generated from the file Hugo downloaded. Your browser will then fetch the script from the remote CDN at runtime and check it against the hash of the file Hugo stored locally. If the CDN is compromised and serves a bad file, the hashes will differ and it will correctly fail. So, mission accomplished, right?

Well, sort of. The massive, glaring problem is that you are now depending on that remote CDN being available at build time. If code.jquery.com is down when you build your site, your build fails. You’ve traded a runtime dependency for a build-time dependency, which is often worse. For this reason, resources.GetRemote is generally discouraged for mission-critical assets. The correct, professional approach is to vendor your critical JS libraries—download them and commit them to your assets directory. Then use the first method. You control the file, and Hugo generates the hash from your known-good copy.

The Manual Workaround for Remotes

If you absolutely must use a remote resource and want SRI, you have to do the legwork yourself. You need to manually generate the hash (e.g., openssl dgst -sha512 -binary [file] | openssl base64 -A), and then hardcode the entire integrity string.

<script
  src="https://cdn.example.com/library.js"
  integrity="sha512-9aJw5R5Qybf+Aw6sJZ0ElBjEqRytZh4uYdQiwceLjH2bAAbPXETN+AIlQACZHDYh+Qv0zBcTv0Hzg0kZPzBuw=="
  crossorigin="anonymous"></script>

It’s ugly, it’s manual, and you must remember to update it every time you change the library version. This is why vendoring is almost always a better idea. You’re offloading the integrity check from a manual process you might forget to an automated one Hugo handles on every build.