66 * found in the LICENSE file at https://angular.dev/license
77 */
88
9- import { PlatformLocation } from '@angular/common' ;
9+ import { LocationStrategy , PlatformLocation } from '@angular/common' ;
1010import {
1111 ApplicationRef ,
1212 type PlatformRef ,
@@ -21,7 +21,7 @@ import {
2121 platformServer ,
2222 ɵrenderInternal as renderInternal ,
2323} from '@angular/platform-server' ;
24- import { ActivatedRoute , Router } from '@angular/router' ;
24+ import { ActivatedRoute , Router , UrlSerializer } from '@angular/router' ;
2525import { Console } from '../console' ;
2626import { stripIndexHtmlFromURL , stripTrailingSlash } from './url' ;
2727
@@ -107,13 +107,15 @@ export async function renderAngular(
107107
108108 if ( ! routerIsProvided ) {
109109 hasNavigationError = false ;
110- } else if ( lastSuccessfulNavigation ) {
110+ } else if ( lastSuccessfulNavigation ?. finalUrl ) {
111111 hasNavigationError = false ;
112+
112113 const { pathname, search, hash } = envInjector . get ( PlatformLocation ) ;
113- const finalUrl = [ stripTrailingSlash ( pathname ) , search , hash ] . join ( '' ) ;
114+ const finalUrl = constructDecodedUrl ( { pathname, search, hash } ) ;
115+ const urlToRenderString = constructDecodedUrl ( urlToRender ) ;
114116
115- if ( urlToRender . href !== new URL ( finalUrl , urlToRender . origin ) . href ) {
116- redirectTo = finalUrl ;
117+ if ( urlToRenderString !== finalUrl ) {
118+ redirectTo = [ pathname , search , hash ] . join ( '' ) ;
117119 }
118120 }
119121
@@ -171,3 +173,23 @@ function asyncDestroyPlatform(platformRef: PlatformRef): Promise<void> {
171173 } , 0 ) ;
172174 } ) ;
173175}
176+
177+ /**
178+ * Constructs a decoded URL string from its components, ensuring consistency for comparison.
179+ *
180+ * This function takes a URL-like object (containing `pathname`, `search`, and `hash`),
181+ * strips the trailing slash from the pathname, joins the components, and then decodes
182+ * the entire string. This normalization is crucial for accurately comparing URLs
183+ * that might differ only in encoding or trailing slashes.
184+ *
185+ * @param url - An object containing the URL components:
186+ * - `pathname`: The path of the URL.
187+ * - `search`: The query string of the URL (including '?').
188+ * - `hash`: The hash fragment of the URL (including '#').
189+ * @returns The constructed and decoded URL string.
190+ */
191+ function constructDecodedUrl ( url : { pathname : string ; search : string ; hash : string } ) : string {
192+ const joinedUrl = [ stripTrailingSlash ( url . pathname ) , url . search , url . hash ] . join ( '' ) ;
193+
194+ return decodeURIComponent ( joinedUrl ) ;
195+ }
0 commit comments