Model Post Images as Hugo Page Resources, Not static/ Files
Hard-coded image URLs in static/ break when baseURL or language settings change. Model post images as Hugo page resources in leaf bundles so Hugo generates portable URLs and you gain image-processing control.
15 Sept 2026, 19:34 UTC

Hard-coded image URLs are a portability trap. You drop hero.jpg into static/, write <img src="/images/hero.jpg"> in a post, and everything looks fine—until you change baseURL, add a language prefix, enable pretty URLs, or simply move the Markdown file. The link breaks, and the build gives you no warning. The fix is to stop treating post images as loose files and start treating them as page resources inside a leaf bundle.
The takeaway: when an image sits beside the Markdown that uses it, Hugo can generate the URL for you. Links stay portable across moves and URL configuration, and you gain access to Hugo’s image-processing API without leaving the template.
Why static/ breaks content-aware URLs
Files under static/ are copied verbatim into the output. Hugo does not attach them to any page, so it cannot rewrite their paths when your site’s URL structure changes. If you hard-code /images/hero.jpg, that string is brittle in at least four ways:
- Base URL changes. Moving from a project page to a custom domain or switching between staging and production can invalidate absolute paths.
- Language prefixes. Multilingual sites often prepend
/en/or/fr/; a hand-written root-relative path ignores that context. - Pretty URLs. Changing
uglyURLsor permalink settings alters the final path shape. - Content moves. Renaming or reorganizing a post leaves the image behind in
static/, and nothing flags the orphan.
None of this means static/ is useless. It is still the right home for site-wide assets—favicons, global CSS, JavaScript bundles—that are not tied to a specific piece of content.
Leaf bundles keep assets beside their Markdown
A leaf bundle is a content directory that contains an index.md file and represents a single page. In a typical setup, the path looks like content/posts/my-post/index.md. Any non-content file placed in that same directory—hero.jpg, a PDF, a data file—becomes a page resource.
Templates fetch those resources with .Resources.GetMatch and emit .RelPermalink or .Permalink. Because Hugo resolves the path at build time, the generated URL adapts to your baseURL, language settings, and permalink configuration automatically.
One caution: resource scope in branch bundles (directories with an _index.md that represent list pages) is more subtle. Do not assume resources inherit cleanly from parent to child; verify the behavior in your version before relying on it.
A worked responsive-image example
Assume a leaf bundle at content/posts/my-post/ containing index.md and hero.jpg. A template partial can look up the image and emit a fixed-size social-card variant:
{{ $img := .Resources.GetMatch `hero.*` }}
{{ with $img }}
{{ $hero := .Fill `1200x630` }}
<img src="{{ $hero.RelPermalink }}"
width="{{ $hero.Width }}"
height="{{ $hero.Height }}"
alt="A concise description of the hero image">
{{ else }}
{{ warnf "Missing hero image in %s" .Path }}
{{ end }}
Run this inside your layout templates, not in a content file. The with guard matters: .Resources.GetMatch returns nil when no file matches, and rendering .Fill on nil will fail the build. During development, failing loudly is better than shipping a broken <img> tag.
Use .Fill when you need an exact dimension, such as a 1200×630 Open Graph card. Use .Resize when the aspect ratio must be preserved. Generate only the sizes your design actually consumes; every extra variant adds build time.
Image methods, supported formats, and build flavors are version-sensitive. If you plan to emit WebP or other conversions, confirm that your installed Hugo distribution supports the source and target formats before promising them in production. Treat exact method names and options as something to verify in a scratch site.
The trade-off: build cost and operational complexity
Page resources are not free. Resizing many large images can slow builds and stress the resource cache. A site with hundreds of high-resolution photos may see meaningful build-time increases, especially in CI where caches start cold.
Multilingual sites introduce another wrinkle: translated posts often duplicate the same hero image across language-specific bundles. Before migrating, decide whether translated images belong in language-specific bundles or in a shared location. Duplicating large binaries wastes cache space and build time.
Keep site-wide files in static/, share truly common assets, and centralize resource lookup in a partial so authors stop hand-writing paths. A single partial means you change the lookup logic once instead of hunting through dozens of templates.
Migrate one section at a time
Do not convert every content directory in one commit. Pick one high-traffic section, move its images into leaf bundles, and update the templates that render it. This limits blast radius and gives you a real build before you touch the rest of the site.
Before changing production templates, run hugo version in your terminal and test the exact snippet in a scratch site. While editing a bundle, use:
hugo server --buildDrafts --disableFastRender
Run this in your project root. It requires no special permissions, but it does watch the filesystem, so keep the terminal open while you edit. The --disableFastRender flag ensures resource changes are reflected without a full restart.
For a production-like check, build with path warnings enabled:
hugo --printPathWarnings
Inspect the generated HTML for src, width, and height attributes. Then rebuild with a production-like --baseURL to catch absolute-path assumptions. In CI, run a clean build:
hugo --gc --minify
and diff the public output for missing or duplicated images. A quick manual check: touch hero.jpg (for example, touch content/posts/my-post/hero.jpg), rebuild, and confirm the emitted resource path changes as expected. If you customize the cache directory, verify it is preserved or invalidated correctly in your deployment pipeline.
Page resources will not solve every asset problem, and they add build weight when overused. But for post-specific images, they replace brittle, hand-written paths with URLs that Hugo owns—and that is a trade worth making before your next baseURL change.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.