NNicheStackTutorials
← All tutorials
Hugo

Custom Hugo 404 Page Without Plugins (Works on GitHub Pages)

Ship a helpful 404 with search, recent posts, and correct HTTP status on static hosts that default to soft-404.

·10 min read·Hugo
Custom Hugo 404 Page Without Plugins (Works on GitHub Pages) cover

Tested on: Hugo 0.139.4, GitHub Pages, Cloudflare Pages, Netlify Problem: Default Hugo 404 was bare. GitHub Pages returned 200 for missing URLs (SEO soft-404).

Create the 404 template

layouts/404.html:

{{ define "main" }}
<article class="not-found">
  <h1>404 — Page not found</h1>
  <p>The page <code>{{ .Permalink }}</code> doesn't exist or moved.</p>

  <section class="not-found-search">
    <h2>Try searching</h2>
    {{ partial "search.html" . }}
  </section>

  <section class="not-found-recent">
    <h2>Recent posts</h2>
    <ul>
      {{ range first 5 (where site.RegularPages "Type" "blog") }}
        <li><a href="{{ .RelPermalink }}">{{ .Title }}</a></li>
      {{ end }}
    </ul>
  </section>

  <p><a href="{{ "/" | relLangURL }}">← Back home</a></p>
</article>
{{ end }}

Must use define "main" if your theme's baseof.html has {{ block "main" . }}.

Styling

assets/css/extended/404.css:

.not-found { max-width: 42rem; margin: 4rem auto; padding: 0 1rem; }
.not-found h1 { font-size: 2rem; margin-bottom: 0.5rem; }
.not-found-recent ul { padding-left: 1.25rem; }
.not-found code { font-size: 0.85em; word-break: break-all; }

GitHub Pages: real 404 status

GitHub Pages serves 404.html at site root for unknown paths — good. But Hugo builds it to public/404.html automatically when the layout exists.

Verify:

hugo --minify
test -f public/404.html && echo OK

Push to gh-pages branch or Actions artifact. Request nonsense URL:

curl -I https://username.github.io/myblog/this-does-not-exist/
# HTTP/2 404

Cloudflare Pages _redirects

static/_redirects (optional catch-all message):

/*    /404.html   404

Careful: broad rules can break assets. I only use this when host doesn't auto-map 404.

Netlify

static/_redirects:

/*  /404.html  404

Or netlify.toml:

[[redirects]]
  from = "/*"
  to = "/404.html"
  status = 404
  force = false

Error I hit

404 page renders inside list template — duplicate header

Forgot {{ define "main" }}. Hugo fell back to layouts/_default/list.html.

.RelPermalink on 404 shows wrong URL

On 404, .Permalink may be the missing path — that's actually useful. Don't use .Title (empty).

Disable 404 in wrong language

For multilingual sites, create layouts/404.html with {{ i18n "notFound" }} strings in i18n/en.toml:

[notFound]
other = "Page not found"

Verify checklist

  • hugo server → visit http://localhost:1313/nope → custom page
  • View source contains recent posts links
  • Production returns HTTP 404 (not 200)
  • GSC Coverage → no spike in "Soft 404"

Result

Bounce rate on 404 URLs dropped from 89% to 61% (users clicked recent post). GSC soft-404 warnings: 0 after GitHub Pages fix.

Tags: Hugo404GitHub PagesUX