Skip to content

Commit db526a0

Browse files
authored
Merge pull request DSpace#2331 from tdonohue/ssr_redirect_301
Implement basic 301 Redirect for DSpace 6.x URLs when SSR is used.
2 parents 9a5e26f + d4a5308 commit db526a0

5 files changed

Lines changed: 47 additions & 21 deletions

File tree

src/app/core/data/dso-redirect.service.spec.ts

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,15 @@ import { RequestService } from './request.service';
1010
import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils';
1111
import { Item } from '../shared/item.model';
1212
import { EMBED_SEPARATOR } from './base/base-data.service';
13+
import { HardRedirectService } from '../services/hard-redirect.service';
1314

1415
describe('DsoRedirectService', () => {
1516
let scheduler: TestScheduler;
1617
let service: DsoRedirectService;
1718
let halService: HALEndpointService;
1819
let requestService: RequestService;
1920
let rdbService: RemoteDataBuildService;
20-
let router;
21+
let redirectService: HardRedirectService;
2122
let remoteData;
2223
const dsoUUID = '9b4f22f4-164a-49db-8817-3316b6ee5746';
2324
const dsoHandle = '1234567789/22';
@@ -38,9 +39,6 @@ describe('DsoRedirectService', () => {
3839
generateRequestId: requestUUID,
3940
send: true
4041
});
41-
router = {
42-
navigate: jasmine.createSpy('navigate')
43-
};
4442

4543
remoteData = createSuccessfulRemoteDataObject(Object.assign(new Item(), {
4644
type: 'item',
@@ -52,12 +50,17 @@ describe('DsoRedirectService', () => {
5250
a: remoteData
5351
})
5452
});
53+
54+
redirectService = jasmine.createSpyObj('redirectService', {
55+
redirect: {}
56+
});
57+
5558
service = new DsoRedirectService(
5659
requestService,
5760
rdbService,
5861
objectCache,
5962
halService,
60-
router,
63+
redirectService
6164
);
6265
});
6366

@@ -104,7 +107,7 @@ describe('DsoRedirectService', () => {
104107
redir.subscribe();
105108
scheduler.schedule(() => redir);
106109
scheduler.flush();
107-
expect(router.navigate).toHaveBeenCalledWith(['/items/' + remoteData.payload.uuid]);
110+
expect(redirectService.redirect).toHaveBeenCalledWith('/items/' + remoteData.payload.uuid, 301);
108111
});
109112
it('should navigate to entities route with the corresponding entity type', () => {
110113
remoteData.payload.type = 'item';
@@ -121,7 +124,7 @@ describe('DsoRedirectService', () => {
121124
redir.subscribe();
122125
scheduler.schedule(() => redir);
123126
scheduler.flush();
124-
expect(router.navigate).toHaveBeenCalledWith(['/entities/publication/' + remoteData.payload.uuid]);
127+
expect(redirectService.redirect).toHaveBeenCalledWith('/entities/publication/' + remoteData.payload.uuid, 301);
125128
});
126129

127130
it('should navigate to collections route', () => {
@@ -130,7 +133,7 @@ describe('DsoRedirectService', () => {
130133
redir.subscribe();
131134
scheduler.schedule(() => redir);
132135
scheduler.flush();
133-
expect(router.navigate).toHaveBeenCalledWith(['/collections/' + remoteData.payload.uuid]);
136+
expect(redirectService.redirect).toHaveBeenCalledWith('/collections/' + remoteData.payload.uuid, 301);
134137
});
135138

136139
it('should navigate to communities route', () => {
@@ -139,7 +142,7 @@ describe('DsoRedirectService', () => {
139142
redir.subscribe();
140143
scheduler.schedule(() => redir);
141144
scheduler.flush();
142-
expect(router.navigate).toHaveBeenCalledWith(['/communities/' + remoteData.payload.uuid]);
145+
expect(redirectService.redirect).toHaveBeenCalledWith('/communities/' + remoteData.payload.uuid, 301);
143146
});
144147
});
145148

src/app/core/data/dso-redirect.service.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
*/
88
/* eslint-disable max-classes-per-file */
99
import { Injectable } from '@angular/core';
10-
import { Router } from '@angular/router';
1110
import { Observable } from 'rxjs';
1211
import { tap } from 'rxjs/operators';
1312
import { hasValue } from '../../shared/empty.util';
@@ -21,6 +20,7 @@ import { getFirstCompletedRemoteData } from '../shared/operators';
2120
import { DSpaceObject } from '../shared/dspace-object.model';
2221
import { IdentifiableDataService } from './base/identifiable-data.service';
2322
import { getDSORoute } from '../../app-routing-paths';
23+
import { HardRedirectService } from '../services/hard-redirect.service';
2424

2525
const ID_ENDPOINT = 'pid';
2626
const UUID_ENDPOINT = 'dso';
@@ -74,13 +74,16 @@ export class DsoRedirectService {
7474
protected rdbService: RemoteDataBuildService,
7575
protected objectCache: ObjectCacheService,
7676
protected halService: HALEndpointService,
77-
private router: Router,
77+
private hardRedirectService: HardRedirectService
7878
) {
7979
this.dataService = new DsoByIdOrUUIDDataService(requestService, rdbService, objectCache, halService);
8080
}
8181

8282
/**
83-
* Retrieve a DSpaceObject by
83+
* Redirect to a DSpaceObject's path using the given identifier type and ID.
84+
* This is used to redirect paths like "/handle/[prefix]/[suffix]" to the object's path (e.g. /items/[uuid]).
85+
* See LookupGuard for more examples.
86+
*
8487
* @param id the identifier of the object to retrieve
8588
* @param identifierType the type of the given identifier (defaults to UUID)
8689
*/
@@ -94,7 +97,8 @@ export class DsoRedirectService {
9497
if (hasValue(dso.uuid)) {
9598
let newRoute = getDSORoute(dso);
9699
if (hasValue(newRoute)) {
97-
this.router.navigate([newRoute]);
100+
// Use a "301 Moved Permanently" redirect for SEO purposes
101+
this.hardRedirectService.redirect(newRoute, 301);
98102
}
99103
}
100104
}

src/app/core/services/hard-redirect.service.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@ export abstract class HardRedirectService {
1111
*
1212
* @param url
1313
* the page to redirect to
14+
* @param statusCode
15+
* optional HTTP status code to use for redirect (default = 302, which is a temporary redirect)
1416
*/
15-
abstract redirect(url: string);
17+
abstract redirect(url: string, statusCode?: number);
1618

1719
/**
1820
* Get the current route, with query params included

src/app/core/services/server-hard-redirect.service.spec.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,20 +22,33 @@ describe('ServerHardRedirectService', () => {
2222
expect(service).toBeTruthy();
2323
});
2424

25-
describe('when performing a redirect', () => {
26-
25+
describe('when performing a default redirect', () => {
2726
const redirect = 'test redirect';
2827

2928
beforeEach(() => {
3029
service.redirect(redirect);
3130
});
3231

33-
it('should update the response object', () => {
32+
it('should perform a 302 redirect', () => {
3433
expect(mockResponse.redirect).toHaveBeenCalledWith(302, redirect);
3534
expect(mockResponse.end).toHaveBeenCalled();
3635
});
3736
});
3837

38+
describe('when performing a 301 redirect', () => {
39+
const redirect = 'test 301 redirect';
40+
const redirectStatusCode = 301;
41+
42+
beforeEach(() => {
43+
service.redirect(redirect, redirectStatusCode);
44+
});
45+
46+
it('should redirect with passed in status code', () => {
47+
expect(mockResponse.redirect).toHaveBeenCalledWith(redirectStatusCode, redirect);
48+
expect(mockResponse.end).toHaveBeenCalled();
49+
});
50+
});
51+
3952
describe('when requesting the current route', () => {
4053

4154
beforeEach(() => {

src/app/core/services/server-hard-redirect.service.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,14 @@ export class ServerHardRedirectService extends HardRedirectService {
1717
}
1818

1919
/**
20-
* Perform a hard redirect to URL
20+
* Perform a hard redirect to a given location.
21+
*
2122
* @param url
23+
* the page to redirect to
24+
* @param statusCode
25+
* optional HTTP status code to use for redirect (default = 302, which is a temporary redirect)
2226
*/
23-
redirect(url: string) {
27+
redirect(url: string, statusCode?: number) {
2428

2529
if (url === this.req.url) {
2630
return;
@@ -38,8 +42,8 @@ export class ServerHardRedirectService extends HardRedirectService {
3842
process.exit(1);
3943
}
4044
} else {
41-
// attempt to use the already set status
42-
let status = this.res.statusCode || 0;
45+
// attempt to use passed in statusCode or the already set status (in request)
46+
let status = statusCode || this.res.statusCode || 0;
4347
if (status < 300 || status >= 400) {
4448
// temporary redirect
4549
status = 302;

0 commit comments

Comments
 (0)