Hugo Pipes: Asset Processing and Bundling
Hugo Pipes is Hugo’s built-in asset processing pipeline. It handles Sass/SCSS compilation, CSS and JavaScript minification, fingerprinting for cache busting, and bundling — at build time, without external build tools like Webpack or Vite. For publishers running Hugo sites, understanding Pipes is the difference between manually managing compiled CSS and having the build handle it automatically.
The Assets Directory
Hugo Pipes works with files in the assets/ directory. Unlike static/, which copies files verbatim to the output, assets/ is a processing source — files there are available to Pipes functions but are only written to the output if explicitly processed and referenced.
A typical assets structure for a publication:
assets/
css/
main.scss
_variables.scss
_typography.scss
_layout.scss
js/
main.js
search.js
Processing Sass/SCSS
The Hugo extended binary (required for Sass processing) compiles SCSS to CSS using the resources.Get and toCSS functions:
{{- $style := resources.Get "css/main.scss" | toCSS -}}
<link rel="stylesheet" href="{{ $style.RelPermalink }}">
Pass Sass options to control output:
{{- $opts := dict "outputStyle" "compressed" "enableSourceMap" true -}}
{{- $style := resources.Get "css/main.scss" | toCSS $opts -}}
<link rel="stylesheet" href="{{ $style.RelPermalink }}">
The outputStyle option accepts nested, expanded, compact, and compressed. Use compressed for production.
Fingerprinting
Fingerprinting appends a content hash to the filename — main.css becomes main.abc123.css. This enables aggressive long-term caching (the browser caches forever because the filename changes when content changes):
{{- $style := resources.Get "css/main.scss"
| toCSS (dict "outputStyle" "compressed")
| fingerprint -}}
<link rel="stylesheet"
href="{{ $style.RelPermalink }}"
integrity="{{ $style.Data.Integrity }}"
crossorigin="anonymous">
The integrity attribute adds Subresource Integrity verification — the browser checks that the file hash matches before executing it. This is a meaningful security measure for published sites.
Minification
Minify CSS, JavaScript, JSON, HTML, SVG, and XML with the minify function:
{{- $style := resources.Get "css/main.scss"
| toCSS
| minify
| fingerprint -}}
For JavaScript:
{{- $script := resources.Get "js/main.js" | minify | fingerprint -}}
<script src="{{ $script.RelPermalink }}" defer></script>
Bundling Multiple Files
Combine multiple JavaScript or CSS files into a single bundle with resources.Concat:
{{- $search := resources.Get "js/search.js" -}}
{{- $utils := resources.Get "js/utils.js" -}}
{{- $bundle := slice $search $utils | resources.Concat "js/bundle.js" | minify | fingerprint -}}
<script src="{{ $bundle.RelPermalink }}" defer></script>
The concatenated file is processed as a single resource through the rest of the pipeline.
Environment-Conditional Processing
Skip minification in development for readable output:
{{- $opts := dict "outputStyle" "expanded" -}}
{{- if hugo.IsProduction -}}
{{- $opts = dict "outputStyle" "compressed" -}}
{{- end -}}
{{- $style := resources.Get "css/main.scss" | toCSS $opts -}}
{{- if hugo.IsProduction -}}
{{- $style = $style | minify | fingerprint -}}
{{- end -}}
<link rel="stylesheet" href="{{ $style.RelPermalink }}">
Hugo sets hugo.IsProduction to true when the HUGO_ENV environment variable is production, or when building with the --environment production flag. In your Cloudflare Pages or Netlify build config, set HUGO_ENV=production.
PostCSS Integration
For PostCSS (autoprefixer, CSS nesting, custom properties fallbacks), install PostCSS and configure it alongside Hugo:
npm init -y
npm install postcss postcss-cli autoprefixer
Create postcss.config.js:
module.exports = {
plugins: [
require('autoprefixer'),
]
}
In your Hugo template, pipe through postCSS after toCSS:
{{- $style := resources.Get "css/main.scss"
| toCSS
| postCSS
| minify
| fingerprint -}}
Hugo calls the PostCSS CLI automatically if it is available.
JavaScript with ESBuild
Hugo extended includes ESBuild integration for JavaScript bundling and transpilation:
{{- $opts := dict
"targetPath" "js/app.js"
"minify" hugo.IsProduction
"params" (dict "env" hugo.Environment) -}}
{{- $script := resources.Get "js/main.js" | js.Build $opts -}}
<script src="{{ $script.RelPermalink }}" defer></script>
js.Build handles ES module imports, tree-shaking, and transpilation. Import other modules in your JavaScript:
// assets/js/main.js
import { initSearch } from './search.js';
import { setupLazyLoad } from './lazyload.js';
document.addEventListener('DOMContentLoaded', () => {
initSearch();
setupLazyLoad();
});
Hugo resolves the imports and bundles them into a single output file. This eliminates the need for a separate Webpack or Rollup configuration for most publishing site JavaScript needs.
Caching
Hugo caches Pipes processing results in the resources/ directory. Commit this directory to your repository to avoid regenerating assets on every CI build. With the cache committed, only changed source files trigger reprocessing.
For sites with many assets, this meaningfully speeds up build times in CI environments where npm packages and the cache directory are both available from the previous build.
Practical Template Pattern
A complete base template head section using Pipes:
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ if .IsHome }}{{ .Site.Title }}{{ else }}{{ .Title }} | {{ .Site.Title }}{{ end }}</title>
{{- $cssOpts := dict "outputStyle" (cond hugo.IsProduction "compressed" "expanded") -}}
{{- $style := resources.Get "css/main.scss" | toCSS $cssOpts -}}
{{- if hugo.IsProduction -}}
{{- $style = $style | minify | fingerprint -}}
<link rel="stylesheet" href="{{ $style.RelPermalink }}" integrity="{{ $style.Data.Integrity }}" crossorigin="anonymous">
{{- else -}}
<link rel="stylesheet" href="{{ $style.RelPermalink }}">
{{- end -}}
</head>
This pattern compiles, minifies, and fingerprints in production while keeping readable expanded CSS in development.