How to cache bust static files with a fingerprinted file name?

Hello,

I’m currently thinking about migrating my Jekyll blog with ~500 posts to Hugo and before I start that adventure I’d like to confirm I’m able to do something first. I searched around and didn’t find a definitive answer.

With my current site in Jekyll, images are adjacent to js and css when it comes to where they are located on disk. For example I have an image located at:

_assets/images/hello.jpg

Which I reference in Jekyll’s template with {% image hello.jpg class:media-object %}.

In development, that produces:

<img src="/assets/hello.jpg" class="media-object" width="107" height="150" alt="hello.jpg">

In production when I build my site, the same image tag produces:

<img src="/assets/hello-01b9ba6ec73eaa954a828e8d96392fc4c41238c1aae0da03711fb27ea8007321.jpg" class="media-object" width="107" height="150" alt="hello.jpg">

This is due to using a Jekyll plugin (GitHub - envygeeks/jekyll-assets: 🎨 Asset pipelines for Jekyll.) which adds digests to all assets. In this case an asset can be css, js, images or whatever else exists in the assets directory.

This allows me to then cache these images forever with nginx because if the content changes then a new digested file name gets created so it gets automatically invalidated.

I’d like to do the same thing with Hugo but it would be very important that:

  • The file name is the same that was created with Jekyll, for example it would be hello-$md5.jpg. Using a query string param wouldn’t work for my use case.
  • The image must be located alongside other images in their own images directory, not nested in content/blog/my-post/hello.jpg, if that ends up being in static/images instead of assets/images that’s fine, as long as they all exist in 1 place.

Any thoughts on how to achieve this? Thank you.

Images resources

Take site resources as an example.

// assets/images/foobar.jpg
{{ with resources.Get "images/foobar.jpg" }}
  {{ $img := . | fingerprint "md5" }}
  <img src="{{ $img.Permalink }}" />
{{ end }}

static” images

You can use the last modified time as the hash for images located in static folder.

// static/images/foobar.jpg
{{ $img := "images/foobar.jpg" }}
{{ $hash := "" }}
{{- with os.Stat (path.Join "static" $img) }}
   {{- $hash = .ModTime.Format "20060102150405" | md5 }}
{{- end }}
<img src="{{ $img }}?v={{ $hash }}">

Image hook

If you want to apply this on Markdown image syntax, please create your own image render hook.

I didn’t test these code, there might be typo.

Thanks. 2 questions if you don’t mind:

  • With the resource option, the image never shows up but assets/images/foobar.jpg exists. Is there anything else I need to configure to make that work? That call as is produces an empty string
  • With the static option, is there a way to make the digest a part of the file name? Using a query string won’t work for my use case
    • I know I can likely extract the file extension from $img but the file won’t exist on disk

Whats that “call as is”?

The resource code should work. Resources have to be published explicitely by invoking a special method in the example code .Permalink. See Publish | Hugo

Think the static option won’t work as you need the md5 in the filename. Static files are copied verbatim

Oh, I took their example exactly how it was and only edited the file name. It never renders the tag, just an empty string.

If it helps I’m running hugo v0.128.2-de36c1a95d28595d8243fd8b891665b069ed0850 linux/amd64 BuildDate=2024-07-04T08:13:25Z VendorInfo=gohugoio.

When I do {{ $styles := resources.Get "/css/app.css" }} and then reference its $styles.RelPermalink, this works. It seems to only not pick up images. I also adjusted the images path to add a leading / like css has and it also doesn’t work.

Ok, it’s working now. I forgot I modified my assetDir to be the output of where esbuild copies files. Now it’s being picked up and fingerprinted.

However, the files are being created with a filename.fingerprint.extension as seen by this path http://localhost:1313/images/hello.4a90c0d74632b4933ab2c5f62e94be2c.jpg.

Given I have thousands of images with filename-fingerprint.extension is there anything I can modifiy at the Hugo level to use - instead of . to separate the file name and extension? This way none of the hot links to my images on the internet will break. I didn’t see this as a configurable option for fingerprint.

according to the docs it should be: at Fingerprint | Hugo

The default hash algorithm is sha256 . Other available algorithms are sha384 and (as of Hugo 0.55 ) sha512 and md5

please recheck. if so it’s either a bug in the docs or the method :wink:

check out resources.Copy | Hugo - so

  • after fingerprinting (don’t call the .Permalink)
  • get the resource.Name
  • use string replacement methods on the name
  • copy the resource to the new path
  • publish the copy only

When I call:

  {{ $img := . | fingerprint }}
  {{ $img.Name }

It returns back the non-fingerprinted value so there’s nothing I can find / replace.

Do you think it’s worth opening a feature request to allow to customize this part of the filename? This is starting to look quite involved.

here’s the workaround:

  • you have the non-fingerprinted name
  • after fingerprinting the resource has a property .Data.Integrity where you have all the other needed information (see the fingerprint Usage link above)
  • build the new filename
  • copy and publish

regarding feature request: you may give it a try - there’s a category feature here where you can post your request and see what happens.

Let’s see how it goes: Ability to customize the fingerprint delimiter that goes in between the filename and extension · Issue #12658 · gohugoio/hugo · GitHub

Digged a little into and I didn’t manage to get the Data.Integrity back to the MD5.

Here’s a solution with manual fingerprinting and publishing to public/assets/images/ :wink:

you need:

  • the image in assets/images/foobar.png
hugo.toml
baseURL      = 'https://example.org/'
languageCode = 'en-us'
title        = 'Forum Topic 50695'
disableKinds = ['sitemap', 'rss', 'taxonomy', 'term']
layouts/_default/baseof.html
<!doctype html>
<html lang="en">
   <body>
      <main>
         {{ block "main" . }}{{ end }}
      </main>
   </body>
</html>
content/index.html
---
---
# Just an Image

layouts/_default/home.html:

{{ define "main" }}
   {{ .Content }}
   <h2>directly from template</h2>
   <!-- load the image -->
   {{ with resources.Get "images/foobar.png" }}
      <!-- calculate MD5 -->
      {{ $md5 := crypto.MD5 .Content }}
      <!-- remove extension -->
      {{ $basePath := replaceRE `(.*)\.png$` "$1" .Name }}
      <!-- remove leading / -->
      {{ $basePath = strings.TrimLeft "/" $basePath }}
      <!-- generate filename -->
      {{ $targetFullPath := printf "assets/%s-%s.png" $basePath $md5 }}
      <!-- copy to destination (virtually) -->
      {{ with resources.Copy $targetFullPath . }}
         <!-- publish to destination by creating the link -->
         <img src="{{ .RelPermalink }}" />
         <p>{{ .Name }} => {{ $targetFullPath }}</p>
      {{ end }}
   {{ end }}
{{ end }}

code could be shortened by using pies or combing stuff in expressions but i guess expressive is better here.

and of course the code may be

  1. used in a partial to be re-used in multiple templates
  2. added to a shortcode to be used in markdown
  3. a shortcode that calles partial (1) (DRY)
  4. added to a image_render_hook

Hope that helps

1 Like

Thanks. I was able to get this working in the most basic sense, basically copy / pasting your solution and using hard coded values.

It does indeed fingerprint them with the custom file format and produce the file on disk.

Thanks a lot.

The next steps for me will be to make it more dynamic such as turning it into a partial to support a custom file name and also attempt to extract the extension from the file name since not all images are pngs.

Speaking of which, Hugo makes it easy to get the extension with {{ $ext := path.Ext .Name }} which returns .jpg (with the dot) but how would you modify {{ $basePath := replaceRE (.*).jpg$ "$1" .Name }} to use $ext? The docs don’t have any examples of this and Google is coming up empty.

The print statement was easy enough to modify {{ $targetFullPath := printf "%s-%s%s" $basePath $sha256 $ext }}.

Great to hear

If you get stuck with some part of that just come back to the forum

With a new topic, specific question

Pretty sure someone will pick up to assist

Guess this topic is solved with the github issue and the example code.

Uh. Hit me hard…

I would guess you create a pattern string using printf and pass this instead of a hardcoded pattern

Or just use a regexp to delete everything after the last dot. As you already have the extension.

Like untested

replaceRE `\.[^.]+` ""
1 Like

That seems to remove everything after the first ..

I happened to have a file named nick-janetakis.jpg and I renamed it to nick-janet.akis.jpg and /images/nick-janet was the result of $basePath.

Edit: This regex seems to work \.[^.]+$, the trailing $ is the difference.

Here’s an end to end solution. A lot of this was done by @irkode in previous replies, I just abstracted it out into a partial and appended in some of my existing image partial logic.

Keep in mind I just started using Hugo so I don’t really know if this is the best way to do things but:

Previously I had this image.html partial:

{{ $src := print "/images/" .src }}
{{ $height := .height }}
{{ $width := .width }}
{{ $class := .class }}

{{ $img := resources.Get $src | fingerprint }}
<img src="{{ $img.Permalink }}"{{ with $class }} class="{{ $class }}"{{ end }} height="{{ $width | default $img.Height }}" width="{{ $width | default $img.Width }}" alt="{{ index ((split $img "/") | last 1) 0 }}" >

Which was called in an image.html shortcode like this:

{{ $src := .Get "src" }}
{{ $height := .Get "height" }}
{{ $width := .Get "width" }}
{{ $class := .Get "class" }}

{{ partial "image.html" (dict "src" $src "height" $height "width" $width "class" $class) }}

Which can then be used in your content markdown file like this:

{{< image src="hello.jpg" >}}

Here is the new custom image.html partial:

{{ $fingerprintDelimiter := "-" }}
{{ $src := print "/images/" .src }}
{{ $height := .height }}
{{ $width := .width }}
{{ $class := .class }}

{{ with resources.Get $src }}
  <!-- Calculate SHA -->
  {{ $sha256 := sha256 .Content }}
  <!-- Get the file extension -->
  {{ $ext := path.Ext .Name }}
  <!-- Remove extension by removing everything after the last dot -->
  {{ $pathWithoutExt := replaceRE `\.[^.]+$` "" .Name }}
  <!-- Remove leading / -->
  {{ $pathWithoutExt = strings.TrimLeft "/" $pathWithoutExt }}
  <!-- Generate filename -->
  {{ $fullPathWithFingerprint := printf "%s%s%s%s" $pathWithoutExt $fingerprintDelimiter $sha256 $ext }}
  <!-- Copy to destination (virtually) -->
  {{ with resources.Copy $fullPathWithFingerprint . }}
     <!-- Publish to destination by creating the link -->
     <img src="{{ .Permalink }}" {{ with $class }} class="{{ $class }}"{{ end }} height="{{ $width | default .Height }}" width="{{ $width | default .Width }}" alt="{{ index ((split .Name "/") | last 1) 0 }}" >
     <!-- For debugging purposes so you can see the file name, remove this as needed -->
     <p>{{ .Name }} => {{ $fullPathWithFingerprint }}</p>
  {{ end }}
{{ end }}

The shortcode and how you call it didn’t change, but the above solution produces file names with basefile-fingerprint.ext instead of the original basefile.fingerprint.ext.

1 Like

This topic was automatically closed 2 days after the last reply. New replies are no longer allowed.