Table of Contents

Introduction

This is a quick overview of how to use Hugo’s resources feature to automatically process site assets.

Getting Resources

First off, you need to instruct Hugo to load the resource. This has a number of different syntaxes depending on the situation, and the documentation can be quite confusing. If a resource cannot be found, the function will return nil, which can tell you that you’re doing something wrong.

Sitewide

If you have a resource that you want to use for the entire site, such as a CSS file, JavaScript, or cover image, place this in the assets directory of your site project. Use the resources.get function to get one of these resources, by passing it a relative path to the resource inside the assets directory.

1{{ $js := resources.Get "js/main.js" }}

Page Specific

If you have a resource that is specific to a page, place it in a page bundle, and then get it with the .Page.Resources.get function.

1{{ $image := .Page.Resources.Get "myimage.jpg" }}

In some weird circumstances, I’ve found that getting the resource from the parent page may be required:

1{{ $image := .Page.Parent.Resources.Get "myimage.jpg" }}

Remote

Lastly, if you want to live life on the edge a little bit more, you can also reference a resource from a remote URL, with the resources.getRemote function.

1{{ $jquery := resources.GetRemote "https://code.jquery.com/jquery-3.6.0.min.js" }}

I generally don’t recommend this, since you won’t be able to build your site if the remote source is offline, but obviously it depends on your use case.

This does automatically cache the resource depending on the response headers, so don’t feel bad if you’re constantly running hugo. You’re not repeatedly downloading the same file over and over.

Fingerprinting

One very useful feature of resources is the ability to add a fingerprint to them. What this means is that Hugo can automatically add a hash to a filename.

1{{ $css := resources.Get "css/style.css" | resources.Fingerprint }}
2<link rel="stylesheet" href="{{ $css.RelPermalink }}" />
3<!-- This will produce something like /css/styles.fa61de5858ec4fba3617c9d81d66046547755a44aa5efda6e7727872b3ee6daa.css -->

The advantage of this is cache busting. If you screw up your cache policies, and replace the contents of a file on your website, the user’s browser may continue to use the old cached copy. The user’s browser fetches the HTML for the page, sees a file with the same name, and depending on how long the cache expiration was set for, may just use its local copy. By adding a hash to the filename, whenever the contents of the file change, the file gets a new name, and the browser will be forced to download a new copy.

This can also be used for subresource integrity, particularly with JavaScript assets, with the .Data.Integrity property.

1<script type="text/javascript" src="{{ $bundle.RelPermalink }}" integrity="{{ $bundle.Data.Integrity }}"></script>

If you do this, make sure this is the last function in your sequence.

Examples

Here are a few examples I’ve put together that are modified from templates I’ve made for my own websites recently.

CSS

Here is an example of how you can use Hugo resources to minify CSS and create a source map. One thing to note, is that generating source maps is most easily done (without needing to install external tools and things), with the resources.ToCSS function. If your source CSS code is not Sass or SCSS, just change the file extension.

1{{ $css := resources.Get "css/style.scss" }}
2{{ $css := $css | resources.ToCSS (dict "targetPath" "css/styles.css" "enableSourceMap" true) | resources.Minify | resources.Fingerprint }}
3<link rel="stylesheet" href="{{ $css.RelPermalink }}" />

JS

This example shows how you can combine multiple JavaScript files into one, and then minify them and create a source map. The function js.build does use ESBuild and I have found that this can break certain libraries, that make a global variable available like $ for jQuery. If this is the case, you may want to use resources.Babel instead, though this requires Babel to be installed.

1{{ $menu := resources.Get "js/menu.js" }}
2{{ $prism := resources.Get "js/prism.js" }}
3{{ $theme := resources.Get "js/theme.js" }}
4{{ $bundle := slice $menu $prism $theme | resources.Concat "js/bundle.js" | js.Build (dict "sourceMap" "external" ) | resources.Minify | resources.Fingerprint }}
5<script type="text/javascript" src="{{ $bundle.RelPermalink }}" integrity="{{ $bundle.Data.Integrity }}"></script>

Images

Images are where Hugo’s asset processing capabilities really shine. Images are often the largest files on a static site, so resizing and compressing them appropriately can significantly decrease the loading time of a page. In the past, this has been … challenging. I always found manually resizing images hard because I had to manually try to keep the dimensions consistent and then I would often forget to keep a copy of the original uncompressed image. Additionally, .webp is frequently recommended now, but some browsers still do not support it, and many desktop image editors don’t either. This usually lead me to giving up trying to do this, and just serve the original image instead.

Hugo lets you resize images automatically, and convert them to different formats automatically at build time. This is amazing. You can keep the original source image in the site repository and only serve smaller resized versions to users. By being able to also convert the images to different formats, you can easily serve optimized versions to browsers that support it.

See this page for all the options, but I generally use the .Fit function, since it resizes the image to fit inside a given box while maintaining the aspect ratio.

Here is an example template I made to overwrite layouts/_default/_markup/render-image.html to automatically resize and convert images in a Markdown image tag, utilizing the alt text as a caption:

 1{{ $image := .Page.Resources.Get .Destination }}
 2{{ $image_webp := false }}
 3{{ $image_fit := false }}
 4
 5{{ if not $image }}
 6    {{ errorf "Image %q not found on page %q" .Destination .Page }}
 7{{ end }}
 8
 9{{/* Can only resize raster images */}}
10{{ if ne $image.MediaType.SubType "svg" }}
11  {{ $image_fit = $image.Fit "1000x1000" | resources.Fingerprint }}
12  {{ $image_webp = $image.Fit "1000x1000 webp" | resources.Fingerprint }}
13{{ end }}
14
15{{ $image = $image | resources.Fingerprint }}
16
17<figure>
18    <picture>
19        {{ with $image_webp }}
20        <source loading="lazy" srcset="{{ $image_webp.Permalink }}" type="image/webp" alt="{{ .Text }}">
21        {{ end }}
22
23        {{ if $image_fit }}
24        <img loading="lazy" src="{{ $image_fit.Permalink }}" alt="{{ .Text }}">
25        {{ else}}
26        <img loading="lazy" src="{{ $image.Permalink }}" alt="{{ .Text }}">
27        {{ end }}
28    </picture>
29
30    {{ with .Text}}
31    <figcaption class="center">{{ . | markdownify }}</figcaption>
32    {{ end }}
33</figure>

The weird {{ if ne $image.MediaType.SubType "svg" }} statements are because vector images cannot be resized or converted.

Do note that Hugo still copies the original image into your final output, even if you resize or convert it. In some deployment environments like GitHub Pages, this may quickly balloon your website size past limits. In that case, you may want to think about not worrying about it, or writing a script to purge unused images.

Using Resources in Third Party Themes

While I’ve become a big fan of resources, a lot of Hugo themes don’t support them or properly implement them yet. One of my personal pet peeves are themes that use JS/CSS from multiple different CDNs. I personally prefer to host all of my site assets myself, so I like to rewrite these templates. However, these changes to the templates are often small, yet the entire template can be quite large, and I don’t want to overwrite the entire thing. To get around this, I install the theme as an npm package and then use the patch-package package.

package.json

 1{
 2  "dependencies": {
 3    "hello-friend": "github:panr/hugo-theme-hello-friend#3.0.0",
 4    "hugo-extended": "^0.111.3",
 5    "patch-package": "^6.5.1"
 6  },
 7  "scripts": {
 8    "postinstall": "patch-package"
 9  }
10}

config/hugo.toml

1themesDir = "node_modules"
2theme     = "hello-friend"

Now, I can edit the theme directly in the node_modules/hello-friend directory, and then run npx patch-package hello-friend. This will create a patch file in the patches/ directory, and will automatically apply it after any npm install.

Along with not needing to copy/paste large templates and overwrite a single line, GitHub dependabot can now also automatically create pull requests for new theme versions at the same time as any other npm packages.

Here is an example that I made for this blog at time of writing:

 1diff --git a/node_modules/hello-friend/layouts/partials/footer.html b/node_modules/hello-friend/layouts/partials/footer.html
 2index 7b7f8c6..e7ffed2 100644
 3--- a/node_modules/hello-friend/layouts/partials/footer.html
 4+++ b/node_modules/hello-friend/layouts/partials/footer.html
 5@@ -18,10 +18,10 @@
 6   </div>
 7 </footer>
 8
 9-{{ $menu := resources.Get "js/menu.js" | js.Build }}
10-{{ $prism := resources.Get "js/prism.js" | js.Build }}
11-{{ $theme := resources.Get "js/theme.js" | js.Build }}
12-{{ $bundle := slice $menu $prism $theme | resources.Concat "bundle.js" | resources.Minify }}
13-<script type="text/javascript" src="{{ $bundle.RelPermalink }}"></script>
14+{{ $menu := resources.Get "js/menu.js" }}
15+{{ $prism := resources.Get "js/prism.js" }}
16+{{ $theme := resources.Get "js/theme.js" }}
17+{{ $bundle := slice $menu $prism $theme | resources.Concat "js/bundle.js" | js.Build (dict "sourceMap" "external" ) | resources.Minify | resources.Fingerprint }}
18+<script type="text/javascript" src="{{ $bundle.RelPermalink }}" integrity="{{ $bundle.Data.Integrity }}"></script>
19
20 {{- partial "extended_footer.html" . }}
21diff --git a/node_modules/hello-friend/layouts/partials/head.html b/node_modules/hello-friend/layouts/partials/head.html
22index 07938e1..6013e64 100644
23--- a/node_modules/hello-friend/layouts/partials/head.html
24+++ b/node_modules/hello-friend/layouts/partials/head.html
25@@ -16,14 +16,16 @@
26
27 <!-- Theme CSS -->
28 {{ $res := resources.Get "css/style.scss" }}
29-{{ $style := $res | resources.ToCSS }}
30+{{ $style := $res | resources.ToCSS (dict "targetPath" "css/styles.css" "enableSourceMap" true) | resources.Minify | resources.Fingerprint }}
31 <link rel="stylesheet" href="{{ $style.RelPermalink }}" />
32 <!-- Custom CSS to override theme properties (/static/style.css) -->
33-<link rel="stylesheet" href="{{ "style.css" | absURL }}" />
34+{{ $custom_res := resources.Get "style.scss" }}
35+{{ $custom_style := $custom_res | resources.ToCSS (dict "targetPath" "css/custom_styles.css" "enableSourceMap" true) | resources.Minify | resources.Fingerprint }}
36+<link rel="stylesheet" href="{{ $custom_style.RelPermalink }}"/>
37
38 <!-- Icons -->
39-<link rel="apple-touch-icon-precomposed" sizes="144x144" href="{{ "img/apple-touch-icon-144-precomposed.png" | absURL }}" />
40-<link rel="shortcut icon" href="{{ "img/favicon.png" | absURL }}" />
41+<link rel="shortcut icon" href=https://nathanv.me/img/theme-colors/green.png>
42+<link rel=apple-touch-icon href=https://nathanv.me/img/theme-colors/green.png>
43
44 <!-- Fonts -->
45 <link href="{{ (resources.Get "fonts/Inter-Italic.woff2").RelPermalink }}" rel="preload" type="font/woff2" as="font" crossorigin="">