Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 10 additions & 12 deletions packages/angular/ssr/src/utils/ng.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.dev/license
*/

import { APP_BASE_HREF, PlatformLocation } from '@angular/common';
import { LocationStrategy } from '@angular/common';
import {
ApplicationRef,
type PlatformRef,
Expand All @@ -21,7 +21,7 @@ import {
platformServer,
ɵrenderInternal as renderInternal,
} from '@angular/platform-server';
import { ActivatedRoute, Router } from '@angular/router';
import { ActivatedRoute, Router, UrlSerializer } from '@angular/router';
import { Console } from '../console';
import { joinUrlParts, stripIndexHtmlFromURL } from './url';

Expand Down Expand Up @@ -60,12 +60,12 @@ export async function renderAngular(
serverContext: string,
): Promise<{ hasNavigationError: boolean; redirectTo?: string; content: () => Promise<string> }> {
// A request to `http://www.example.com/page/index.html` will render the Angular route corresponding to `http://www.example.com/page`.
const urlToRender = stripIndexHtmlFromURL(url).toString();
const urlToRender = stripIndexHtmlFromURL(url);
const platformRef = platformServer([
{
provide: INITIAL_CONFIG,
useValue: {
url: urlToRender,
url: urlToRender.href,
document: html,
},
},
Expand Down Expand Up @@ -110,15 +110,13 @@ export async function renderAngular(
} else if (lastSuccessfulNavigation?.finalUrl) {
hasNavigationError = false;

const { finalUrl, initialUrl } = lastSuccessfulNavigation;
const finalUrlStringified = finalUrl.toString();
const urlSerializer = envInjector.get(UrlSerializer);
const locationStrategy = envInjector.get(LocationStrategy);
const finalUrlSerialized = urlSerializer.serialize(lastSuccessfulNavigation.finalUrl);
const finalExternalUrl = joinUrlParts(locationStrategy.getBaseHref(), finalUrlSerialized);

if (initialUrl.toString() !== finalUrlStringified) {
const baseHref =
envInjector.get(APP_BASE_HREF, null, { optional: true }) ??
envInjector.get(PlatformLocation).getBaseHrefFromDOM();

redirectTo = joinUrlParts(baseHref, finalUrlStringified);
if (urlToRender.href !== new URL(finalExternalUrl, urlToRender.origin).href) {
redirectTo = finalExternalUrl;
}
}

Expand Down
14 changes: 0 additions & 14 deletions packages/angular/ssr/test/app-engine_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,6 @@ import { RenderMode } from '../src/routes/route-config';
import { setAngularAppTestingManifest } from './testing-utils';

function createEntryPoint(locale: string) {
@Component({
standalone: true,
selector: `app-ssr-${locale}`,
template: `SSR works ${locale.toUpperCase()}`,
})
class SSRComponent {}

@Component({
standalone: true,
selector: `app-ssg-${locale}`,
template: `SSG works ${locale.toUpperCase()}`,
})
class SSGComponent {}

return async () => {
@Component({
standalone: true,
Expand Down
49 changes: 47 additions & 2 deletions packages/angular/ssr/test/app_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
import '@angular/compiler';
/* eslint-enable import/no-unassigned-import */

import { Component } from '@angular/core';
import { Component, inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import { AngularServerApp } from '../src/app';
import { RenderMode } from '../src/routes/route-config';
import { setAngularAppTestingManifest } from './testing-utils';
Expand All @@ -27,14 +28,46 @@ describe('AngularServerApp', () => {
})
class HomeComponent {}

@Component({
selector: 'app-redirect',
})
class RedirectComponent {
constructor() {
void inject(Router).navigate([], {
queryParams: { filter: 'test' },
});
}
}

const queryParamAdderGuard: CanActivateFn = (_route, state) => {
const urlTree = inject(Router).parseUrl(state.url);

if (urlTree.queryParamMap.has('filter')) {
return true;
}

urlTree.queryParams = {
filter: 'test',
};

return urlTree;
};

setAngularAppTestingManifest(
[
{ path: 'home', component: HomeComponent },
{ path: 'home-csr', component: HomeComponent },
{ path: 'home-ssg', component: HomeComponent },
{ path: 'page-with-headers', component: HomeComponent },
{ path: 'page-with-status', component: HomeComponent },

{ path: 'redirect', redirectTo: 'home' },
{ path: 'redirect-via-navigate', component: RedirectComponent },
{
path: 'redirect-via-guard',
canActivate: [queryParamAdderGuard],
component: HomeComponent,
},
{ path: 'redirect/relative', redirectTo: 'home' },
{ path: 'redirect/:param/relative', redirectTo: 'home' },
{ path: 'redirect/absolute', redirectTo: '/home' },
Expand Down Expand Up @@ -260,11 +293,23 @@ describe('AngularServerApp', () => {
});

describe('SSR pages', () => {
it('returns a 302 status and redirects to the correct location when redirectTo is a function', async () => {
it('returns a 302 status and redirects to the correct location when `redirectTo` is a function', async () => {
const response = await app.handle(new Request('http://localhost/redirect-to-function'));
expect(response?.headers.get('location')).toBe('/home');
expect(response?.status).toBe(302);
});

it('returns a 302 status and redirects to the correct location when `router.navigate` is used', async () => {
const response = await app.handle(new Request('http://localhost/redirect-via-navigate'));
expect(response?.headers.get('location')).toBe('/redirect-via-navigate?filter=test');
expect(response?.status).toBe(302);
});

it('returns a 302 status and redirects to the correct location when `urlTree` is updated in a guard', async () => {
const response = await app.handle(new Request('http://localhost/redirect-via-guard'));
expect(response?.headers.get('location')).toBe('/redirect-via-guard?filter=test');
expect(response?.status).toBe(302);
});
});
});
});