25.1 What Hugo Pipes Are: Processing assets/ Files
Right, let’s talk about Hugo Pipes. Forget what you’ve heard about static site generators just slapping together pre-made files. Hugo Pipes is the reason I can, with a straight face, call Hugo a modern frontend build tool that happens to output static HTML. It’s the engine under the hood that takes your raw, un-minified, un-processed assets and transforms them into the optimized, production-ready CSS and JS you actually want to serve.
Think of it this way: you don’t serve raw flour, eggs, and milk to your dinner guests; you bake a cake. Hugo Pipes is your kitchen. It’s a collection of functions you use within your templates (.html files, basically) to process your assets on the fly during the hugo build process. The key thing to remember: this all happens at build time. Your user’s browser never knows the struggle; it just gets the perfectly baked cake.
The Core Concept: From Resource to Pipe
Everything in Pipes starts with a Resource. This is Hugo’s abstraction for a source file that can be processed. You don’t just use a file path like css/main.css. You have to get its Resource object. For assets in your assets directory (more on that crucial point in a second), you use the resources.Get function.
{{/* This fetches the file assets/sass/main.scss and makes it a Resource object */}}
{{ $style := resources.Get "sass/main.scss" }}
Now you have $style holding a Resource. By itself, it’s not that useful. This is where the “pipe” part comes in. You pipe this resource through a series of transformation functions.
{{ $style := resources.Get "sass/main.scss" | toCSS | minify | fingerprint }}
Look at that. In one line, we’re telling Hugo to:
- Get the SCSS file.
- Pipe it (
|) to thetoCSSfunction to compile it to CSS. - Pipe that result to the
minifyfunction to crush all the whitespace. - Pipe that result to the
fingerprintfunction to add a unique hash to the filename for cache busting.
Each function takes the output of the previous one and returns a new, transformed Resource. It’s beautifully Unix-like.
The Non-Negotiable: The assets Directory
Here’s the first “gotcha” that trips everyone up, and it’s a doozy. Hugo Pipes will only process files from the assets directory at the root of your project. Not static/. Not content/. Not data/. assets/.
This is Hugo being opinionated, and honestly, it’s the right opinion. It creates a clear separation: /static is for files you want copied over verbatim, untouched. /assets is for the raw materials you want to process, compile, and transform. If you try to resources.Get a file from /static, Hugo will look at you like you’ve asked it to microwave a metal bowl—it just won’t do it, and you’ll get a nil value. This is the most common pitfall. Don’t fight it; just create the assets folder and move your source files there.
Why Fingerprinting is Your Best Friend
I need to have a direct talk with you about the fingerprint function. If you’re not using it on your CSS and JS, you’re doing it wrong, and your users are paying the price with stale caches.
fingerprint takes the content of your file, generates a cryptographic hash of it (like main.abcd1234.css), and appends that hash to the filename. Why is this black magic so essential? Cache busting. When you update your main.css file, the content changes, so the hash changes (main.efgh5678.css). The browser sees this as a completely new file and fetches it immediately. Users who had the old file cached get the new one instantly. Without it, you’re relying on ?v=2 querystring hacks and telling users to “hard refresh.” It’s unprofessional. fingerprint also automatically adds a integrity attribute for Subresource Integrity (SRI), which is a nice security bonus.
{{ $script := resources.Get "js/main.js" | minify | fingerprint }}
<script src="{{ $script.RelPermalink }}" integrity="{{ $script.Data.Integrity }}" crossorigin="anonymous"></script>
The Power of Source Maps (And How to Not Blow Your Foot Off)
When you pipe things through multiple transformations (e.g., toCSS then minify), the browser’s dev tools have no idea how the final, minified line of code maps back to your original, beautiful SCSS. This is a nightmare for debugging. This is where sourceMap comes in.
You can ask most pipe functions to generate a source map by passing a parameter. The brilliant part? Hugo handles this for you intelligently.
{{ $style := resources.Get "sass/main.scss" | toCSS (dict "enableSourceMap" true) }}
{{ $style = $style | minify | fingerprint }}
But here’s the genius: when you run hugo server in development mode, it will inline the source map data for easy debugging. When you run hugo build for production, it will omit the source map entirely, keeping your production files lean and mean. You get the best of both worlds without any extra configuration. It’s one of those details that shows the Hugo team actually builds websites.
The One Weird Trick with PostCSS
You want to use Tailwind CSS or Autoprefixer? Of course you do. Hugo has you covered with postCSS. This function is a bit of a diva, though—it requires a local installation of PostCSS and the plugins you need.
npm install postcss postcss-cli autoprefixer
Then, you can use it in your pipes:
{{ $options := dict "use" "autoprefixer" }}
{{ $style := resources.Get "css/main.css" | postCSS $options | minify | fingerprint }}
The rough edge here is that this depends on a local node_modules. It works, but it ties your Hugo build to your Node environment. It’s a necessary evil for accessing that ecosystem, but it’s the one point where the “static” build feels a bit less self-contained.