This site had a bug that survived several deploys unnoticed: the whole site worked, except that the OpenGraph card, the image that link previews show when you paste a URL into WhatsApp or LinkedIn, returned 404 in production. Browsers never load that file when you visit a page, so nothing looked broken. Every build was green. The file was in the repository. And the URL was dead.

If you want the ending first: declaring any entry under module.mounts for a target root replaces Hugo’s default mount for that root. I had mounted one subfolder of static, and in doing so had silently unmounted everything else in it.

The debugging, in layers

The meta tag pointed at a literal path:

<meta property="og:image" content="/images/site/og-card.jpg" />

Layer one: the wrong directory. The image lived in assets/. Hugo only publishes things from assets/ when they go through the asset pipeline (resources.Get and friends); a file that is merely referenced by a hard-coded path in a meta tag never gets published. Fine, that one is on me: I moved the file to static/, where everything is supposed to be copied through verbatim, and pushed.

Still 404.

Layer two: the part worth writing down. The file was in the repo, the deploy succeeded, and the local build reproduced the problem, public/images/site/ simply never appeared. The config contained this, inherited from the theme’s example site:

module:
  mounts:
  - source: static/files
    target: static/files
  - source: ./node_modules/@fontsource/mulish/files
    target: static/files

That first mount looks harmless. It is not. The moment you declare a mount whose target falls under static, Hugo stops applying its default static -> static mount. From then on, the only things published from your static directory are the ones you mounted explicitly. static/files made it through; static/images no longer existed as far as the build was concerned.

The fix is one line, mounting the whole directory back:

module:
  mounts:
  - source: static
    target: static
  - source: ./node_modules/@fontsource/mulish/files
    target: static/files

Rebuild, and public/images/site/og-card.jpg exists. Push, and the 404 became a 200.

Why this class of bug is nasty

Three things conspired to hide it:

  1. The failure was invisible in normal use. Browsers never request an og:image during ordinary browsing, so the site looked perfect while every link preview quietly failed.
  2. The build cannot fail. Not publishing a directory is not an error. Hugo did exactly what the config said, which was not what I meant.
  3. The behaviour is a side effect at a distance. The mount that broke static/images mentions neither. You have to know that customising mounts for a target root switches off the default for that root, which the documentation does state, in the calm voice documentation uses for things that will one day cost you an evening.

The transferable lesson

When a framework lets you customise something that normally has a sensible default, check whether your customisation extends the default or replaces it. Hugo mounts replace. And after any deploy involving files that nothing on the page loads, verify the artefact, not the build: a green pipeline proves the site compiled, while curl -I against the actual URL proves the file shipped. The second check takes ten seconds and is the only one of the two that would have caught this.