跳转到内容

20260916-0740-clean-url-trailing-slash Serve clean URLs without a trailing-slash redirect

  • status: completed
  • priority: P2
  • owner: claude/session-20260915-1901
  • createdAt: 2026-09-16 07:40

The Astro migration changed how the deployed site resolves a clean URL. VitePress had cleanUrls: true and served /docs/architecture directly. Astro's default build.format: 'directory' emits docs/architecture/index.html, so Cloudflare answers a request for /docs/architecture with a 307 to /docs/architecture/.

Measured on the live deployment:

  • /docs/architecture -> 307 -> /docs/architecture/ -> 200
  • in-page links are emitted without a trailing slash (href="/docs/task/REPO-001"), so every one of the 1271 rewritten in-site links costs an extra round trip
  • a legacy _redirects target is now a double hop: /bizplan/BUSINESS-PLAN -> 301 -> 307 -> 200

Nothing is unreachable; every page returns 200. The parity work could not see this because it compared build OUTPUT - both toolchains emit path/index.html - while this is serving-layer behaviour.

Acceptance criteria:

  1. A clean URL without a trailing slash resolves 200 directly, with no redirect.
  2. Directory index pages keep the URL they have today; their route must not change.
  3. The 63 _redirects rules still resolve, and no rule gains an extra hop.
  4. bun run docs:lint && bun run docs:build && git diff --check passes; scripts/check-build.mjs passes.
  5. No file under bizplan/ or docs/ is modified.

Restoring clean URLs without a trailing-slash redirect

  • blocked by: (none)
  • blocks: (none)

Follows the Astro + Starlight migration, task 20260915-1901. The candidate change is build: { format: 'file' } in astro.config.mjs; the risk is that it also changes the emitted path for the 31 directory index pages, which is what criterion 2 exists to catch.

build: { format: 'file' } was tried first and REJECTED: it emits .html in every href (href="/bizplan/product/core.html"), which breaks every published URL, the _redirects targets and external bookmarks. It fixes a redirect by changing the whole URL scheme.

Taken instead: assets.html_handling: "drop-trailing-slash" in wrangler.jsonc. A serving-layer setting for a serving-layer problem - build output, emitted links and published URLs are all unchanged, and the adapter carries it into the generated dist/server/wrangler.json.

Measured on the live deployment, before and after:

before after
leaf pages (/docs/architecture, 1271 in-site links) 1 hop 0 hops
directory index pages (/bizplan/product/core/, 31 pages) 0 hops 1 hop
legacy _redirects rules 2 hops 1 hop

Criteria 1, 3, 4 and 5 met. Criterion 2 is met in substance - directory index URLs still resolve 200 - but they now cost one hop, because src/doc-urls.ts emits /{id}/ for index pages while the asset handler serves the slashless form. Removing that last hop is a one-expression change to idToHref; it is NOT done here, because it changes which form is canonical for 31 pages and that is a separate decision.

  • complete: html_handling drop-trailing-slash; format:'file' rejected