Skip to content

Add a trailing-slash policy that canonicalizes with a redirect #255

Description

@vivek7405

Problem

Because route patterns end in /?$ (router.js buildRoutePattern:237), /about and /about/ both serve a 200 with identical content and there is no canonical redirect to one form. This is duplicate-content: search engines index both URLs, link-equity splits, analytics double-counts, and CDN cache keys split on the slash, halving hit rate. There is no config to pick a policy and no redirect to enforce it.

Design / approach

In handle(), before matchPage, normalize the native URL pathname per a webjs.trailingSlash setting and return a literal 308 to the canonical form (never the root). Pure URL string handling and a standard redirect.

Web-standards fit: Operates on the native URL object and emits a standard 308; the policy is a tiny string check, no router rewrite engine.

Prior art: Next.js trailingSlash:false defaults and 308-redirects; SvelteKit trailingSlash route option; Rails strips by convention.

Acceptance criteria

  • A package.json webjs.trailingSlash setting (default 'never') controls canonicalization
  • With 'never', GET /about/ returns 308 to /about (and 'always' does the inverse), preserving query and method semantics
  • The root path / is never redirected to empty
  • 'ignore' preserves the current both-forms-200 behaviour for back-compat
  • Applies in dev.js and the prod handler before SSR and the 404 fallback
  • Unit + e2e network-probe: canonical form 200, non-canonical form a single 308 (no loop)
  • Tests cover the new behaviour at the applicable layer(s)
  • Docs / AGENTS.md / CONVENTIONS.md updated if the public surface changed

Filed from the production-readiness audit (webjs vs Next.js / Remix / Rails / Turbo / Lit). Theme: routing. Priority: P1. Kept to webjs identity: no-build, progressive enhancement, web-components-first, AI-first, batteries-included, close to web standards.

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type

Projects

Status
Done

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions