feat(i18n): a multilingual site ships a root 404 page in the default language #321

Merged
dominik.polakovics merged 1 commit from afk/320 into main 2026-10-20 16:13:25 +02:00

Closes #320

What

On a site whose languages sit in folders, the theme now writes a root /404.html: a copy of the default language's whole 404 document. A host that answers a miss with the root /404.html (deckle-hosted's Caddyfile tries /<segment>/404.html, then /404.html; Netlify, GitHub Pages, nginx/Caddy) now serves the site's own page with status 404 for unprefixed misses like cloonar.com's /leistungen/, where it used to serve the host's generic page.

How

  • baseof.html's shell moves into partials/document/open.html and close.html. baseof.html wraps them around block "main", so the shell still has one owner.
  • layouts/404.html no longer uses baseof. It renders its main markup, with its notfound_* strings, between the same two partials and outputs that document.
  • On the multilingual default language (hugo.IsMultilingual and hugo.Sites.Default, the existing owner of the default language), it also publishes the document with resources.FromString. It is a copy, not an alias, so there is no meta refresh and no 200.
  • --minify does not minify a published resource, so the copy goes through minify. The minified result is re-wrapped as 404.html, because minify renames the file to *.min.html and resources.Copy does not undo that.
    • Under hugo --minify the copy is byte-identical to /<default>/404.html.
    • Under a plain hugo it is that same document, minified.

Docs

  • Theme README:
    • new "The root 404 page" section under "Languages";
    • the "Layout files" entries for baseof.html, the document partials and 404.html;
    • every other baseof.html mention now names the partial it moved to.
  • AGENTS.md "Addresses": the No redirects paragraph says what an unprefixed miss gets.
  • hugo.toml scaffold comment and the internal/langcmd package doc: one sentence each.
  • ADR 0017: a consequence records that redirects for the flip were declined in #320.
  • Skills:
    • redo-theme: step 1 font preloads, step 3 contract, and references/theme-contract.md.
    • verify-site: the multilingual top-level listing.
    • migrate-site: section 2 and step 4.
    • remove-element: where the lightbox Store flag now lives.

Verification

  • go build ./... && go vet ./... && go test -count=1 ./... pass with the pinned Hugo 0.167.0 on PATH; the theme tests ran, none skipped. The web vitest suite passes (3357 tests).
  • New tests in internal/theme/notfound_test.go:
    • multilingual, German default, --minify: the root copy is byte-identical to de/404.html, has the German strings, no refresh and no hreflang; the exact file list shows no intermediate or *.min.html file is published;
    • a plain build's copy equals the minified page;
    • single language, plain and minify: no copy.
  • End to end with a freshly built harness:
    • one-language harness init site: diff -r against a build with main's theme is empty;
    • after harness lang add en: harness check passes, the build lists 404.html de/404.html de/index.html de/leistungen/index.html en/404.html index.html, cmp 404.html de/404.html reports identical, and the h1 reads "Seite nicht gefunden";
    • after harness lang remove en: the output is identical to the one-language baseline.
  • Also diffed by the implementer: the i18n, news and library fixtures, plain and minified. The only new file is the multilingual root 404.html.

Not verified here: the hosted acceptance item, an unprefixed miss on a deckle-hosted preview or live site answering 404 with the site's page. It needs a deploy from this branch. I did confirm that the deckle-hosted snapshot's deploy/Caddyfile uses the try_files /{segment}/404.html /404.html order the issue assumes.

Note: the 404 layout's inline partial uses Hugo's _partials/inline/ naming, which needs Hugo 0.146 or later. The theme already depends on hugo.Sites.Default, and harness lang add requires 0.167.

🤖 Generated with Claude Code

Closes #320 ## What On a site whose languages sit in folders, the theme now writes a root `/404.html`: a copy of the default language's whole 404 document. A host that answers a miss with the root `/404.html` (deckle-hosted's Caddyfile tries `/<segment>/404.html`, then `/404.html`; Netlify, GitHub Pages, nginx/Caddy) now serves the site's own page with status 404 for unprefixed misses like cloonar.com's `/leistungen/`, where it used to serve the host's generic page. ## How - `baseof.html`'s shell moves into `partials/document/open.html` and `close.html`. `baseof.html` wraps them around `block "main"`, so the shell still has one owner. - `layouts/404.html` no longer uses baseof. It renders its main markup, with its `notfound_*` strings, between the same two partials and outputs that document. - On the multilingual default language (`hugo.IsMultilingual` and `hugo.Sites.Default`, the existing owner of the default language), it also publishes the document with `resources.FromString`. It is a copy, not an alias, so there is no meta refresh and no 200. - `--minify` does not minify a published resource, so the copy goes through `minify`. The minified result is re-wrapped as `404.html`, because `minify` renames the file to `*.min.html` and `resources.Copy` does not undo that. - Under `hugo --minify` the copy is byte-identical to `/<default>/404.html`. - Under a plain `hugo` it is that same document, minified. ## Docs - Theme README: - new "The root 404 page" section under "Languages"; - the "Layout files" entries for `baseof.html`, the document partials and `404.html`; - every other `baseof.html` mention now names the partial it moved to. - AGENTS.md "Addresses": the **No redirects** paragraph says what an unprefixed miss gets. - `hugo.toml` scaffold comment and the `internal/langcmd` package doc: one sentence each. - ADR 0017: a consequence records that redirects for the flip were declined in #320. - Skills: - `redo-theme`: step 1 font preloads, step 3 contract, and `references/theme-contract.md`. - `verify-site`: the multilingual top-level listing. - `migrate-site`: section 2 and step 4. - `remove-element`: where the lightbox Store flag now lives. ## Verification - `go build ./... && go vet ./... && go test -count=1 ./...` pass with the pinned Hugo 0.167.0 on PATH; the theme tests ran, none skipped. The web vitest suite passes (3357 tests). - New tests in `internal/theme/notfound_test.go`: - multilingual, German default, `--minify`: the root copy is byte-identical to `de/404.html`, has the German strings, no refresh and no hreflang; the exact file list shows no intermediate or `*.min.html` file is published; - a plain build's copy equals the minified page; - single language, plain and minify: no copy. - End to end with a freshly built harness: - one-language `harness init` site: `diff -r` against a build with `main`'s theme is empty; - after `harness lang add en`: `harness check` passes, the build lists `404.html de/404.html de/index.html de/leistungen/index.html en/404.html index.html`, `cmp 404.html de/404.html` reports identical, and the h1 reads "Seite nicht gefunden"; - after `harness lang remove en`: the output is identical to the one-language baseline. - Also diffed by the implementer: the i18n, news and library fixtures, plain and minified. The only new file is the multilingual root `404.html`. **Not verified here:** the hosted acceptance item, an unprefixed miss on a deckle-hosted preview or live site answering 404 with the site's page. It needs a deploy from this branch. I did confirm that the deckle-hosted snapshot's `deploy/Caddyfile` uses the `try_files /{segment}/404.html /404.html` order the issue assumes. Note: the 404 layout's inline partial uses Hugo's `_partials/inline/` naming, which needs Hugo 0.146 or later. The theme already depends on `hugo.Sites.Default`, and `harness lang add` requires 0.167. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(i18n): a multilingual site ships a root 404 page in the default language
All checks were successful
ci / build-and-test (pull_request) Successful in 3m35s
ci / release (pull_request) Has been skipped
36d8108b43
Hugo writes one 404 page per language folder on a site whose languages
sit in folders, and none at the root, so a host that answers a miss with
/404.html served its own generic page for every unprefixed address.

The theme's 404 layout now renders its document itself, between the
document shell partials baseof.html also wraps around its main block
(partials/document/open.html and close.html), and on a multilingual
site's default language (hugo.Sites.Default) publishes that document
again at the root with resources.FromString. It is a copy, not an alias,
so the visitor stays on the address they typed with status 404. The copy
goes through minify, so under `hugo --minify` it is byte-identical to
/<default>/404.html. A one-language site's output is unchanged byte for
byte, and so is the output after `harness lang remove` back to one.

The theme README, AGENTS.md "Addresses", the hugo.toml scaffold comment,
the langcmd package doc, ADR 0017 (redirects for the flip declined) and
the redo-theme, verify-site, migrate-site and remove-element skills
follow.

Closes #320

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Author
Owner

[autoland] verdict: pass

PASS. Signal: CI aggregate success (ci / build-and-test, run 333: npm test, go vet, go test ./... with pinned Hugo 0.167.0, so the internal/theme tests incl. the new notfound_test.go and TestLightboxInProduction ran). Conventions: Conventional Commits title, Closes #320, diff matches the issue (theme 404 + document partials, README/AGENTS.md/ADR 0017/hugo.toml/langcmd docs, redo-theme/verify-site/migrate-site skills, remove-element pointer for the moved Store flag). Default language goes through hugo.Sites.Default, which already owns that rule. Independently checked with Hugo 0.167.0, main's theme vs this PR's on a de site: one-language output identical under diff -r, plain and --minify. On a multilingual site the only new file is the root 404.html, byte-identical to de/404.html under --minify and equal to the minified page on a plain build. deckle-hosted's deploy/Caddyfile tries /{segment}/404.html, then /404.html. Still open after deploy: the hosted acceptance item (an unprefixed miss answers 404 with the site's page).

[autoland] verdict: pass PASS. Signal: CI aggregate success (ci / build-and-test, run 333: npm test, go vet, go test ./... with pinned Hugo 0.167.0, so the internal/theme tests incl. the new notfound_test.go and TestLightboxInProduction ran). Conventions: Conventional Commits title, Closes #320, diff matches the issue (theme 404 + document partials, README/AGENTS.md/ADR 0017/hugo.toml/langcmd docs, redo-theme/verify-site/migrate-site skills, remove-element pointer for the moved Store flag). Default language goes through hugo.Sites.Default, which already owns that rule. Independently checked with Hugo 0.167.0, main's theme vs this PR's on a de site: one-language output identical under diff -r, plain and --minify. On a multilingual site the only new file is the root 404.html, byte-identical to de/404.html under --minify and equal to the minified page on a plain build. deckle-hosted's deploy/Caddyfile tries /{segment}/404.html, then /404.html. Still open after deploy: the hosted acceptance item (an unprefixed miss answers 404 with the site's page).
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Cloonar/deckle!321
No description provided.