feat(i18n): a site whose languages sit in folders ships a root 404 page in the default language #320
Labels
No labels
bug
enhancement
needs-triage
ready-for-agent
ready-for-human
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Cloonar/deckle#320
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
A site whose languages sit in folders (
defaultContentLanguageInSubdir = true) ships a root/404.html, showing the default language's 404 page, so a visitor who hits an unprefixed address that does not exist (/leistungen/on cloonar.com) sees the site's own page, in the site's default language, with a 404 status — not the host's generic page.Current behavior
Reproduced on 2026-10-20 with Hugo 0.167.0 extended (the version
harness initpins) on a fresh scaffold site with one page, afterharness lang add en:Hugo writes one 404 page per language folder and none at the root. The hosts that answer a miss with the root
/404.html— deckle-hosted (its Caddyfile tries/<first path segment>/404.html, then/404.html, with status 404, then its own page), Netlify, GitHub Pages, plain nginx or Caddy — therefore catch/en/…and/de/…misses with the site's page and every unprefixed miss with their own. On cloonar.com (2026-10-20),/404.htmland/leistungen/answer the host's generic page;/de/404.htmlis the site's. The German sitemap lists only/de/,/de/produkte/…and/de/datenschutz/:/leistungen/is a URL of the site cloonar.com replaced, which is exactly the kind of address only a root 404 page can catch.A one-language site builds
/404.htmlat the root today and must keep doing so.Desired behavior
/404.html: the same document as/<default>/404.html(same head, header, texts and footer, in the default language, root-relative links intact), produced by the theme so that deckle-hosted's plainhugo --minifybuild — pinned Hugo, no network, no harness step — gets it too./404.htmlis a copy, never an alias page: a visitor stays on the address they typed with the 404 status, instead of being bounced to/<default>/404.htmlwith a 200.harness lang removetakes a site back to one language, the next build has no extra file: the root/404.htmlis then Hugo's own again.Decisions
harness lang addand the engine write noaliases, and no machine-owned redirect list is introduced. Reasons: B would reverse ADR 0017 (the flip writing aliases is what PR #193 removed), it only helps addresses that were real pages before the flip (cloonar.com's/leistungen/was not), andmigrate-sitealready covers an old site's URLs with hand-written/../x/aliases. Record that decision where it is durable: ADR 0017's consequences gain a line saying that redirects for the flip were considered in #320 and declined, and that the root 404 copy is the answer to unprefixed misses.resources.FromString "404.html" <document> | .Publish, executed while rendering the default language's 404 page, writespublic/404.htmlat the root of adefaultContentLanguageInSubdirbuild, also under--minify. Rejected alternatives, verified not to work or not acceptable: the 404 kind takes no front matter so it cannot carry an alias; a content page withurl: /../404.htmllands at the root but is a regular page in every collection and does not use the 404 layout; Hugo has no setting that renders the 404 page at the root.hugo.IsMultilingualis true and the language being rendered ishugo.Sites.Default's — the owner of "which language is the default" the theme already uses (partials/l10n/source.html,baseof.html'sx-default). The 404 layout calls it; it does not re-derive the default fromhugo.tomlor language weights. On a one-language site the gate is false and nothing changes.baseof.htmlrenders it, not themainblock alone. How the theme obtains the full document as a string is the implementer's call (the 404 layout rendering outside baseof'sblock "main"through the same partials, or baseof's shell becoming a partial both use); whichever it is, the theme README's layout-file contract for404.htmlsays it.hugo.tomlscaffold comment and theinternal/langcmdpackage comment (both say "nothing redirects the old unprefixed addresses") stay true and gain the sentence that the site's own 404 page answers them.Key interfaces
themes/deckle/layouts/404.htmlandlayouts/_default/baseof.html— the 404 page's document, and the root copy on a multilingual site (starting hint; the layout-file list is in the theme README "Layout files").hugo.Sites.Default.Language.Lang— the default-language rule the theme already evaluates; call it.layouts/404.html — its texts are i18n strings) — where the new fact and the changed contract line go.scaffold/site/hugo.tomlheader comment and theinternal/langcmdpackage doc — one sentence each.docs/adr/0017-the-engine-writes-no-aliases.md— consequences gain the #320 decision.redo-theme(step 3 lists404.htmlamong the files whose contract a redone theme keeps — name the root copy),verify-site(the "look at the output's top level" paragraph for a multilingual build lists the root404.htmlbeside the per-language folders and the redirectingindex.html),migrate-site(section 2 "Addresses" bullet and step 4's alias note: hand-written/../x/aliases where a URL must keep working, the root 404 page for everything else).Acceptance criteria
hugo --minifyon a scaffold site with two languages (harness init, thenharness lang add) writespublic/404.html, and it is the same document aspublic/<default>/404.html(the Germannotfound_*strings on a German-default site, the full head, header and footer, root-relative links), with no meta refresh.public/tree against a build frommain).harness lang removetakes a two-language site back to one, the next build'spublic/404.htmlis Hugo's own 404 page and nopublic/<code>/404.htmlremains.internal/theme) cover the root copy on a multilingual fixture and its absence on a single-language one.hugo.tomlscaffold comment and theinternal/langcmdpackage comment state the behaviour;scaffold/skills_test.go,internal/element/agentsdoc_test.goanddocs/docs_test.gopass.redo-theme,verify-siteandmigrate-siteare revisited as the Key interfaces section says, pointing at the owning sections rather than restating them.How to verify
go build ./... && go test ./...(the web client'sdistmust exist for the harness binary; the theme tests need the pinned Hugo, whichinternal/hugobindownloads).git initand commit, add a page, build with the pinnedhugo --minify -d <tmp>,find <tmp> -name '*.html' | sort; thenharness lang add en --label English, build again, list again, anddiff <tmp>/404.html <tmp>/de/404.html.harness checkon the scaffold site before and after the flip.curl -s -o /dev/null -w '%{http_code}\n' https://<site>/does-not-exist/and read the body; expect 404 and the site's own page.Assumptions to check
resources.FromString … .Publishkeeps writing to the publish root, not the language folder, on the Hugo versions a site may pin above 0.167.0; verified on 0.167.0 only..Publishcalls,.Storeflags in baseof).try_files /<segment>/404.html /404.htmlorder is what the deckle-hosted snapshot of 2026-10-20 shows (it is indeploy/Caddyfile), so the root copy is served only for misses outside a language folder.Out of scope
harness lang add,harness lang remove,migrate-site's hand-written aliases and ADR 0017's rule are unchanged.harness checkand the editor's link targets: links are language-neutral page references the theme resolves at render time, andharness checkhas no URL-level check, so neither needs to know about 404 pages or redirects.Blocked by
None - can start immediately
Original report
Constraints and observations kept from the maintainer's report of 2026-10-20:
hugo --minify: the site's pinned Hugo, in a container without network, with no harness step. Whatever produces/404.htmlmust come out of Hugo itself: the theme's templates,hugo.toml, or front matter the harness writes once./404.htmlthat is itself an alias to/<default>/404.htmlwould send the 404 status first, then leave the visitor on/de/404.htmlwith a 200./is an alias page to/de/;/de/404.htmland/en/404.htmlexist;/404.htmldoes not;/leistungen/and anything below it answer the host's generic 404 page.harness lang addat the flip and bymigrate-sitefor an old site's URLs. Its open questions (what the flip records, the reverse flip, renames, migrations) were not answered because the option was dropped.feat(i18n): a site whose languages sit in folders keeps its old addresses — a root 404 page, redirects, or bothto feat(i18n): a site whose languages sit in folders ships a root 404 page in the default language