Skip to content

Commit fe1fc61

Browse files
authored
[ENG-10560] Make angular routing redirects have rel canonical links (#907)
- Ticket: [ENG-10560] - Feature flag: n/a ## Summary of Changes 1. Added canonical url for guid pages. 2. Updated unit tests.
1 parent d226c70 commit fe1fc61

23 files changed

Lines changed: 1197 additions & 328 deletions

angular.json

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,20 @@
117117
"namedChunks": true
118118
},
119119
"development": {
120+
"outputMode": "static",
121+
"server": false,
122+
"ssr": false,
123+
"optimization": false,
124+
"extractLicenses": false,
125+
"sourceMap": true,
126+
"fileReplacements": [
127+
{
128+
"replace": "src/environments/environment.ts",
129+
"with": "src/environments/environment.development.ts"
130+
}
131+
]
132+
},
133+
"dev-ssr": {
120134
"optimization": false,
121135
"extractLicenses": false,
122136
"sourceMap": true,
@@ -164,6 +178,9 @@
164178
"development": {
165179
"buildTarget": "osf:build:development"
166180
},
181+
"dev-ssr": {
182+
"buildTarget": "osf:build:dev-ssr"
183+
},
167184
"docker": {
168185
"buildTarget": "osf:build:docker"
169186
},

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
"ngxs:store": "ng generate @ngxs/store:store --name --path",
2121
"prepare": "husky",
2222
"start": "ng serve",
23+
"start:ssr": "ng serve --configuration dev-ssr",
2324
"start:docker": "npm run check:config && ng serve --host 0.0.0.0 --port 4200 --poll 2000 --configuration development",
2425
"start:docker:local": "npm run check:config && ng serve --host 0.0.0.0 --port 4200 --poll 2000 --configuration docker",
2526
"test": "jest",

src/app/features/files/files.routes.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export const filesRoutes: Routes = [
1818
{
1919
path: ':fileProvider',
2020
canMatch: [isFileProvider],
21+
data: { canonicalPathTemplate: 'files/:fileProvider' },
2122
loadComponent: () => import('@osf/features/files/pages/files/files.component').then((c) => c.FilesComponent),
2223
},
2324
{
@@ -27,18 +28,12 @@ export const filesRoutes: Routes = [
2728
},
2829
{
2930
path: ':fileGuid',
31+
data: { canonicalPathTemplate: 'files/:fileGuid' },
3032
loadComponent: () => {
3133
return import('@osf/features/files/pages/file-detail/file-detail.component').then(
3234
(c) => c.FileDetailComponent
3335
);
3436
},
35-
children: [
36-
{
37-
path: 'metadata',
38-
loadChildren: () => import('@osf/features/metadata/metadata.routes').then((mod) => mod.metadataRoutes),
39-
data: { resourceType: ResourceType.File },
40-
},
41-
],
4237
},
4338
],
4439
},

src/app/features/files/pages/file-detail/file-detail.component.ts

Lines changed: 9 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { createDispatchMap, select, Store } from '@ngxs/store';
22

3-
import { TranslatePipe, TranslateService } from '@ngx-translate/core';
3+
import { TranslatePipe } from '@ngx-translate/core';
44

55
import { Button } from 'primeng/button';
66
import { Menu } from 'primeng/menu';
@@ -10,7 +10,6 @@ import { Tab, TabList, Tabs } from 'primeng/tabs';
1010
import { switchMap } from 'rxjs';
1111

1212
import { Clipboard } from '@angular/cdk/clipboard';
13-
import { DatePipe } from '@angular/common';
1413
import {
1514
ChangeDetectionStrategy,
1615
Component,
@@ -43,10 +42,10 @@ import { MetadataTabsComponent } from '@osf/shared/components/metadata-tabs/meta
4342
import { SubHeaderComponent } from '@osf/shared/components/sub-header/sub-header.component';
4443
import { MetadataResourceEnum } from '@osf/shared/enums/metadata-resource.enum';
4544
import { ResourceType } from '@osf/shared/enums/resource-type.enum';
46-
import { pathJoin } from '@osf/shared/helpers/path-join.helper';
4745
import { CustomConfirmationService } from '@osf/shared/services/custom-confirmation.service';
4846
import { DataciteService } from '@osf/shared/services/datacite/datacite.service';
4947
import { MetaTagsService } from '@osf/shared/services/meta-tags.service';
48+
import { MetaTagsBuilderService } from '@osf/shared/services/meta-tags-builder.service';
5049
import { ToastService } from '@osf/shared/services/toast.service';
5150
import { ViewOnlyLinkHelperService } from '@osf/shared/services/view-only-link-helper.service';
5251
import { FileDetailsModel } from '@shared/models/files/file.model';
@@ -92,7 +91,6 @@ import {
9291
templateUrl: './file-detail.component.html',
9392
styleUrl: './file-detail.component.scss',
9493
changeDetection: ChangeDetectionStrategy.OnPush,
95-
providers: [DatePipe],
9694
})
9795
export class FileDetailComponent {
9896
@HostBinding('class') classes = 'flex flex-column flex-1 w-full h-full';
@@ -106,16 +104,13 @@ export class FileDetailComponent {
106104
readonly customConfirmationService = inject(CustomConfirmationService);
107105

108106
private readonly metaTags = inject(MetaTagsService);
109-
private readonly datePipe = inject(DatePipe);
107+
private readonly metaTagsBuilder = inject(MetaTagsBuilderService);
110108
private readonly viewOnlyService = inject(ViewOnlyLinkHelperService);
111-
private readonly translateService = inject(TranslateService);
112109
private readonly environment = inject(ENVIRONMENT);
113110
private readonly clipboard = inject(Clipboard);
114111

115112
readonly dataciteService = inject(DataciteService);
116113

117-
private readonly webUrl = this.environment.webUrl;
118-
119114
private readonly actions = createDispatchMap({
120115
getFile: GetFile,
121116
getFileRevisions: GetFileRevisions,
@@ -204,24 +199,14 @@ export class FileDetailComponent {
204199
}
205200

206201
const file = this.file();
202+
207203
if (!file) return null;
208204

209-
return {
210-
osfGuid: file.guid,
211-
title: this.fileCustomMetadata()?.title || file.name,
212-
type: this.fileCustomMetadata()?.resourceTypeGeneral,
213-
description:
214-
this.fileCustomMetadata()?.description ?? this.translateService.instant('files.metaTagDescriptionPlaceholder'),
215-
url: pathJoin(this.webUrl, this.fileGuid),
216-
publishedDate: this.datePipe.transform(file.dateCreated, 'yyyy-MM-dd'),
217-
modifiedDate: this.datePipe.transform(file.dateModified, 'yyyy-MM-dd'),
218-
language: this.fileCustomMetadata()?.language,
219-
contributors: this.resourceContributors()?.map((contributor) => ({
220-
fullName: contributor.fullName,
221-
givenName: contributor.givenName,
222-
familyName: contributor.familyName,
223-
})),
224-
};
205+
return this.metaTagsBuilder.buildFileMetaTagsData({
206+
file,
207+
fileMetadata: this.fileCustomMetadata(),
208+
contributors: this.resourceContributors() ?? [],
209+
});
225210
});
226211

227212
constructor() {

src/app/features/metadata/metadata.routes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export const metadataRoutes: Routes = [
1414
},
1515
{
1616
path: ':recordId',
17+
data: { canonicalPathTemplate: 'metadata/:recordId' },
1718
component: MetadataComponent,
1819
},
1920
];

src/app/features/preprints/pages/preprint-details/preprint-details.component.spec.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,11 @@ import { ActivatedRoute, Router } from '@angular/router';
1313
import { HelpScoutService } from '@core/services/help-scout.service';
1414
import { PrerenderReadyService } from '@core/services/prerender-ready.service';
1515
import { ClearCurrentProvider } from '@core/store/provider';
16+
import { MetaTagsData } from '@osf/shared/models/meta-tags/meta-tags-data.model';
1617
import { CustomDialogService } from '@osf/shared/services/custom-dialog.service';
1718
import { DataciteService } from '@osf/shared/services/datacite/datacite.service';
1819
import { MetaTagsService } from '@osf/shared/services/meta-tags.service';
20+
import { MetaTagsBuilderService } from '@osf/shared/services/meta-tags-builder.service';
1921
import { ToastService } from '@osf/shared/services/toast.service';
2022
import { ContributorsSelectors } from '@osf/shared/stores/contributors';
2123

@@ -55,6 +57,7 @@ import { provideOSFCore } from '@testing/osf.testing.provider';
5557
import { CustomDialogServiceMockBuilder } from '@testing/providers/custom-dialog-provider.mock';
5658
import { HelpScoutServiceMockFactory } from '@testing/providers/help-scout.service.mock';
5759
import { MetaTagsServiceMockFactory } from '@testing/providers/meta-tags.service.mock';
60+
import { MetaTagsBuilderServiceMockFactory } from '@testing/providers/meta-tags-builder.service.mock';
5861
import { PrerenderReadyServiceMockFactory } from '@testing/providers/prerender-ready.service.mock';
5962
import { ActivatedRouteMockBuilder } from '@testing/providers/route-provider.mock';
6063
import { RouterMockBuilder, RouterMockType } from '@testing/providers/router-provider.mock';
@@ -70,6 +73,7 @@ describe('PreprintDetailsComponent', () => {
7073
let prerenderReadyServiceMock: jest.Mocked<PrerenderReadyService>;
7174
let dataciteServiceMock: ReturnType<typeof DataciteMockFactory>;
7275
let metaTagsServiceMock: ReturnType<typeof MetaTagsServiceMockFactory>;
76+
let metaTagsBuilderServiceMock: ReturnType<typeof MetaTagsBuilderServiceMockFactory>;
7377
let customDialogServiceMock: ReturnType<CustomDialogServiceMockBuilder['build']>;
7478
let toastService: ToastServiceMockType;
7579

@@ -122,6 +126,13 @@ describe('PreprintDetailsComponent', () => {
122126
prerenderReadyServiceMock = PrerenderReadyServiceMockFactory();
123127
dataciteServiceMock = DataciteMockFactory();
124128
metaTagsServiceMock = MetaTagsServiceMockFactory();
129+
metaTagsBuilderServiceMock = MetaTagsBuilderServiceMockFactory();
130+
metaTagsBuilderServiceMock.buildPreprintMetaTagsData.mockImplementation(
131+
({ providerId, preprint }) =>
132+
({
133+
canonicalUrl: `http://localhost:4200/preprints/${providerId}/${preprint?.id}`,
134+
}) as MetaTagsData
135+
);
125136
toastService = ToastServiceMock.simple();
126137
customDialogServiceMock =
127138
overrides?.dialogReturnsCloseValue === false
@@ -167,6 +178,7 @@ describe('PreprintDetailsComponent', () => {
167178
MockProvider(PrerenderReadyService, prerenderReadyServiceMock),
168179
MockProvider(DataciteService, dataciteServiceMock),
169180
MockProvider(MetaTagsService, metaTagsServiceMock),
181+
MockProvider(MetaTagsBuilderService, metaTagsBuilderServiceMock),
170182
MockProvider(CustomDialogService, customDialogServiceMock),
171183
provideMockStore({ signals }),
172184
],
@@ -199,7 +211,19 @@ describe('PreprintDetailsComponent', () => {
199211
it('should update meta tags when preprint and contributors are loaded', () => {
200212
setup();
201213

202-
expect(metaTagsServiceMock.updateMetaTags).toHaveBeenCalled();
214+
expect(metaTagsBuilderServiceMock.buildPreprintMetaTagsData).toHaveBeenCalledWith(
215+
expect.objectContaining({
216+
providerId: 'osf',
217+
preprint: expect.objectContaining({ id: 'preprint-1' }),
218+
})
219+
);
220+
221+
expect(metaTagsServiceMock.updateMetaTags).toHaveBeenCalledWith(
222+
expect.objectContaining({
223+
canonicalUrl: 'http://localhost:4200/preprints/osf/preprint-1',
224+
}),
225+
expect.anything()
226+
);
203227
});
204228

205229
it('should not fetch moderation actions when not moderator and no permissions', () => {
@@ -532,6 +556,7 @@ describe('PreprintDetailsComponent SSR', () => {
532556
MockProvider(Router, routerMock),
533557
MockProvider(CustomDialogService, CustomDialogServiceMockBuilder.create().withDefaultOpen().build()),
534558
MockProvider(DataciteService, DataciteMockFactory()),
559+
MockProvider(MetaTagsBuilderService, MetaTagsBuilderServiceMockFactory()),
535560
MockProvider(MetaTagsService, MetaTagsServiceMockFactory()),
536561
MockProvider(PrerenderReadyService, PrerenderReadyServiceMockFactory()),
537562
MockProvider(HelpScoutService, helpScoutServiceMock),

src/app/features/preprints/pages/preprint-details/preprint-details.component.ts

Lines changed: 12 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { Skeleton } from 'primeng/skeleton';
77

88
import { catchError, EMPTY, filter, map } from 'rxjs';
99

10-
import { DatePipe, isPlatformBrowser } from '@angular/common';
10+
import { isPlatformBrowser } from '@angular/common';
1111
import { HttpErrorResponse } from '@angular/common/http';
1212
import {
1313
ChangeDetectionStrategy,
@@ -30,10 +30,10 @@ import { PrerenderReadyService } from '@core/services/prerender-ready.service';
3030
import { ClearCurrentProvider } from '@core/store/provider';
3131
import { UserSelectors } from '@core/store/user';
3232
import { ReviewPermissions } from '@osf/shared/enums/review-permissions.enum';
33-
import { pathJoin } from '@osf/shared/helpers/path-join.helper';
3433
import { CustomDialogService } from '@osf/shared/services/custom-dialog.service';
3534
import { DataciteService } from '@osf/shared/services/datacite/datacite.service';
3635
import { MetaTagsService } from '@osf/shared/services/meta-tags.service';
36+
import { MetaTagsBuilderService } from '@osf/shared/services/meta-tags-builder.service';
3737
import { ToastService } from '@osf/shared/services/toast.service';
3838
import { ContributorsSelectors } from '@osf/shared/stores/contributors';
3939

@@ -82,7 +82,6 @@ import { CreateNewVersion, PreprintStepperSelectors } from '../../store/preprint
8282
],
8383
templateUrl: './preprint-details.component.html',
8484
styleUrl: './preprint-details.component.scss',
85-
providers: [DatePipe],
8685
changeDetection: ChangeDetectionStrategy.OnPush,
8786
})
8887
export class PreprintDetailsComponent implements OnInit, OnDestroy {
@@ -97,14 +96,13 @@ export class PreprintDetailsComponent implements OnInit, OnDestroy {
9796
private readonly customDialogService = inject(CustomDialogService);
9897
private readonly translateService = inject(TranslateService);
9998
private readonly metaTags = inject(MetaTagsService);
100-
private readonly datePipe = inject(DatePipe);
99+
private readonly metaTagsBuilder = inject(MetaTagsBuilderService);
101100
private readonly dataciteService = inject(DataciteService);
102101
private readonly prerenderReady = inject(PrerenderReadyService);
103-
private readonly platformId = inject(PLATFORM_ID);
104102
private readonly environment = inject(ENVIRONMENT);
103+
private readonly isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
105104

106-
private readonly isBrowser = isPlatformBrowser(this.platformId);
107-
105+
readonly providerId = toSignal(this.route.params.pipe(map((params) => params['providerId'])));
108106
private readonly preprintId = toSignal(this.route.params.pipe(map((params) => params['id'])));
109107

110108
private readonly actions = createDispatchMap({
@@ -118,7 +116,6 @@ export class PreprintDetailsComponent implements OnInit, OnDestroy {
118116
clearCurrentProvider: ClearCurrentProvider,
119117
});
120118

121-
readonly providerId = toSignal(this.route.params.pipe(map((params) => params['providerId'])));
122119
currentUser = select(UserSelectors.getCurrentUser);
123120
preprintProvider = select(PreprintProvidersSelectors.getPreprintProviderDetails(this.providerId()));
124121
isPreprintProviderLoading = select(PreprintProvidersSelectors.isPreprintProviderDetailsLoading);
@@ -410,26 +407,13 @@ export class PreprintDetailsComponent implements OnInit, OnDestroy {
410407
}
411408

412409
private setMetaTags() {
413-
this.metaTags.updateMetaTags(
414-
{
415-
osfGuid: this.preprint()?.id,
416-
title: this.preprint()?.title,
417-
description: this.preprint()?.description,
418-
publishedDate: this.datePipe.transform(this.preprint()?.datePublished, 'yyyy-MM-dd'),
419-
modifiedDate: this.datePipe.transform(this.preprint()?.dateModified, 'yyyy-MM-dd'),
420-
url: pathJoin(this.environment.webUrl, this.preprint()?.id ?? ''),
421-
doi: this.preprint()?.doi,
422-
keywords: this.preprint()?.tags,
423-
siteName: 'OSF',
424-
license: this.preprint()?.embeddedLicense?.name,
425-
contributors: this.contributors().map((contributor) => ({
426-
fullName: contributor.fullName,
427-
givenName: contributor.givenName,
428-
familyName: contributor.familyName,
429-
})),
430-
},
431-
this.destroyRef
432-
);
410+
const metaTags = this.metaTagsBuilder.buildPreprintMetaTagsData({
411+
providerId: this.providerId(),
412+
preprint: this.preprint(),
413+
contributors: this.contributors(),
414+
});
415+
416+
this.metaTags.updateMetaTags(metaTags, this.destroyRef);
433417
}
434418

435419
private checkAndSetVersionToTheUrl() {

0 commit comments

Comments
 (0)