Right, let’s talk about the assets/ directory. This is where you put the raw materials—the stuff that isn’t ready for the web just yet. Think of it as your workshop, not the showroom. These are the files you want Hugo, specifically its Hugo Pipes feature, to get its hands dirty with: your un-minified CSS, your un-bundled JavaScript, your un-optimized images, your SASS/SCSS files.

The magic, and the entire reason this directory exists, is that only files in the assets/ directory (and a few other designated folders) are available to Hugo’s template functions like resources.Get. You can’t just reach into some random folder from your template; Hugo needs to know where to look for these source files. This is a good thing. It keeps things organized and prevents you from accidentally processing your taxes_2022.xlsx file as a Sass stylesheet, which would probably result in a very depressing color scheme.

What Actually Goes In Here?

This isn’t a junk drawer. Be intentional. Here’s the inventory:

  • CSS/Sass/SCSS/Less Files: Your main .scss or .sass files live here. You @import your partials from here. This is the starting point.
  • JavaScript/TypeScript Files: Your main entry points for scripts. You’ll use Hugo Pipes to bundle and transpile these.
  • Source Images for Processing: Logos, icons, or pictures you intend to have Hugo resize, convert, or otherwise manipulate. If you’re just plopping a full-size photo into a page, static/ is fine. If you want Hugo to generate five different sizes for a responsive image set, the source goes in assets/.
  • Font Files: If you need to process them (e.g., subsetting, though Hugo doesn’t do that natively), they go here. Otherwise, static/ is simpler.
  • Other Frontend Assets: Things like .svg files you want to inline directly into your HTML, or data files you want to read into a template.

The Workflow: From Source Asset to Final Output

You don’t link to files in the assets/ directory. You process them in your templates. The classic example is your CSS. You wouldn’t do this:

<!-- This is WRONG. This file is not publicly accessible. -->
<link rel="stylesheet" href="{{ "css/main.scss" | absURL }}">

Instead, you use Hugo Pipes to grab the resource, process it, and then output it where it needs to go. The processed file will be written to the public/ directory (or wherever your final site is built) with a cache-busting fingerprint in its filename.

Here’s how you do it right for a Sass file:

{{- with resources.Get "css/main.scss" | toCSS | minify | fingerprint -}}
<link rel="stylesheet" href="{{ .RelPermalink }}" integrity="{{ .Data.Integrity }}" crossorigin="anonymous">
{{- end }}

Let’s break down that pipeline, because it’s beautiful:

  1. resources.Get "css/main.scss": Fetches the source file from /assets/css/main.scss.
  2. | toCSS: This is the key. Hugo sees the .scss extension and hands it off to the built-in LibSass compiler, turning it into plain CSS. If it was just a .css file, this step would be a no-op.
  3. | minify: Takes that CSS and crunches it down using the popular minify library.
  4. | fingerprint: Generates a unique hash based on the file’s contents (e.g., main.min.abcd1234.css) and also generates the SRI integrity attribute. This is for cache busting and security. The file is now ready.

The same pattern applies to JavaScript:

{{- $js := resources.Get "js/main.js" | js.Build | minify | fingerprint -}}
<script src="{{ $js.RelPermalink }}" integrity="{{ $js.Data.Integrity }}" crossorigin="anonymous"></script>

Common Pitfalls and How to Avoid Them

The Path Trap: The path inside resources.Get is relative to the assets/ directory. If you have a file at assets/css/theme/colors.scss, you reference it as resources.Get "css/theme/colors.scss". This seems obvious until 2 AM when you’ve been debugging for an hour only to realize you’ve been using assets/css/theme/colors.scss in the function call. Don’t be that person. I’ve been that person.

The Import SASS Quandary: When you’re in your main.scss and you want to import a partial, your import paths are also relative to the assets/ directory. This is the one that feels a bit absurd, but you get used to it.

Your file structure:

assets/
└── css/
    ├── main.scss
    └── partials/
        └── _variables.scss

Inside main.scss, you’d import with:

// This is correct. Path is relative to the assets/ directory.
@import "css/partials/variables";

// This will FAIL. It's not looking relative to the current file.
@import "partials/variables";

Yes, it feels redundant to say css/ from inside the css/ folder. The designers made a choice here for consistency—all resource paths are from the root of assets/. It’s questionable, but it’s what we have. Just be consistent.

Caching and Live Reload Quirks: Hugo is brilliantly fast because it caches aggressively. Sometimes, too aggressively. If you change a SASS partial that’s imported into main.scss, Hugo might not instantly detect the change and rebuild the CSS. If your live reload seems to be slacking, stop the server and run with hugo server --ignoreCache to be sure. It’s a small annoyance for the sake of incredible build speed the rest of the time.