Skip to content

Conversation

@atscott
Copy link
Contributor

@atscott atscott commented Dec 29, 2025

This commit introduces a highly requested trailingSlash configuration option to the Angular Router, allowing developers to control how trailing slashes are handled in their applications. The options are:

  • 'always': Enforces a trailing slash on all URLs.
  • 'never': Removes trailing slashes from all URLs (default).
  • 'preserve': Respects the presence or absence of a trailing slash as defined in the UrlTree.

Additionally, this commit updates the defaultUrlMatcher to optionally consume a single trailing empty path segment if the route otherwise matches. This ensures that routes with pathMatch: 'full' can correctly match URLs with trailing slashes (e.g., /a/ matching {path: 'a', pathMatch: 'full'}), resolving a long-standing inconsistency where such routes would fail despite pathMatch: 'prefix' variants (with empty children) succeeding.

Alternatives Considered & Design Rationale:

  • UrlTree.hasTrailingSlash Property: We initially explored adding a explicit boolean property to UrlTree. However, this would have required a significant public API change and breaking updates to custom UrlSerializer implementations to ensure the state was preserved. It also complicated the mental model of the UrlTree as a direct representation of the URL structure. We settled on representing trailing slashes as a trailing empty UrlSegment, which leverages the existing structure and serialization logic without API expansion.

    Why we chose the current approach:
    We decided on a "Permissive Matcher, Strict Serializer/Location" pattern.

    • The Matcher is permissive: it allows consuming a trailing empty segment if it's the only thing left, treating it effectively as optional for pathMatch: 'full'. This aligns logic with how pathMatch: 'prefix' matches empty children.
    • The Serializer/Location are strict: The trailingSlash configuration is verified at the boundaries (serialization and location normalization). If trailingSlash: 'never' is set, the slash is stripped before matching ever occurs. If matching sees a slash, it implies the configuration allowed it (or we are in a permissive mode), so the matcher should handle it gracefully.

fixes #16051

This commit introduces a highly requested `trailingSlash` configuration option to the Angular Router, allowing developers to control how trailing slashes are handled in their applications. The options are:
- 'always': Enforces a trailing slash on all URLs.
- 'never': Removes trailing slashes from all URLs (default).
- 'preserve': Respects the presence or absence of a trailing slash as defined in the UrlTree.

Additionally, this commit updates the `defaultUrlMatcher` to optionally consume a single trailing empty path segment if the route otherwise matches. This ensures that routes with `pathMatch: 'full'` can correctly match URLs with trailing slashes (e.g., `/a/` matching `{path: 'a', pathMatch: 'full'}`), resolving a long-standing inconsistency where such routes would fail despite `pathMatch: 'prefix'` variants (with empty children) succeeding.

**Alternatives Considered & Design Rationale:**

*  **`UrlTree.hasTrailingSlash` Property:**
    We initially explored adding a explicit boolean property to `UrlTree`.
    However, this would have required a significant public API change and breaking updates to custom `UrlSerializer` implementations to ensure the state was preserved. It also complicated the mental model of the `UrlTree` as a direct representation of the URL structure. We settled on representing trailing slashes as a trailing empty `UrlSegment`, which leverages the existing structure and serialization logic without API expansion.

    **Why we chose the current approach:**
    We decided on a "Permissive Matcher, Strict Serializer/Location" pattern.
    -   The **Matcher** is permissive: it allows consuming a trailing empty segment if it's the *only* thing left, treating it effectively as optional for `pathMatch: 'full'`. This aligns logic with how `pathMatch: 'prefix'` matches empty children.
    -   The **Serializer/Location** are strict: The `trailingSlash` configuration is verified at the boundaries (serialization and location normalization). If `trailingSlash: 'never'` is set, the slash is stripped *before* matching ever occurs. If matching sees a slash, it implies the configuration allowed it (or we are in a permissive mode), so the matcher should handle it gracefully.

fixes angular#16051
@atscott atscott added the target: minor This PR is targeted for the next minor release label Dec 29, 2025
@angular-robot angular-robot bot added detected: feature PR contains a feature commit area: router labels Dec 29, 2025
@atscott atscott added area: common Issues related to APIs in the @angular/common package and removed detected: feature PR contains a feature commit labels Dec 29, 2025
@ngbot ngbot bot added this to the Backlog milestone Dec 29, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: common Issues related to APIs in the @angular/common package area: router target: minor This PR is targeted for the next minor release

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Routing: Preserve trailing slash

1 participant