Skip to content

Commit 9bacf39

Browse files
committed
fix(@angular/ssr): properly manage catch-all routes with base href
This fix ensures that catch-all routes (e.g., wildcard routes like `**`) are handled correctly when a base href is configured in an Angular SSR application. Closes #29397 (cherry picked from commit 3546c6d)
1 parent 14d2f7c commit 9bacf39

File tree

3 files changed

+18
-14
lines changed

3 files changed

+18
-14
lines changed

packages/angular/ssr/src/routes/ng-routes.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -590,13 +590,13 @@ export async function getRoutesFromAngularRouterConfig(
590590

591591
if (serverConfigRouteTree) {
592592
for (const { route, presentInClientRouter } of serverConfigRouteTree.traverse()) {
593-
if (presentInClientRouter || route === '**') {
593+
if (presentInClientRouter || route.endsWith('/**')) {
594594
// Skip if matched or it's the catch-all route.
595595
continue;
596596
}
597597

598598
errors.push(
599-
`The '${route}' server route does not match any routes defined in the Angular ` +
599+
`The '${stripLeadingSlash(route)}' server route does not match any routes defined in the Angular ` +
600600
`routing configuration (typically provided as a part of the 'provideRouter' call). ` +
601601
'Please make sure that the mentioned server route is present in the Angular routing configuration.',
602602
);

packages/angular/ssr/src/routes/route-tree.ts

+7-11
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9-
import { stripTrailingSlash } from '../utils/url';
9+
import { addLeadingSlash } from '../utils/url';
1010
import { RenderMode } from './route-config';
1111

1212
/**
@@ -116,7 +116,7 @@ export class RouteTree<AdditionalMetadata extends Record<string, unknown> = {}>
116116
* The root node of the route tree.
117117
* All routes are stored and accessed relative to this root node.
118118
*/
119-
private readonly root = this.createEmptyRouteTreeNode('');
119+
private readonly root = this.createEmptyRouteTreeNode('<root>');
120120

121121
/**
122122
* A counter that tracks the order of route insertion.
@@ -155,7 +155,7 @@ export class RouteTree<AdditionalMetadata extends Record<string, unknown> = {}>
155155
// At the leaf node, store the full route and its associated metadata
156156
node.metadata = {
157157
...metadata,
158-
route: normalizedSegments.join('/'),
158+
route: addLeadingSlash(normalizedSegments.join('/')),
159159
};
160160

161161
node.insertionIndex = this.insertionIndexCounter++;
@@ -230,7 +230,7 @@ export class RouteTree<AdditionalMetadata extends Record<string, unknown> = {}>
230230
* @returns An array of path segments.
231231
*/
232232
private getPathSegments(route: string): string[] {
233-
return stripTrailingSlash(route).split('/');
233+
return route.split('/').filter(Boolean);
234234
}
235235

236236
/**
@@ -246,18 +246,14 @@ export class RouteTree<AdditionalMetadata extends Record<string, unknown> = {}>
246246
* @returns The node that best matches the remaining segments or `undefined` if no match is found.
247247
*/
248248
private traverseBySegments(
249-
remainingSegments: string[] | undefined,
249+
remainingSegments: string[],
250250
node = this.root,
251251
): RouteTreeNode<AdditionalMetadata> | undefined {
252252
const { metadata, children } = node;
253253

254254
// If there are no remaining segments and the node has metadata, return this node
255-
if (!remainingSegments?.length) {
256-
if (metadata) {
257-
return node;
258-
}
259-
260-
return;
255+
if (!remainingSegments.length) {
256+
return metadata ? node : node.children.get('**');
261257
}
262258

263259
// If the node has no children, end the traversal

packages/angular/ssr/test/routes/route-tree_spec.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ describe('RouteTree', () => {
150150
describe('match', () => {
151151
it('should handle empty routes', () => {
152152
routeTree.insert('', { renderMode: RenderMode.Server });
153-
expect(routeTree.match('')).toEqual({ route: '', renderMode: RenderMode.Server });
153+
expect(routeTree.match('')).toEqual({ route: '/', renderMode: RenderMode.Server });
154154
});
155155

156156
it('should insert and match basic routes', () => {
@@ -274,5 +274,13 @@ describe('RouteTree', () => {
274274
renderMode: RenderMode.Server,
275275
});
276276
});
277+
278+
it('should correctly match catch-all segments with a prefix', () => {
279+
routeTree.insert('/de/**', { renderMode: RenderMode.Server });
280+
expect(routeTree.match('/de')).toEqual({
281+
route: '/de/**',
282+
renderMode: RenderMode.Server,
283+
});
284+
});
277285
});
278286
});

0 commit comments

Comments
 (0)