feat(l10n): multilingual sites — content model, translation API and harness lang #193

Merged
dominik.polakovics merged 3 commits from afk/190 into main 2026-10-10 21:18:13 +02:00

Closes #190

What

The engine and CLI side of multilingual sites:

  • Config. harness.yaml languages is a list of {code, label} entries; the first entry is the default. The legacy languages: [en] form still loads. harness init writes the new shape and pins Hugo 0.167.0.
  • Content model. A translation is content/<dir>/_index.<code>.md (also index.<code>.md / <name>.<code>.md) beside its source; there is no content/<code>/ directory. As in Hugo, only a configured non-default language makes a file a translation: node.js.md, jquery.min.md or faq.en.md stay ordinary pages.
    • Connected translations (l10n: connected) keep the source's uids in the same structure. The new internal/l10n package enforces this on write and repairs it on open.
    • Appearance-tab params are synced from the source. General params and bodies are translatable.
    • l10nSource holds a hash of the source's translatable values and drives the "changed since translated" marks.
  • Addresses. Once two languages exist, every language is served under /<code>/, with per-segment slug overrides from each ancestor's translation. Tree nodes get a new address field; permalink stays the language-neutral link value.
  • API.
    • Every lang-scoped endpoint resolves to the translation file. GET /api/page answers missing: true for an untranslated page.
    • /api/tree?lang= returns translated titles plus a translation state per node (translated / missing / outdated).
    • New endpoints: translate page (connected / free / empty, with subpages), translate-plan, translate element, translate all, disconnect, element delete-plan.
    • Structural element ops on a source page propagate into its connected translations. On a connected translation they answer 409.
    • Publishing is site-wide: the lang parameter of publish-all, pending and discard-all is ignored. Pending rows carry lang, and badges and undo/discard work per file.
    • The README section "Translations on the wire" documents every shape.
  • harness lang add|remove.
    • Writes both config files and the language's draft home page.
    • Refuses when the effective Hugo version is below 0.167.
    • remove refuses while translations exist unless --purge; the default language cannot be removed.
    • Re-running lang add for an existing language is a no-op with exit 0 (status already-present).
  • harness check. Reports a translation without a source (error), a connected translation whose structure has drifted or that has elements without a uid (warnings), a page named with the default language's code on a multilingual site (warning), and hugo.toml languages, default-language or subdir settings that disagree with harness.yaml (errors).
  • Docs and skills.
    • scaffold/site/AGENTS.md: new sections Languages, harness lang, Addresses, Translations, Language checks, and Translations by hand.
    • README: harness lang, Multilingual sites, and the wire shapes.
    • Skills updated: setup-site, migrate-site, verify-site, edit-content, add-element, add-page-type, create-feature.

Where this differs from the PRD (checked on Hugo 0.167.0)

  • [languages.<code>] label, not languageName. languageName is deprecated since Hugo 0.158 and prints a warning on every build. The theme issue (#191) should read .Language.Label.
  • Aliases are relative to the language root once languages are in subdirectories: an alias /x/ is emitted at /en/x/. So the engine writes aliases without a language prefix, and the one-time flip writes /../services/, which Hugo path-cleans to /services/. I confirmed in the build output that the old unprefixed URLs redirect. Home is never aliased: Hugo's own root redirect covers /, and an alias / is a build error.
  • A root-relative url front matter is not language-prefixed by Hugo, so the address uses it as written.
  • Uppercase language subtags (pt-BR) are published lowercase (/pt-br/), and the address follows that.
  • Shortcodes inside a text body (inline chips) are body content, not translated elements, so a translator can add or reorder them.
  • A cut into a page whose translation is free drops the translated element. This keeps free translations "never written by propagation".

Known limits and follow-ups

  • deckle-hosted builds with its own nix-shell Hugo, whose pin is not changed here. A site with two languages may fail to verify there until that repository moves to Hugo 0.167 or newer.
  • Theme (#191). Hugo reads type, weight, layouts and page-type fields per file, while translations deliberately hold only title, slug, aliases, description, menuTitle, draft and l10n. The theme has to read those from the default-language page, as the PRD says.
    • Page-type fields such as a news teaser are therefore not translatable, which is the PRD's decision.
    • Taxonomy list pages (/de/categories/) are built by Hugo even without a translation.
  • Restart harness serve after harness lang add/remove; it reads harness.yaml only at start. This is documented.
  • Renaming a source page rewrites link values in the source and its translations alike, so the source hash changes and translated elements that link to the page show a spurious "changed" mark.
  • Undo after a cross-page cut. Undoing a non-default-language step after a default-language cross-page cut restores only that language's file.

Review fixes (second round)

  • Translations need a configured language (B1). pagepath.SplitLang takes the site's languages, and every caller uses it, so a one-language site behaves as before.
  • The move plan is exact. move-refs takes an optional toParentUid. A language whose target translation lacks the destination parent is listed as dropped, for a same-page move into a container too.
  • The hash ignores defaults. It resolves a param the way the form does and leaves out default or empty values. Writing a default back, or adding an optional field to a sidecar, changes no hash.
  • Undo of "Translate page" removes the subpage translations it created.
  • Uid-less elements are flagged. harness check warns about a connected translation with elements that have no uid, because opening the page would delete them.
  • Addresses match Hugo. Segments are sanitized and lowercased unless disablePathToLower is set. A section slug applies on one-language sites too. The flip keeps an existing Aliases key.
  • Translation addresses stay unique and keep their redirects.
    • A slug that would give a translation its sibling's address is a 422 field error.
    • Deleting a slugged section translation adds aliases to the translations below it.
    • Translate strips aliases that its new addresses shadow.
  • Leaf bundle resources. Discarding the en page also discards a translated resource of its leaf bundle.
  • Self-closing parents. A self-closing container in a translation becomes paired tags when it receives translated children.
  • Minor fixes:
    • Link resolve answers by page.
    • A copy out of a connected translation drops l10nSource.
    • The Hugo guard checks hugo.binary first, as hugobin.Resolve does.
  • Docs. AGENTS.md, README and the setup-site, edit-content, migrate-site and verify-site skills now match the code (undo/discard, the tree address, the checks).

For #191 (theme): the language switch should use .Language.Label, not languageName. Reading backendLayout, weight, hidden and footer from the default-language page (PRD §4) has to be in that issue's scope.

Verification

  • go build ./..., go vet ./... and gofmt are clean, and go test ./... passes. That includes new table tests for siteconfig, pagepath, pagetree (trees and addresses per language), l10n (hash, repair, translate), langcmd, checkcmd and the server endpoints, plus the AGENTS.md and skills drift tests.
  • cd web && npm test passes (2418 tests); web/ is unchanged.
  • Single-language regression check. I diffed about 42 GET and 11 mutation answers between the base and this branch on a languages: [en] site. The only differences are the tree address field, the {code, label} entries of /api/site, and the new element delete-plan endpoint.
    • After review, I added the dotted-name case on a one-language site. node.js.md, asp.net.md, jquery.min.md, release.new.md, faq.en.md and notes.de.md are pages in the tree and open through /api/page. Rename and the flip alias them, they get pending rows of their own, and harness check is clean. Tests cover each of these.
    • With Hugo 0.167.0, the tree's address matches the build: /about-us/ for content/About Us/, /services/web/ under a section with slug: services, and /tools/node.js/. After harness lang add de, every old URL is an alias redirect, a hand-written Aliases: key included.
  • End to end: harness init, then harness lang add de --label Deutsch, translations made through the API with curl, then harness check (clean) and hugo 0.167.0.
    • The built public/ has /en/, /de/ and /de/leistungen/<child>/.
    • The root index.html redirects to /en/, and the old unprefixed addresses are alias redirect pages.
    • harness lang remove works in all three paths: blocked, --purge, and the default language refused.

🤖 Generated with Claude Code

Closes #190 ## What The engine and CLI side of multilingual sites: - **Config.** `harness.yaml` `languages` is a list of `{code, label}` entries; the first entry is the default. The legacy `languages: [en]` form still loads. `harness init` writes the new shape and pins Hugo **0.167.0**. - **Content model.** A translation is `content/<dir>/_index.<code>.md` (also `index.<code>.md` / `<name>.<code>.md`) beside its source; there is no `content/<code>/` directory. As in Hugo, only a **configured non-default language** makes a file a translation: `node.js.md`, `jquery.min.md` or `faq.en.md` stay ordinary pages. - Connected translations (`l10n: connected`) keep the source's uids in the same structure. The new `internal/l10n` package enforces this on write and repairs it on open. - Appearance-tab params are synced from the source. General params and bodies are translatable. - `l10nSource` holds a hash of the source's translatable values and drives the "changed since translated" marks. - **Addresses.** Once two languages exist, every language is served under `/<code>/`, with per-segment `slug` overrides from each ancestor's translation. Tree nodes get a new `address` field; `permalink` stays the language-neutral link value. - **API.** - Every lang-scoped endpoint resolves to the translation file. `GET /api/page` answers `missing: true` for an untranslated page. - `/api/tree?lang=` returns translated titles plus a `translation` state per node (translated / missing / outdated). - New endpoints: translate page (connected / free / empty, with subpages), translate-plan, translate element, translate all, disconnect, element delete-plan. - Structural element ops on a source page propagate into its connected translations. On a connected translation they answer 409. - Publishing is site-wide: the `lang` parameter of publish-all, pending and discard-all is ignored. Pending rows carry `lang`, and badges and undo/discard work per file. - The README section "Translations on the wire" documents every shape. - **`harness lang add|remove`.** - Writes both config files and the language's draft home page. - Refuses when the effective Hugo version is below 0.167. - `remove` refuses while translations exist unless `--purge`; the default language cannot be removed. - Re-running `lang add` for an existing language is a no-op with exit 0 (status `already-present`). - **`harness check`.** Reports a translation without a source (error), a connected translation whose structure has drifted or that has elements without a uid (warnings), a page named with the default language's code on a multilingual site (warning), and `hugo.toml` languages, default-language or subdir settings that disagree with `harness.yaml` (errors). - **Docs and skills.** - `scaffold/site/AGENTS.md`: new sections Languages, `harness lang`, Addresses, Translations, Language checks, and Translations by hand. - README: `harness lang`, Multilingual sites, and the wire shapes. - Skills updated: setup-site, migrate-site, verify-site, edit-content, add-element, add-page-type, create-feature. ## Where this differs from the PRD (checked on Hugo 0.167.0) - **`[languages.<code>] label`, not `languageName`.** `languageName` is deprecated since Hugo 0.158 and prints a warning on every build. The theme issue (#191) should read `.Language.Label`. - **Aliases are relative to the language root** once languages are in subdirectories: an alias `/x/` is emitted at `/en/x/`. So the engine writes aliases without a language prefix, and the one-time flip writes `/../services/`, which Hugo path-cleans to `/services/`. I confirmed in the build output that the old unprefixed URLs redirect. Home is never aliased: Hugo's own root redirect covers `/`, and an alias `/` is a build error. - **A root-relative `url` front matter is not language-prefixed by Hugo**, so the address uses it as written. - **Uppercase language subtags** (`pt-BR`) are published lowercase (`/pt-br/`), and the address follows that. - **Shortcodes inside a text body** (inline chips) are body content, not translated elements, so a translator can add or reorder them. - **A cut into a page whose translation is free** drops the translated element. This keeps free translations "never written by propagation". ## Known limits and follow-ups - **deckle-hosted** builds with its own nix-shell Hugo, whose pin is not changed here. A site with two languages may fail to verify there until that repository moves to Hugo 0.167 or newer. - **Theme (#191).** Hugo reads `type`, `weight`, layouts and page-type fields per file, while translations deliberately hold only title, slug, aliases, description, menuTitle, draft and l10n. The theme has to read those from the default-language page, as the PRD says. - Page-type fields such as a news `teaser` are therefore not translatable, which is the PRD's decision. - Taxonomy list pages (`/de/categories/`) are built by Hugo even without a translation. - **Restart `harness serve`** after `harness lang add/remove`; it reads `harness.yaml` only at start. This is documented. - **Renaming a source page** rewrites link values in the source and its translations alike, so the source hash changes and translated elements that link to the page show a spurious "changed" mark. - **Undo after a cross-page cut.** Undoing a non-default-language step after a default-language cross-page cut restores only that language's file. ## Review fixes (second round) - **Translations need a configured language (B1).** `pagepath.SplitLang` takes the site's languages, and every caller uses it, so a one-language site behaves as before. - **The move plan is exact.** `move-refs` takes an optional `toParentUid`. A language whose target translation lacks the destination parent is listed as `dropped`, for a same-page move into a container too. - **The hash ignores defaults.** It resolves a param the way the form does and leaves out default or empty values. Writing a default back, or adding an optional field to a sidecar, changes no hash. - **Undo of "Translate page"** removes the subpage translations it created. - **Uid-less elements are flagged.** `harness check` warns about a connected translation with elements that have no uid, because opening the page would delete them. - **Addresses match Hugo.** Segments are sanitized and lowercased unless `disablePathToLower` is set. A section `slug` applies on one-language sites too. The flip keeps an existing `Aliases` key. - **Translation addresses stay unique and keep their redirects.** - A slug that would give a translation its sibling's address is a 422 field error. - Deleting a slugged section translation adds aliases to the translations below it. - Translate strips aliases that its new addresses shadow. - **Leaf bundle resources.** Discarding the en page also discards a translated resource of its leaf bundle. - **Self-closing parents.** A self-closing container in a translation becomes paired tags when it receives translated children. - **Minor fixes:** - Link resolve answers by page. - A copy out of a connected translation drops `l10nSource`. - The Hugo guard checks `hugo.binary` first, as `hugobin.Resolve` does. - **Docs.** AGENTS.md, README and the setup-site, edit-content, migrate-site and verify-site skills now match the code (undo/discard, the tree `address`, the checks). **For #191 (theme):** the language switch should use `.Language.Label`, not `languageName`. Reading `backendLayout`, `weight`, `hidden` and `footer` from the default-language page (PRD §4) has to be in that issue's scope. ## Verification - `go build ./...`, `go vet ./...` and gofmt are clean, and `go test ./...` passes. That includes new table tests for siteconfig, pagepath, pagetree (trees and addresses per language), l10n (hash, repair, translate), langcmd, checkcmd and the server endpoints, plus the AGENTS.md and skills drift tests. - `cd web && npm test` passes (2418 tests); `web/` is unchanged. - **Single-language regression check.** I diffed about 42 GET and 11 mutation answers between the base and this branch on a `languages: [en]` site. The only differences are the tree `address` field, the `{code, label}` entries of `/api/site`, and the new element delete-plan endpoint. - After review, I added the dotted-name case on a one-language site. `node.js.md`, `asp.net.md`, `jquery.min.md`, `release.new.md`, `faq.en.md` and `notes.de.md` are pages in the tree and open through `/api/page`. Rename and the flip alias them, they get pending rows of their own, and `harness check` is clean. Tests cover each of these. - With Hugo 0.167.0, the tree's `address` matches the build: `/about-us/` for `content/About Us/`, `/services/web/` under a section with `slug: services`, and `/tools/node.js/`. After `harness lang add de`, every old URL is an alias redirect, a hand-written `Aliases:` key included. - **End to end:** `harness init`, then `harness lang add de --label Deutsch`, translations made through the API with curl, then `harness check` (clean) and `hugo` 0.167.0. - The built `public/` has `/en/`, `/de/` and `/de/leistungen/<child>/`. - The root `index.html` redirects to `/en/`, and the old unprefixed addresses are alias redirect pages. - `harness lang remove` works in all three paths: blocked, `--purge`, and the default language refused. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(l10n): multilingual sites — content model, translation API and harness lang
All checks were successful
ci / build-and-test (pull_request) Successful in 12m10s
4e7453664e
A site declares several languages in harness.yaml (`{code, label}` list,
the legacy string list still loads; the first entry is the default). A
translation is a file beside its default-language page (`_index.de.md`);
there is no per-language content directory.

- siteconfig: Languages become {Code, Label}; the "exactly one" rule goes.
- pagepath: SplitLang / TranslationPath; every default-language view
  (tree, pageops, pending, backend layouts, removecmd) ignores translation
  files, and plain pages carry their configured translations on
  move/rename/delete/convert.
- pagetree: per-language trees (translated titles, enabled, translation
  state translated/missing/outdated, l10n mode) and a new `address` field:
  `/<code>/` prefix once two languages exist, per-segment slug overrides
  from each ancestor's translation, root-relative `url` verbatim.
- l10n (new): connected-translation invariant and Repair, the l10nSource
  hash over translatable values, synced appearance params, translate
  element/document with the "[Translate to <label>:] " prefix, disconnect.
  `l10nSource` and `l10n` are reserved names.
- server: lang-scoped endpoints resolve to translation files; repair on
  open; change marks; source saves sync appearance params and propagate
  structure; insert/move/delete/duplicate/transfer propagate into
  connected translations and answer 409 on them; new endpoints translate
  page (connected/free/empty, subpages), translate-plan, translate
  element/all, disconnect, element delete-plan; delete translation,
  rename/enabled/properties per translation with alias rules; categories,
  module entries and link resolve per language; site-wide publish, pending
  rows with lang, per-file badges, per-file undo/discard.
- harness lang add/remove: both config files, the one-time
  defaultContentLanguageInSubdir flip with `/../<old>/` aliases (Hugo
  resolves aliases relative to the language root), the draft home
  translation, Hugo >= 0.167 guard, --purge.
- harness check: translation without source, unconfigured language
  suffix, connected structure drift, hugo.toml languages / default /
  subdir mismatches; `translations` count.
- initcmd: DefaultHugoVersion 0.167.0, new harness.yaml shape.
- Docs: AGENTS.md (Languages, Addresses, Translations, Language checks,
  Translations by hand), README (harness lang, Multilingual sites, wire
  shapes) and the setup-site, migrate-site, verify-site, edit-content,
  add-element, add-page-type, create-feature skills.

Closes #190

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

[autoland] verdict: reject

Lander verdict: FAIL

What I relied on

  • CI: ci / build-and-test passed (go vet, go test, go build, npm test with Hugo 0.167.0). I did not re-run it.
  • gofmt is clean on every changed Go file (checked locally).
  • Seven review passes, one per area, with repro tests and harness builds in scratch copies. Some ran real Hugo 0.167.0 builds.

Conventions are fine

  • The title follows Conventional Commits.
  • Closes #190 is present.
  • The diff stays within #190.

Blocking: acceptance criterion 1 is broken. That criterion is "a site with languages: [en] … behaves exactly as before: same addresses, same tree, same API answers".

B1. Page files with a dotted name stop being pages, even on a one-language site

pagepath.SplitLang (internal/pagepath/pagepath.go:60) treats any <stem>.<2–3 letters>.md as a translation and never checks the configured languages. Hugo only reads a configured language from a file name. With languages: [en], Hugo 0.167 builds /tools/node.js/, /jquery.min/ and /notes.faq/ as normal pages. Five of the seven review passes reproduced this independently, against origin/main:

  • Tree: internal/pagetree/build.go:198 drops node.js.md, asp.net.md, jquery.min.md, release.new.md and faq.en.md from GET /api/tree.
  • Page API: GET /api/page?path=tools/node.js.md now answers 400 ("…is "tools/node.md" in "js""). server.go:639/701 (refuseTranslationPath) refuses it, and so does pageops/paths.go:186/209.
  • Rename: a rename no longer writes the /tools/node.js/ alias. relocate.go:624 (affectedPages) builds its list from the tree and no longer sees the file, so the published URL 404s.
  • harness check: a site that passed on main now has 2 errors ("language "js" is not configured", "translation without a source"). The cause is checkcmd/check.go:417.
  • Language flip: harness lang add writes no alias for such pages (langcmd/content.go:72), so /jquery.min/ 404s after the flip.
  • Pending changes: pending.go:299-304 lists {path:"release.md", lang:"new"}, and the editor routes that row to an unknown-language 404. git.go:190/739 (badges/discard) and publish.go:473 key these files as translations.
  • PR body: its single-language regression check does not mention any of this.

The PR is also inconsistent with itself. pageops.pageTranslations and delete.go:277 already filter by langs[lang]. Fix: classify a file as a translation only when its tag is a configured non-default language, in every caller listed above. The check's "unconfigured language suffix" finding can still look at the name alone if you want it to. Add a one-language test with dotted file names. Then update the AGENTS.md "Translations" paragraph (around line 904) that currently documents the limitation.

Concerns to fix in the same run (each reproduced unless marked)

  1. The transfer plan reports a loss as "moved". In internal/server/translation_ops.go:363-398 (transferPlanFor), cutting into a container that is untranslated in the target's connected translation still lists the language under moved. Then receiveTranslated/l10n.Place (:267) drops the translated text. The doc comment admits it, but the confirmation the acceptance criteria rely on misreports the loss. A same-page move into an untranslated container also deletes the translated element (through Repair), and no plan or count mentions it.
  2. An appearance-only save marks translations "changed". In internal/l10n/hash.go:91-97,132-147, an absent param hashes as name=\x00. The form writes a default back, such as wide=false for an optional bool, so a save that changed only an appearance param flips the hash. Adding any optional general-tab field to a sidecar does the same: it flips the hash of every existing element of that type, site-wide. Fix: resolve a value the way the form does (stored, else default) and leave out params that have no value.
  3. Undo of "Translate page" with subpages misses the subpages. internal/server/translation_pages.go:110 snapshots only res.Page. Undo deletes posts/_index.de.md but leaves posts/first/index.de.md and posts/second/index.de.md.
  4. Opening a translation deletes hand-written elements without uids, and harness check says nothing. server/translation.go:193-206 (openPage) stamps fresh uids on a connected translation's uid-less elements. Repair then deletes them as strays. checkcmd/check.go:471 runs Repair without stamping first, so it reports "ok: no problems found". verify-site and edit-content lead an agent to read that as safe.
  5. The alias flip writes the raw path (langcmd/content.go:166, pagetree.RelativeAddress). content/About Us/_index.md gets /../About Us/ while Hugo published /about-us/, so the old URL 404s after the flip. Fix: apply Hugo's urlize and lowercasing, unless disablePathToLower is set.
  6. An existing capitalised Aliases: key gets a second, lowercase key (langcmd/content.go:177). Hugo keeps using Aliases, so the flip's aliases are lost.
  7. Translation addresses can collide or lose redirects:
    • translation_settings.go:110/245 checks only the format of a translation slug. Two sibling de translations can both claim /de/leistungen/hosting/.
    • translation.go:120-125: deleting a slugged section translation moves its descendants' addresses and writes no aliases.
    • pageops/translation.go:239/302: Translate never strips an alias that its new address shadows, which contradicts the package doc ("every URL-assigning operation strips…").
  8. A translated .md resource in a leaf bundle cannot be discarded per page. pending.go:293 counts posts/first/notes.de.md as a resource of the en page, but git.go:190 leaves it out of the en file set. Discarding the en page answers 200 and changes nothing.
  9. A self-closing container or items owner in a translation can never receive children (l10n/structure.go:301, targetList). InsertTranslated fails with "cannot be translated before its parent", although the parent is translated. TranslateAll adds 0, and Repair and Place drop children silently.
  10. The docs contradict the code:
    • AGENTS.md:990-992 and README.md:2557-2560 say undo/discard of a source page never touches its translations. README.md:940-956 and server/git.go:650-666,808-822 say and do otherwise.
    • README.md:976-977 says one-language answers are "exactly what they always were", but every tree node now carries address.
  11. Single-language Node.Address ignores a section's slug (pagetree/build.go:437). Hugo 0.167 serves /svc/web/ when services/_index.md has slug: svc, but the tree reports /services/web/. The flip's RelativeAddress uses the slug, so the two disagree about "the old address".

Minor:

  • The link-resolve fallback for an untranslated element on a translated page (linktargets.go:286-305) contradicts PRD §6, which resolves by page.
  • A copy out of a connected translation keeps l10nSource in a free target (element_transfer.go:260).
  • The Hugo version guard trusts hugo.version before hugo.binary (langcmd.go:380), the reverse of hugobin.Resolve.

For the human, outside this PR:

  • #191 still says the switch is labelled with languageName. This PR writes label instead, so #191 needs a note.
  • The theme reading backendLayout, weight, hidden and footer from the default-language page is in PRD §4, but #191's own scope does not list it. Until that lands, a published two-language site renders translations with the wrong layout and menus.
[autoland] verdict: reject ## Lander verdict: FAIL **What I relied on** - CI: `ci / build-and-test` passed (`go vet`, `go test`, `go build`, `npm test` with Hugo 0.167.0). I did not re-run it. - gofmt is clean on every changed Go file (checked locally). - Seven review passes, one per area, with repro tests and harness builds in scratch copies. Some ran real Hugo 0.167.0 builds. **Conventions are fine** - The title follows Conventional Commits. - `Closes #190` is present. - The diff stays within #190. **Blocking: acceptance criterion 1 is broken.** That criterion is "a site with `languages: [en]` … behaves exactly as before: same addresses, same tree, same API answers". ### B1. Page files with a dotted name stop being pages, even on a one-language site `pagepath.SplitLang` (`internal/pagepath/pagepath.go:60`) treats any `<stem>.<2–3 letters>.md` as a translation and never checks the configured languages. Hugo only reads a *configured* language from a file name. With `languages: [en]`, Hugo 0.167 builds `/tools/node.js/`, `/jquery.min/` and `/notes.faq/` as normal pages. Five of the seven review passes reproduced this independently, against `origin/main`: - **Tree:** `internal/pagetree/build.go:198` drops `node.js.md`, `asp.net.md`, `jquery.min.md`, `release.new.md` and `faq.en.md` from `GET /api/tree`. - **Page API:** `GET /api/page?path=tools/node.js.md` now answers 400 ("…is "tools/node.md" in "js""). `server.go:639/701` (`refuseTranslationPath`) refuses it, and so does `pageops/paths.go:186/209`. - **Rename:** a rename no longer writes the `/tools/node.js/` alias. `relocate.go:624` (`affectedPages`) builds its list from the tree and no longer sees the file, so the published URL 404s. - **`harness check`:** a site that passed on main now has 2 errors ("language "js" is not configured", "translation without a source"). The cause is `checkcmd/check.go:417`. - **Language flip:** `harness lang add` writes no alias for such pages (`langcmd/content.go:72`), so `/jquery.min/` 404s after the flip. - **Pending changes:** `pending.go:299-304` lists `{path:"release.md", lang:"new"}`, and the editor routes that row to an unknown-language 404. `git.go:190/739` (badges/discard) and `publish.go:473` key these files as translations. - **PR body:** its single-language regression check does not mention any of this. The PR is also inconsistent with itself. `pageops.pageTranslations` and `delete.go:277` already filter by `langs[lang]`. **Fix:** classify a file as a translation only when its tag is a configured non-default language, in every caller listed above. The check's "unconfigured language suffix" finding can still look at the name alone if you want it to. Add a one-language test with dotted file names. Then update the `AGENTS.md` "Translations" paragraph (around line 904) that currently documents the limitation. **Concerns to fix in the same run** (each reproduced unless marked) 1. **The transfer plan reports a loss as "moved".** In `internal/server/translation_ops.go:363-398` (`transferPlanFor`), cutting into a container that is untranslated in the target's connected translation still lists the language under `moved`. Then `receiveTranslated`/`l10n.Place` (`:267`) drops the translated text. The doc comment admits it, but the confirmation the acceptance criteria rely on misreports the loss. A same-page move into an untranslated container also deletes the translated element (through Repair), and no plan or count mentions it. 2. **An appearance-only save marks translations "changed".** In `internal/l10n/hash.go:91-97,132-147`, an absent param hashes as `name=\x00`. The form writes a default back, such as `wide=false` for an optional bool, so a save that changed only an appearance param flips the hash. Adding any optional general-tab field to a sidecar does the same: it flips the hash of every existing element of that type, site-wide. **Fix:** resolve a value the way the form does (stored, else default) and leave out params that have no value. 3. **Undo of "Translate page" with subpages misses the subpages.** `internal/server/translation_pages.go:110` snapshots only `res.Page`. Undo deletes `posts/_index.de.md` but leaves `posts/first/index.de.md` and `posts/second/index.de.md`. 4. **Opening a translation deletes hand-written elements without uids, and `harness check` says nothing.** `server/translation.go:193-206` (`openPage`) stamps fresh uids on a connected translation's uid-less elements. Repair then deletes them as strays. `checkcmd/check.go:471` runs Repair without stamping first, so it reports "ok: no problems found". `verify-site` and `edit-content` lead an agent to read that as safe. 5. **The alias flip writes the raw path** (`langcmd/content.go:166`, `pagetree.RelativeAddress`). `content/About Us/_index.md` gets `/../About Us/` while Hugo published `/about-us/`, so the old URL 404s after the flip. **Fix:** apply Hugo's urlize and lowercasing, unless `disablePathToLower` is set. 6. **An existing capitalised `Aliases:` key gets a second, lowercase key** (`langcmd/content.go:177`). Hugo keeps using `Aliases`, so the flip's aliases are lost. 7. **Translation addresses can collide or lose redirects:** - `translation_settings.go:110/245` checks only the format of a translation slug. Two sibling de translations can both claim `/de/leistungen/hosting/`. - `translation.go:120-125`: deleting a slugged section translation moves its descendants' addresses and writes no aliases. - `pageops/translation.go:239/302`: Translate never strips an alias that its new address shadows, which contradicts the package doc ("every URL-assigning operation strips…"). 8. **A translated `.md` resource in a leaf bundle cannot be discarded per page.** `pending.go:293` counts `posts/first/notes.de.md` as a resource of the en page, but `git.go:190` leaves it out of the en file set. Discarding the en page answers 200 and changes nothing. 9. **A self-closing container or items owner in a translation can never receive children** (`l10n/structure.go:301`, `targetList`). `InsertTranslated` fails with "cannot be translated before its parent", although the parent is translated. `TranslateAll` adds 0, and Repair and Place drop children silently. 10. **The docs contradict the code:** - `AGENTS.md:990-992` and `README.md:2557-2560` say undo/discard of a source page never touches its translations. `README.md:940-956` and `server/git.go:650-666,808-822` say and do otherwise. - `README.md:976-977` says one-language answers are "exactly what they always were", but every tree node now carries `address`. 11. **Single-language `Node.Address` ignores a section's `slug`** (`pagetree/build.go:437`). Hugo 0.167 serves `/svc/web/` when `services/_index.md` has `slug: svc`, but the tree reports `/services/web/`. The flip's `RelativeAddress` uses the slug, so the two disagree about "the old address". **Minor:** - The link-resolve fallback for an untranslated element on a translated page (`linktargets.go:286-305`) contradicts PRD §6, which resolves by page. - A copy out of a connected translation keeps `l10nSource` in a free target (`element_transfer.go:260`). - The Hugo version guard trusts `hugo.version` before `hugo.binary` (`langcmd.go:380`), the reverse of `hugobin.Resolve`. **For the human, outside this PR:** - #191 still says the switch is labelled with `languageName`. This PR writes `label` instead, so #191 needs a note. - The theme reading `backendLayout`, `weight`, `hidden` and `footer` from the default-language page is in PRD §4, but #191's own scope does not list it. Until that lands, a published two-language site renders translations with the wrong layout and menus.
fix(l10n): classify translations by configured language and resolve review findings
All checks were successful
ci / build-and-test (pull_request) Successful in 12m39s
d1dd32413a
Address the rejection review of #193.

B1: a file is a translation only when the code before ".md" is a
configured non-default language, the rule Hugo applies. pagepath.SplitLang
now takes the site's languages (pagepath.Langs); SplitLangTag keeps the
name-only reading for the few callers that already know the language. The
page tree, the page API, rename/move aliases, the alias flip, pending rows,
badges, discard and the publish link check all use it, so "node.js.md",
"jquery.min.md", "release.new.md" and "faq.en.md" are pages on a
one-language site exactly as before. harness check no longer errors on
such names; a default-language tag on a multilingual site is a warning.

Also:
- The transfer plan (move-refs) takes toParentUid and reports a language
  whose target translation lacks the destination parent as dropped, for
  same-page moves into a container too.
- The l10nSource hash resolves a param as the form does (stored, else
  default, else empty) and leaves default-or-empty params out, so writing
  a default back or adding an optional field changes no hash.
- Undo of "Translate page" removes the subpage translations it created.
- harness check warns about a connected translation with uid-less elements,
  which opening it would delete.
- Addresses: segments are sanitized and lowercased as Hugo publishes them
  (unless disablePathToLower), a section slug applies on a one-language
  site too, and the flip keeps a hand-written "Aliases" key.
- A translation slug that would take a sibling translation's address is a
  field error; deleting a slugged section translation aliases the
  translations below it; Translate strips aliases its new addresses shadow.
- A leaf bundle's resource translation is discarded with its page.
- A self-closing container or items owner in a translation receives
  translated children (it becomes paired tags).
- Link resolve answers by page for an untranslated element on a translated
  page; a copy out of a connected translation drops l10nSource; the Hugo
  guard checks hugo.binary before hugo.version, as hugobin.Resolve does.
- AGENTS.md, README.md and the setup-site, edit-content, migrate-site and
  verify-site skills follow the code (undo/discard, tree address, checks).

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

[autoland] verdict: fix-done

[autoland] verdict: fix-done
Author
Owner

[autoland] verdict: pass

Lander verdict: CONCERNS (non-blocking), so approved but not merged

What I relied on

  • CI: ci / build-and-test on d1dd324 passed in 12m39s (go vet, go test, go build, npm test, with Hugo 0.167.0). I did not re-run it. A local go vet ./... and go test ./... also pass.
  • gofmt is clean on every changed Go file.
  • Four re-review passes of the fix commit, one per area. They wrote repro tests in scratch copies, ran differential runs against origin/main, and built sites with real Hugo 0.167.0 before and after harness lang add.

Conventions are fine

  • The title follows Conventional Commits.
  • Closes #190 is present.
  • The base is current, so there is no conflict.
  • The diff stays within #190. The backendlayout change in d1dd324 only reverts 4e74536's change: its net diff is empty, and every caller passes the source path.

The previous rejection is resolved

  • B1 is fixed. Every caller now uses pagepath.SplitLang(rel, TranslationLangs(...)). SplitLangTag is used only where the language is already known to be configured.
    • Acceptance criterion 1 holds. On a languages: [en] site with node.js.md, faq.en.md, jquery.min.md, a slugged section, paths with spaces and url: pages, base and head were run through the same scenarios. The tree, page, refs, link resolve, delete-plan, pending, badges, discard, rename and delete all gave the same results, and the resulting file bytes were identical. The only differences are the new tree address field and the {code,label} entries in /api/site that the issue specifies.
    • On en+de, node.js.md and faq.en.md stay pages and x.de.md is a translation, consistently across the tree, API, pending, badges, discard, rename, delete, check, the flip and purge.
  • Fixed and confirmed by repro: 3, 4, 6, 7b, 7c, 8, 9, 10, 11 and the three minor items. Items 1, 2, 5 and 7a are partly fixed; see below.

Concerns (worth a look; none is a blocker. Most can be follow-ups)

  1. New regression: undo of "Translate page" leaves stripped aliases gone. Translate now strips shadowed aliases from other existing translations (internal/pageops/translation.go:318, translateStrips at :361). The undo snapshot names only res.Created (internal/server/translation_pages.go:115), so undo does not restore those files.
    • Repro: posts/first/index.de.md carries the alias /posts/second/, and translating posts/second strips it. After undo, posts/second/index.de.md is gone and so is the alias.
  2. Translation address collisions are only partly prevented (7a). translationAddressTaken (internal/pageops/translation_settings.go:292-318) compares only against siblings that already have a translation in that language. Grouping folders are skipped (s.Path == "", :314). All of these were reproduced, and Hugo 0.167 silently builds one page over the other:
    • Set a slug equal to an untranslated sibling's folder name, then translate that sibling. Translate (translation.go:270) has no collision check.
    • A slug equal to an untranslated sibling section makes its translated children collide.
    • Children of grouping-folder siblings collide.
  3. The flip's aliases still miss some Hugo URLs (5). PathRules.Segment (internal/pagetree/hugopath.go:30) sanitizes but skips Hugo's NormalizePathStringBasic, which first lowercases the segment and turns every single space into -.
    • So a b.md and C++ & Go!.md are published at /a--b/ and /c++--go/. The flip aliases /a-b/ and /c++-go/, and the old URLs 404 after lang add. This was confirmed with Hugo 0.167.0.
    • Also not handled: removePathAccents, and a per-language [languages.<code>] disablePathToLower (ReadPathRules reads only the root key).
  4. Section slug versus stored links under the new Hugo pin.
    • Hugo 0.167 applies a section's slug to its subtree: /dienste/web/, confirmed against 0.163.3, which publishes /services/web/.
    • The engine's link values and rename aliases still use the unslugged path (pageURL, internal/server/relocate.go:442,505). Link resolve matches only that form (linktargets.go:258). The comment at internal/pagetree/permalink.go:21-23 still says Hugo ignores a section slug.
    • Rendering link values is #191's job (PRD §6). But until that lands, a new site that sets a section slug renders links that 404, and a rename records an alias that was never published. Please make sure #191 covers this, and fix the stale comment.
  5. The transfer plan is only exact when the client sends toParentUid (1). Without it (translation_ops.go:386-407), a cut into a container that a connected translation lacks still lists that language as moved. The current editor (web/src/api/links.ts:136-150) sends no toParentUid and ignores translations. #192 has to send it.
  6. The hash residual (2). Inline chips in rte/markdown bodies are hashed verbatim (internal/l10n/hash.go:178). /api/form/inline writes defaults back, so pressing OK on a chip dialog without changing anything still marks the owner's translations "changed".
  7. A skill gives a misleading instruction. scaffold/site/.claude/skills/verify-site/SKILL.md:30-32 says to fix a language-config error by redoing it with harness lang add/remove.
    • When the code was hand-added to harness.yaml, lang add returns already-present and changes nothing.
    • When hugo.toml disagrees, lang add refuses.
    • AGENTS.md:822 itself says to fix it by hand first.

Minor doc and comment drift (no action required)

  • README.md:999 and AGENTS.md:912: Hugo builds faq.en.md at /faq/ even on a one-language site, as the default-language file. The "exactly as Hugo builds them" claim does not hold for a default-language tag.
  • Stale comments:
    • internal/pagepath/pagepath.go:89 says only harness check uses SplitLangTag; there are five other callers.
    • internal/server/publish.go:379 describes a branch that can no longer be reached, at :473.
  • AGENTS.md:86-88 says the scaffold's defaultContentLanguageInSubdir appears only once there is a second language; the scaffold writes = false from the start.
  • README.md:993-995 says one-language answers gain only address. /api/site languages changed shape too.
  • add-page-type/SKILL.md:36-40 lists what a translation owns but leaves out draft, aliases and l10n.
  • edit-content/SKILL.md:174 gives only the _index.<code>.md form.

For the human: each concern is narrow and none breaks the one-language guarantee or the core translation flows, so I did not reject. I approved instead of merging so that someone looks at concerns 1–3 first: fix them before the merge or file them as follow-ups. Concerns 4–5 are for #191 and #192.

[autoland] verdict: pass ## Lander verdict: CONCERNS (non-blocking), so approved but not merged **What I relied on** - CI: `ci / build-and-test` on d1dd324 passed in 12m39s (`go vet`, `go test`, `go build`, `npm test`, with Hugo 0.167.0). I did not re-run it. A local `go vet ./...` and `go test ./...` also pass. - gofmt is clean on every changed Go file. - Four re-review passes of the fix commit, one per area. They wrote repro tests in scratch copies, ran differential runs against `origin/main`, and built sites with real Hugo 0.167.0 before and after `harness lang add`. **Conventions are fine** - The title follows Conventional Commits. - `Closes #190` is present. - The base is current, so there is no conflict. - The diff stays within #190. The `backendlayout` change in d1dd324 only reverts 4e74536's change: its net diff is empty, and every caller passes the source path. **The previous rejection is resolved** - **B1 is fixed.** Every caller now uses `pagepath.SplitLang(rel, TranslationLangs(...))`. `SplitLangTag` is used only where the language is already known to be configured. - **Acceptance criterion 1 holds.** On a `languages: [en]` site with `node.js.md`, `faq.en.md`, `jquery.min.md`, a slugged section, paths with spaces and `url:` pages, base and head were run through the same scenarios. The tree, page, refs, link resolve, delete-plan, pending, badges, discard, rename and delete all gave the same results, and the resulting file bytes were identical. The only differences are the new tree `address` field and the `{code,label}` entries in `/api/site` that the issue specifies. - **On en+de,** `node.js.md` and `faq.en.md` stay pages and `x.de.md` is a translation, consistently across the tree, API, pending, badges, discard, rename, delete, check, the flip and purge. - **Fixed and confirmed by repro:** 3, 4, 6, 7b, 7c, 8, 9, 10, 11 and the three minor items. Items 1, 2, 5 and 7a are partly fixed; see below. **Concerns** (worth a look; none is a blocker. Most can be follow-ups) 1. **New regression: undo of "Translate page" leaves stripped aliases gone.** Translate now strips shadowed aliases from *other* existing translations (`internal/pageops/translation.go:318`, `translateStrips` at `:361`). The undo snapshot names only `res.Created` (`internal/server/translation_pages.go:115`), so undo does not restore those files. - Repro: `posts/first/index.de.md` carries the alias `/posts/second/`, and translating `posts/second` strips it. After undo, `posts/second/index.de.md` is gone and so is the alias. 2. **Translation address collisions are only partly prevented (7a).** `translationAddressTaken` (`internal/pageops/translation_settings.go:292-318`) compares only against siblings that already have a translation in that language. Grouping folders are skipped (`s.Path == ""`, `:314`). All of these were reproduced, and Hugo 0.167 silently builds one page over the other: - Set a slug equal to an untranslated sibling's folder name, then translate that sibling. `Translate` (`translation.go:270`) has no collision check. - A slug equal to an untranslated sibling *section* makes its translated children collide. - Children of grouping-folder siblings collide. 3. **The flip's aliases still miss some Hugo URLs (5).** `PathRules.Segment` (`internal/pagetree/hugopath.go:30`) sanitizes but skips Hugo's `NormalizePathStringBasic`, which first lowercases the segment and turns every single space into `-`. - So `a b.md` and `C++ & Go!.md` are published at `/a--b/` and `/c++--go/`. The flip aliases `/a-b/` and `/c++-go/`, and the old URLs 404 after `lang add`. This was confirmed with Hugo 0.167.0. - Also not handled: `removePathAccents`, and a per-language `[languages.<code>] disablePathToLower` (`ReadPathRules` reads only the root key). 4. **Section `slug` versus stored links under the new Hugo pin.** - Hugo 0.167 applies a section's `slug` to its subtree: `/dienste/web/`, confirmed against 0.163.3, which publishes `/services/web/`. - The engine's link values and rename aliases still use the unslugged path (`pageURL`, `internal/server/relocate.go:442,505`). Link resolve matches only that form (`linktargets.go:258`). The comment at `internal/pagetree/permalink.go:21-23` still says Hugo ignores a section slug. - Rendering link values is #191's job (PRD §6). But until that lands, a new site that sets a section slug renders links that 404, and a rename records an alias that was never published. Please make sure #191 covers this, and fix the stale comment. 5. **The transfer plan is only exact when the client sends `toParentUid` (1).** Without it (`translation_ops.go:386-407`), a cut into a container that a connected translation lacks still lists that language as `moved`. The current editor (`web/src/api/links.ts:136-150`) sends no `toParentUid` and ignores `translations`. #192 has to send it. 6. **The hash residual (2).** Inline chips in rte/markdown bodies are hashed verbatim (`internal/l10n/hash.go:178`). `/api/form/inline` writes defaults back, so pressing OK on a chip dialog without changing anything still marks the owner's translations "changed". 7. **A skill gives a misleading instruction.** `scaffold/site/.claude/skills/verify-site/SKILL.md:30-32` says to fix a language-config error by redoing it with `harness lang add`/`remove`. - When the code was hand-added to `harness.yaml`, `lang add` returns `already-present` and changes nothing. - When `hugo.toml` disagrees, `lang add` refuses. - `AGENTS.md:822` itself says to fix it by hand first. **Minor doc and comment drift** (no action required) - `README.md:999` and `AGENTS.md:912`: Hugo builds `faq.en.md` at `/faq/` even on a one-language site, as the default-language file. The "exactly as Hugo builds them" claim does not hold for a default-language tag. - Stale comments: - `internal/pagepath/pagepath.go:89` says only `harness check` uses `SplitLangTag`; there are five other callers. - `internal/server/publish.go:379` describes a branch that can no longer be reached, at `:473`. - `AGENTS.md:86-88` says the scaffold's `defaultContentLanguageInSubdir` appears only once there is a second language; the scaffold writes `= false` from the start. - `README.md:993-995` says one-language answers gain only `address`. `/api/site` `languages` changed shape too. - `add-page-type/SKILL.md:36-40` lists what a translation owns but leaves out `draft`, `aliases` and `l10n`. - `edit-content/SKILL.md:174` gives only the `_index.<code>.md` form. **For the human:** each concern is narrow and none breaks the one-language guarantee or the core translation flows, so I did not reject. I approved instead of merging so that someone looks at concerns 1–3 first: fix them before the merge or file them as follow-ups. Concerns 4–5 are for #191 and #192.
feat(l10n): drop every engine-written alias and resolve the review concerns
All checks were successful
ci / build-and-test (pull_request) Successful in 13m13s
e4d8138ca6
The engine no longer writes `aliases` anywhere: a rename or move, a
translation's slug change, the deletion of a slugged section translation,
Translate and the one-time flip of `harness lang add` change addresses
without recording the old ones, and a hand-written `aliases` key is left
exactly as it is (the shadowing guard that stripped entries from other
pages is gone with it). `harness lang add` touches no content any more; its
result and JSON lose the `aliases` count. Issue #194 records the decision.

With the alias machinery gone, the remaining concerns of the second lander
verdict on #193 are resolved:

- Addresses match Hugo 0.167.0: a file-path segment is lowercased with each
  single space turned into a hyphen before sanitizing ("a  b" is "/a--b/"),
  unless disablePathToLower; a slug is sanitized and lowercased;
  removePathAccents is honoured; both keys are read per
  `[languages.<code>]` table too (pagetree.PathRules.File / .Slug,
  ReadPathRules(siteDir, lang)). Every expectation was checked against a
  real Hugo build.
- Address collisions: a translation's slug is refused when a sibling
  already has that address in the language — a translated sibling's slug,
  an untranslated sibling's source segment, a grouping directory's name —
  and Translate refuses (skips, for a subpage) a page whose segment a
  sibling's translation slug already took. `harness check` reports two
  pages at one address in a language as an error.
- The verify-site skill no longer sends an agent to `harness lang add` for
  a language-config error (a no-op or a refusal there); it says to fix the
  file the error names.
- Stale comments: Permalink deliberately ignores a section slug (it is the
  stored-link form), SplitLangTag's callers, publish's dotted-name branch;
  AGENTS.md's hugo.toml and faq.en.md notes.

Docs and skills (AGENTS.md "Addresses", "harness lang", "Language checks",
"Editing content by hand", "Translations by hand"; the README; edit-content,
migrate-site, setup-site, verify-site) say what the engine does now.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Author
Owner

Third round: no engine-written aliases, remaining concerns resolved (e4d8138)

Per the product decision recorded in #194, commit e4d8138 removes every alias the engine wrote, then fixes what was left of the second verdict's concerns.

Aliases are gone. Rename, move, a translation's slug change, the deletion of a slugged section translation, Translate and the harness lang add flip no longer record old addresses, and the shadowing guard that stripped entries from other pages is removed with them. A hand-written aliases key is left byte for byte (tests cover rename, Translate and the flip). harness lang add touches no content; its result and --json lose aliases. This dissolves concern 1 (undo of stripped aliases) and the rename half of concern 4.

Concern 3, addresses. PathRules now has File and Slug: a file-path segment is lowercased with each single space turned into a hyphen before sanitizing (a b.md → /a--b/, C++ & Go!.md → /c++--go/), unless disablePathToLower; a slug is sanitized and lowercased; removePathAccents is honoured; both keys are read per [languages.<code>] table. Every expectation in the tests was checked against a Hugo 0.167.0 build.

Concern 2, collisions. The slug check now treats every sibling as reserving its address in the language: a translated sibling its slug, an untranslated one its source segment, a grouping directory its name. Translate refuses a page whose segment a sibling's translation slug already took (a subpage in that state is skipped). harness check reports two pages at one address in a language as an error; verified end to end against a Hugo build that collapsed the two pages.

Concern 7. The verify-site skill tells the agent to fix the file the error names, not to rerun harness lang add.

Comments and docs. Permalink's section-slug note now explains it is the stored-link form; SplitLangTag's caller note, publish's dotted-name note, AGENTS.md's hugo.toml and faq.en.md notes are corrected. AGENTS.md, the README and the edit-content, migrate-site, setup-site and verify-site skills describe the no-alias behaviour.

Still for other issues. Concern 5 (the editor sending toParentUid) is #192's; concern 6 (inline chips in the body hash) is a follow-up; #191 must resolve link values by content path.

go build, go vet, gofmt and go test ./... are clean, including the AGENTS.md and skills drift tests. web/ changed in two comments only.

## Third round: no engine-written aliases, remaining concerns resolved (e4d8138) Per the product decision recorded in #194, commit e4d8138 removes every alias the engine wrote, then fixes what was left of the second verdict's concerns. **Aliases are gone.** Rename, move, a translation's slug change, the deletion of a slugged section translation, Translate and the `harness lang add` flip no longer record old addresses, and the shadowing guard that stripped entries from other pages is removed with them. A hand-written `aliases` key is left byte for byte (tests cover rename, Translate and the flip). `harness lang add` touches no content; its result and `--json` lose `aliases`. This dissolves concern 1 (undo of stripped aliases) and the rename half of concern 4. **Concern 3, addresses.** `PathRules` now has `File` and `Slug`: a file-path segment is lowercased with each single space turned into a hyphen before sanitizing (`a b.md` → `/a--b/`, `C++ & Go!.md` → `/c++--go/`), unless `disablePathToLower`; a slug is sanitized and lowercased; `removePathAccents` is honoured; both keys are read per `[languages.<code>]` table. Every expectation in the tests was checked against a Hugo 0.167.0 build. **Concern 2, collisions.** The slug check now treats every sibling as reserving its address in the language: a translated sibling its slug, an untranslated one its source segment, a grouping directory its name. Translate refuses a page whose segment a sibling's translation slug already took (a subpage in that state is skipped). `harness check` reports two pages at one address in a language as an error; verified end to end against a Hugo build that collapsed the two pages. **Concern 7.** The verify-site skill tells the agent to fix the file the error names, not to rerun `harness lang add`. **Comments and docs.** Permalink's section-slug note now explains it is the stored-link form; `SplitLangTag`'s caller note, publish's dotted-name note, AGENTS.md's `hugo.toml` and `faq.en.md` notes are corrected. AGENTS.md, the README and the edit-content, migrate-site, setup-site and verify-site skills describe the no-alias behaviour. **Still for other issues.** Concern 5 (the editor sending `toParentUid`) is #192's; concern 6 (inline chips in the body hash) is a follow-up; #191 must resolve link values by content path. `go build`, `go vet`, gofmt and `go test ./...` are clean, including the AGENTS.md and skills drift tests. `web/` changed in two comments only.
Author
Owner

[autoland] verdict: pass

This was generated by AI while landing a PR.

land-pr verdict: PASS with CONCERNS (non-blocking), approved, awaiting confirm

Signal relied on. ci / build-and-test on e4d8138 (go vet, go test, go build, npm test with Hugo 0.167.0) passed in 13m13s; not re-run. The first two commits had eleven autoland review passes (see the two lander verdicts above); I read the full diff of e4d8138, the only commit no reviewer had seen. Base is current with main, no conflict.

Conventions. Conventional Commits title; Closes #190 present; head is afk/190.

Concerns for the human, before merging

  1. Scope: the no-alias change diverges from #190's acceptance criteria. #190 requires the lang-add flip to write every page's old address into aliases, slug changes and source renames to alias translations. e4d8138 removes all of that, citing #194, which records the decision but says it "needs a proper design before anyone implements it"; the AFK run then closed #194 as implemented. It also removes behaviour that existed on main before this PR: rename and move no longer write the page's old URL as an alias, and create no longer strips shadowing aliases, on one-language sites too. So acceptance criterion 1 ("behaves exactly as before") now holds for reads but not for rename/move writes. Confirm this is wanted in this PR rather than as a follow-up.
  2. Stale claim in code docs. internal/linkrefs/doc.go:53-55 and internal/linkrefs/linkrefs.go:450 now say a link that was not rewritten "is reported by harness check as dangling". internal/checkcmd has no link check; with aliases gone there is no safety net for a skipped rewrite. Fix the comment or add the check (follow-up).
  3. Carried over from the second lander verdict, for other issues: #192 must send toParentUid so the transfer plan is exact; inline chips in the body hash still mark translations changed on a no-op chip save (follow-up); #191 must read .Language.Label and resolve link values by content path.

Everything else from the second verdict (concerns 1, 2, 3, 7 and the comment drift) is resolved in e4d8138 and covered by tests I read (collision with untranslated siblings and grouping directories, harness check address errors, PathRules File/Slug with per-language keys, hand-written aliases left byte for byte).

[autoland] verdict: pass > *This was generated by AI while landing a PR.* ## land-pr verdict: PASS with CONCERNS (non-blocking), approved, awaiting confirm **Signal relied on.** `ci / build-and-test` on e4d8138 (go vet, go test, go build, npm test with Hugo 0.167.0) passed in 13m13s; not re-run. The first two commits had eleven autoland review passes (see the two lander verdicts above); I read the full diff of e4d8138, the only commit no reviewer had seen. Base is current with main, no conflict. **Conventions.** Conventional Commits title; `Closes #190` present; head is `afk/190`. **Concerns for the human, before merging** 1. **Scope: the no-alias change diverges from #190's acceptance criteria.** #190 requires the lang-add flip to write every page's old address into `aliases`, slug changes and source renames to alias translations. e4d8138 removes all of that, citing #194, which records the decision but says it "needs a proper design before anyone implements it"; the AFK run then closed #194 as implemented. It also removes behaviour that existed on main before this PR: rename and move no longer write the page's old URL as an alias, and create no longer strips shadowing aliases, on one-language sites too. So acceptance criterion 1 ("behaves exactly as before") now holds for reads but not for rename/move writes. Confirm this is wanted in this PR rather than as a follow-up. 2. **Stale claim in code docs.** `internal/linkrefs/doc.go:53-55` and `internal/linkrefs/linkrefs.go:450` now say a link that was not rewritten "is reported by `harness check` as dangling". `internal/checkcmd` has no link check; with aliases gone there is no safety net for a skipped rewrite. Fix the comment or add the check (follow-up). 3. **Carried over from the second lander verdict, for other issues:** #192 must send `toParentUid` so the transfer plan is exact; inline chips in the body hash still mark translations changed on a no-op chip save (follow-up); #191 must read `.Language.Label` and resolve link values by content path. Everything else from the second verdict (concerns 1, 2, 3, 7 and the comment drift) is resolved in e4d8138 and covered by tests I read (collision with untranslated siblings and grouping directories, `harness check` address errors, PathRules File/Slug with per-language keys, hand-written aliases left byte for byte).
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!193
No description provided.