Hugo — When Changes Don't Show Up

Hugo — When Changes Don’t Show Up

Search terms: hugo not updating · image not showing · white space instead of image · hard refresh · port 1313 already in use · stale public · serving pages from disk · --cleanDestinationDir · development vs production

Written 2026-08-09 after an image showed as blank white space. Cause turned out to be three separate things stacked, each of which alone would have hidden the problem.


The decision tree

Work down this list. Each step is cheap and rules out a whole class of cause.

# 1. Is more than one server running? (a second one silently takes another port)
pgrep -af "hugo server"

# 2. Is the file actually published? Build into a CLEAN dir and look.
rm -rf /tmp/hugo-check && hugo --destination /tmp/hugo-check
find /tmp/hugo-check -name "the-file-you-expect.*"

# 3. Does the server actually serve it? (proof, not a rendered page)
curl -s -o /dev/null -w "%{http_code} %{size_download} bytes\n" http://localhost:1313/path/to/file.png

# 4. Only then suspect the browser.
#    Ctrl+Shift+R  (Ctrl+Shift+F5 and Ctrl+F5 do the same thing)

Do step 3 before step 4. A 200 with a byte count is evidence. A page that looks right can be lying via cache; a page that looks wrong can be a genuinely missing file.


Cause 1 — a second server on another port

port 1313 already in use, attempting to use an available port

Hugo does not fail when 1313 is taken. It quietly picks a random high port (38311, in the real case) and starts anyway. If a forgotten server is still running in another terminal, the browser tab pointed at :1313 keeps being served by the old process — so edits appear to have no effect no matter how many times you restart.

That line scrolls past in the startup banner and is easy to miss. Check for it, or:

pgrep -af "hugo server"      # expect exactly one
ss -tln | grep 131           # expect 127.0.0.1:1313

Cause 2 — stale public/

Hugo does not clean public/ between builds. Delete a source file and its built copy stays forever. Over months, orphans accumulate — and a page whose source is broken keeps rendering correctly from files that no longer have any source at all.

That is what hid a broken page for a month here: six orphaned images, one of them dated over five weeks earlier, with the page rendering fine the whole time.

Find the orphans:

find public -type f \( -name "*.png" -o -name "*.jpg" \) | while read f; do
  find content -name "$(basename "$f")" -type f 2>/dev/null | grep -q . || echo "ORPHAN: $f"
done

Fix — public/ is disposable build output, gitignored, never deployed from here:

rm -rf public && hugo

hugo --cleanDestinationDir does roughly the same by asking Hugo to remove what it doesn’t recognise. rm -rf is simpler to reason about: nothing can survive, because nothing is there.

Check before nuking that nothing hand-made lives in public/ — a CNAME, .nojekyll, _redirects, or robots.txt you wrote yourself would not be regenerated. On this site there is nothing: every file is Hugo output.

“Serving pages from disk” in the startup banner means the server is serving public/ directly rather than rendering to memory — so a stale public/ is served to the browser, not just left lying around. That appears after you run hugo before hugo server.


Cause 3 — browser cache

Images are cached hard. Change one and the browser will keep showing the old bytes through ordinary reloads and through Hugo restarts, because the problem is not on the server side.

Ctrl+Shift+R · Ctrl+Shift+F5 · Ctrl+F5 — all the same thing, all bypass cache.

Restarting Hugo fixes stale server output. Hard refresh fixes stale browser cache. They are different problems, and restarting Hugo will never fix the second one.

A renamed file dodges the cache entirely, which is why renaming an image can suddenly “break” something that was actually broken all along — the rename just removed the cached copy that was covering it up.


Image paths: relative vs absolute

The underlying bug in the real case. A page at content/github/How to/index.md is a leaf bundle (a folder containing index.md), served at /github/how-to/.

![git status](images/gitstatus.png)

That relative path resolves against the page’s own URL, i.e. /github/how-to/images/gitstatus.png. But the images lived in content/github/images/, which Hugo publishes to /github/images/. The two never met.

Reference style Resolves against Use when
images/foo.png The page’s URL Image is inside the page bundle
/github/images/foo.png Site root Image is in a shared section folder

Both are valid. The mismatch is the bug. To confirm which you need, build clean and look at where the file actually lands:

rm -rf /tmp/hugo-check && hugo --destination /tmp/hugo-check
find /tmp/hugo-check -name "*.png"

Shortcodes in prose need escaping

Writing about a shortcode is not the same as using one. Hugo parses shortcodes before markdown, so backticks do not protect them — a literal {{< relref >}} written inside inline code is still executed, and with no arguments it fails the whole build:

failed to render shortcode "relref": ... invalid value; expected string

Escape with the comment form — {{< ... >}} renders as literal text. Fenced code blocks (```) do protect shortcodes; inline backticks do not.


development vs production

hugo server runs as development. hugo builds as production.

development production
draft: true shown hidden
Future-dated content shown hidden
Minification off on, if configured
LiveReload injected yes no

Seeing Environment: "development" in the server banner is normal, not a warning.

The trap is row one: a page that looks fine in hugo server can be absent from a real build. Check before deploying:

grep -rln "^draft: true" content/

Habits that prevent all of this

  • Verify with a clean build, not public/hugo --destination /tmp/hugo-check and inspect that. public/ carries history you didn’t intend.
  • Verify with curl, not eyes — an HTTP code and a byte count can’t be faked by a cache.
  • One serverpgrep -af "hugo server" when anything seems not to update.
  • Hard refresh after any asset change, every time.

The thread running through all four: something old lying around made a broken thing look correct. The fix in every case is to check the actual artifact rather than the convenient summary.