Alright, let’s talk about stuffing your site with files. You’ve got your content in Markdown, but a website isn’t made of words alone. You need images, PDFs, maybe a weird favicon.ico you made in a fit of inspiration at 2 AM. This is where Page Resources come in.

Think of a Leaf Bundle as a self-contained folder for each of your pages. It’s not just the index.md file; it’s everything that goes with that page. Hugo’s genius here is its brutal, beautiful simplicity: any file you drop into the same directory as your index.md (or _index.md for sections) becomes a page resource for that page. Hugo automatically knows about it, can process it, and, crucially, can link to it.

This is infinitely better than the old way of just chucking everything into a giant static folder and hoping you remember the path later. This keeps assets logically coupled with their content. Deleting a page? You delete its folder, and all its resources are gone too. No orphaned files. It’s clean.

The Absolute Basics: It’s Just a Folder

Here’s the file structure. It’s not a suggestion; it’s the law.

content/
└── posts/
    └── my-cool-post/
        ├── index.md          # Your content
        ├── hero-image.jpg    # A page resource
        ├── diagram.png       # Another page resource
        └── spec.pdf         # Yet another page resource

In this setup, hero-image.jpg, diagram.png, and spec.pdf are all Page Resources for the page my-cool-post. Hugo, being the attentive butler it is, will see these and make them available to you.

How to Access These Resources in Your Templates

This is where the magic happens. In your template for the page (e.g., single.html), the current page object (.) has a .Resources method. This returns a slice of all the resources for that page.

But you don’t want to just list them all; you want to find a specific one. For that, you use .GetMatch or its more specific siblings. The simplest way is by filename:

{{ with .Resources.GetMatch "hero-image.jpg" }}
  <img src="{{ .RelPermalink }}" alt="Hero Image" width="{{ .Width }}" height="{{ .Height }}">
{{ end }}

Let’s break this down. We’re using with because .GetMatch might return nil if it finds nothing, and we’re not fond of template errors. The .RelPermalink is the relative URL to the file. But notice we’re also grabbing .Width and .Height! This is Hugo providing image metadata for free. You can use this to inject native width and height attributes to prevent Cumulative Layout Shift (CLS), which is something you absolutely should care about. Google gets very snippety about it.

The Power of Processing: Images

You could just serve the images as-is. But that would be boring and often inefficient. Hugo has a killer feature baked in: image processing. We’re not just grabbing files; we’re transforming them.

Want a thumbnail? You got it.

{{ $image := .Resources.GetMatch "hero-image.jpg" }}
{{ $thumbnail := $image.Resize "400x q85" }}
<img src="{{ $thumbnail.RelPermalink }}" 
     alt="Thumbnail" 
     width="{{ $thumbnail.Width }}" 
     height="{{ $thumbnail.Height }}">

Here’s the why: you should never serve a 4000px wide masterpiece to someone browsing on a phone. You resize that beast to the exact dimensions you need in your template. The q85 specifies 85% JPEG quality, which is the sweet spot between visual fidelity and file size. This isn’t just a convenience; it’s a core performance best practice. Hugo generates these processed images on the first run and caches them forever (or until you next run hugo --gc).

Finding Resources with Patterns

What if you have a bunch of images and you want to do something with all of them? .GetMatch takes a pattern, not just a full filename.

{{ $images := .Resources.ByType "image" }}
{{ range $images }}
  <figure>
    <img src="{{ (.Resize "800x").RelPermalink }}" alt="{{ .Name }}">
    <figcaption>Image: {{ .Name }}</figcaption>
  </figure>
{{ end }}

ByType "image" is a fantastic method that filters resources for, you guessed it, images. It’s smart too—it knows the MIME type, so a .jpg and a .png are both “images”. You can also use wildcards with .GetMatch: *.png gets you all PNGs.

The Rough Edges and Questionable Choices

Now, the honest part. This system is brilliant, but it has one glaring, forehead-smackingly obvious omission: you cannot organize resources into subfolders. Nope. The designers drew a hard line. Every resource file must sit directly alongside the index.md file. Want to keep your images in an /images subfolder inside your page bundle? Tough luck. It breaks. Hugo will not see them.

This is, frankly, a bizarre and frustrating limitation. It leads to cluttered directories, especially for media-rich pages. The official reasoning is about simplicity and predictability, but anyone who has managed more than five images in a single post will tell you it’s a pain. The best practice is to use descriptive filenames (hero-image-background.jpg, hero-image-foreground.png) and live with the mess. It’s the one thing about Page Resources that feels decidedly last-century.