Hugo Resources
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
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="">