diff --git a/packages/angular/ssr/src/routes/router.ts b/packages/angular/ssr/src/routes/router.ts index f01e9989028e..ff2e7fecd991 100644 --- a/packages/angular/ssr/src/routes/router.ts +++ b/packages/angular/ssr/src/routes/router.ts @@ -7,7 +7,7 @@ */ import { AngularAppManifest } from '../manifest'; -import { stripIndexHtmlFromURL, stripMatrixParams } from '../utils/url'; +import { stripAuxiliaryRoutes, stripIndexHtmlFromURL, stripMatrixParams } from '../utils/url'; import { extractRoutesAndCreateRouteTree } from './ng-routes'; import { RouteTree, RouteTreeNodeMetadata } from './route-tree'; @@ -87,6 +87,7 @@ export class ServerRouter { // A request to `http://www.example.com/page/index.html` will render the Angular route corresponding to `http://www.example.com/page`. let { pathname } = stripIndexHtmlFromURL(url); pathname = stripMatrixParams(pathname); + pathname = stripAuxiliaryRoutes(pathname); pathname = decodeURIComponent(pathname); return this.routeTree.match(pathname); diff --git a/packages/angular/ssr/src/utils/url.ts b/packages/angular/ssr/src/utils/url.ts index 1fa756e19c19..db7fb6511ba7 100644 --- a/packages/angular/ssr/src/utils/url.ts +++ b/packages/angular/ssr/src/utils/url.ts @@ -235,3 +235,38 @@ export function stripMatrixParams(pathname: string): string { export function constructUrl(pathname: string, search: string, hash: string): string { return decodeURIComponent([stripTrailingSlash(pathname), search, hash].join('')); } + +/** + * Removes Angular auxiliary route segments from a given URL path. + * + * Auxiliary routes have the format `(outlet:segment)` appended to a primary URL. + * Multiple auxiliary routes and even nested forms will be removed entirely. + * + * @param pathname - The URL path from which to remove auxiliary route segments. + * @returns The cleaned URL path without auxiliary outlet routes. + * + * @example + * ```ts + * // Single auxiliary route + * stripAuxiliaryRoutes('/inbox/33(popup:compose)'); + * // → '/inbox/33' + * + * // Multiple auxiliary routes + * stripAuxiliaryRoutes('/mail/7(popup:compose)(drawer:details)(chat:room42)'); + * // → '/mail/7' + * + * // Nested auxiliary routes (rare but possible) + * stripAuxiliaryRoutes('/inbox(overlay:view(popup:info))(tracker:read)'); + * // → '/inbox' + * + * // Path without auxiliary routes remains unchanged + * stripAuxiliaryRoutes('/path/to/resource'); + * // → '/path/to/resource' + * ``` + */ + +export function stripAuxiliaryRoutes(pathname: string): string { + const index = pathname.indexOf('('); + + return index !== -1 ? pathname.slice(0, index) : pathname; +} diff --git a/packages/angular/ssr/test/utils/url_spec.ts b/packages/angular/ssr/test/utils/url_spec.ts index 9a7a7cb3ad49..0e91d511c7a9 100644 --- a/packages/angular/ssr/test/utils/url_spec.ts +++ b/packages/angular/ssr/test/utils/url_spec.ts @@ -11,6 +11,7 @@ import { addTrailingSlash, buildPathWithParams, joinUrlParts, + stripAuxiliaryRoutes, stripIndexHtmlFromURL, stripLeadingSlash, stripMatrixParams, @@ -208,4 +209,24 @@ describe('URL Utils', () => { expect(stripMatrixParams('')).toBe(''); }); }); + + describe('stripAuxiliaryRoutes', () => { + it('should remove a single auxiliary route', () => { + expect(stripAuxiliaryRoutes('/inbox/33(popup:compose)')).toBe('/inbox/33'); + }); + + it('should remove multiple auxiliary routes', () => { + expect(stripAuxiliaryRoutes('/mail/7(popup:compose)(drawer:details)(chat:room42)')).toBe( + '/mail/7', + ); + }); + + it('should remove nested auxiliary routes', () => { + expect(stripAuxiliaryRoutes('/inbox(overlay:view(popup:info))(tracker:read)')).toBe('/inbox'); + }); + + it('should not modify a path without auxiliary routes', () => { + expect(stripAuxiliaryRoutes('/path/to/resource')).toBe('/path/to/resource'); + }); + }); });