Skip to content

feat(router): add trailingSlash config option#66452

Closed
atscott wants to merge 2 commits intoangular:mainfrom
atscott:trailingSlash3
Closed

feat(router): add trailingSlash config option#66452
atscott wants to merge 2 commits intoangular:mainfrom
atscott:trailingSlash3

Conversation

@atscott
Copy link
Copy Markdown
Contributor

@atscott atscott commented Jan 9, 2026

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

Route matchers now execute within the route's injection context.
@angular-robot angular-robot bot added detected: feature PR contains a feature commit area: router labels Jan 9, 2026
@ngbot ngbot bot added this to the Backlog milestone Jan 9, 2026
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 added a commit to atscott/angular that referenced this pull request Jan 12, 2026
Adds dedicated `LocationStrategy` subclasses: `NoTrailingSlashPathLocationStrategy` and `TrailingSlashPathLocationStrategy`.

The `TrailingSlashPathLocationStrategy` ensures that URLs prepared for the browser always end with a slash, while `NoTrailingSlashPathLocationStrategy` ensures they never do. This configuration only affects the URL written to the browser history; the `Location` service continues to normalize paths by stripping trailing slashes when reading from the browser.

Example:
```typescript
providers: [
  {provide: LocationStrategy, useClass: TrailingSlashPathLocationStrategy}
]
```

This approach to the trailing slash problem isolates the changes to the
existing LocationStrategy abstraction without changes to Router, as was
attempted in two other options (angular#66452 and angular#66423).

From an architectural perspective, this is the cleanest approach for several reasons:

1. Separation of Concerns and "Router Purity": The Router's primary job is to map a URL structure to an application state (ActivatedRoutes). It shouldn't necessarily be burdened with the formatting nuances of the underlying platform unless those nuances affect the state itself. By pushing trailing slash handling to the LocationStrategy, you treat the trailing slash as a "platform serialization format" rather than a "router state" concern. This avoids the "weirdness" in angular#66423 where the UrlTree (serialization format) disagrees with the ActivatedRouteSnapshot (logical state).

2. Tree Shakability: If an application doesn't care about trailing slashes (which is the default "never" behavior), they don't pay the cost for that logic. It essentially becomes a swappable "driver" for the URL interaction.

3. Simplicity for the Router: angular#66452 (consuming the slash as a segment) bleeds into the matching logic, potentially causing issues with child routes or wildcards effectively "eating" a segment that should be invisible. This option leaves the matching logic purely focused on meaningful path segments by continuing to strip the trailing slash on read.

4. Consistency with Existing Patterns: Angular already uses LocationStrategy to handle Hash vs Path routing. Adding "Trailing Slash" nuances there is a natural extension of that pattern—it's just another variation of "how do we represent this logic in the browser's address bar?"

fixes angular#16051
atscott added a commit to atscott/angular that referenced this pull request Jan 12, 2026
Adds dedicated `LocationStrategy` subclasses: `NoTrailingSlashPathLocationStrategy` and `TrailingSlashPathLocationStrategy`.

The `TrailingSlashPathLocationStrategy` ensures that URLs prepared for the browser always end with a slash, while `NoTrailingSlashPathLocationStrategy` ensures they never do. This configuration only affects the URL written to the browser history; the `Location` service continues to normalize paths by stripping trailing slashes when reading from the browser.

Example:
```typescript
providers: [
  {provide: LocationStrategy, useClass: TrailingSlashPathLocationStrategy}
]
```

This approach to the trailing slash problem isolates the changes to the
existing LocationStrategy abstraction without changes to Router, as was
attempted in two other options (angular#66452 and angular#66423).

From an architectural perspective, this is the cleanest approach for several reasons:

1. Separation of Concerns and "Router Purity": The Router's primary job is to map a URL structure to an application state (ActivatedRoutes). It shouldn't necessarily be burdened with the formatting nuances of the underlying platform unless those nuances affect the state itself. By pushing trailing slash handling to the LocationStrategy, you treat the trailing slash as a "platform serialization format" rather than a "router state" concern. This avoids the "weirdness" in angular#66423 where the UrlTree (serialization format) disagrees with the ActivatedRouteSnapshot (logical state).

2. Tree Shakability: If an application doesn't care about trailing slashes (which is the default "never" behavior), they don't pay the cost for that logic. It essentially becomes a swappable "driver" for the URL interaction.

3. Simplicity for the Router: angular#66452 (consuming the slash as a segment) bleeds into the matching logic, potentially causing issues with child routes or wildcards effectively "eating" a segment that should be invisible. This option leaves the matching logic purely focused on meaningful path segments by continuing to strip the trailing slash on read.

4. Consistency with Existing Patterns: Angular already uses LocationStrategy to handle Hash vs Path routing. Adding "Trailing Slash" nuances there is a natural extension of that pattern—it's just another variation of "how do we represent this logic in the browser's address bar?"

fixes angular#16051
atscott added a commit that referenced this pull request Jan 23, 2026
Adds dedicated `LocationStrategy` subclasses: `NoTrailingSlashPathLocationStrategy` and `TrailingSlashPathLocationStrategy`.

The `TrailingSlashPathLocationStrategy` ensures that URLs prepared for the browser always end with a slash, while `NoTrailingSlashPathLocationStrategy` ensures they never do. This configuration only affects the URL written to the browser history; the `Location` service continues to normalize paths by stripping trailing slashes when reading from the browser.

Example:
```typescript
providers: [
  {provide: LocationStrategy, useClass: TrailingSlashPathLocationStrategy}
]
```

This approach to the trailing slash problem isolates the changes to the
existing LocationStrategy abstraction without changes to Router, as was
attempted in two other options (#66452 and #66423).

From an architectural perspective, this is the cleanest approach for several reasons:

1. Separation of Concerns and "Router Purity": The Router's primary job is to map a URL structure to an application state (ActivatedRoutes). It shouldn't necessarily be burdened with the formatting nuances of the underlying platform unless those nuances affect the state itself. By pushing trailing slash handling to the LocationStrategy, you treat the trailing slash as a "platform serialization format" rather than a "router state" concern. This avoids the "weirdness" in #66423 where the UrlTree (serialization format) disagrees with the ActivatedRouteSnapshot (logical state).

2. Tree Shakability: If an application doesn't care about trailing slashes (which is the default "never" behavior), they don't pay the cost for that logic. It essentially becomes a swappable "driver" for the URL interaction.

3. Simplicity for the Router: #66452 (consuming the slash as a segment) bleeds into the matching logic, potentially causing issues with child routes or wildcards effectively "eating" a segment that should be invisible. This option leaves the matching logic purely focused on meaningful path segments by continuing to strip the trailing slash on read.

4. Consistency with Existing Patterns: Angular already uses LocationStrategy to handle Hash vs Path routing. Adding "Trailing Slash" nuances there is a natural extension of that pattern—it's just another variation of "how do we represent this logic in the browser's address bar?"

fixes #16051
@atscott atscott closed this Jan 23, 2026
@angular-automatic-lock-bot
Copy link
Copy Markdown

This issue has been automatically locked due to inactivity.
Please file a new issue if you are encountering a similar or related problem.

Read more about our automatic conversation locking policy.

This action has been performed automatically by a bot.

@angular-automatic-lock-bot angular-automatic-lock-bot bot locked and limited conversation to collaborators Feb 23, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area: router detected: feature PR contains a feature commit

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Routing: Preserve trailing slash

1 participant