Skip to content

Commit c75d7b7

Browse files
authored
[ENG-8639] add <meta> tags to files detail (#324)
* chore(meta-tags): cleaner meta-tag cleanup (without urls) * chore(meta-tags): add full name to contributor tag * feat(meta-tags): add meta tags to file-detail page * fix(meta-tags): use image that exists
1 parent 5bbf4b8 commit c75d7b7

File tree

9 files changed

+100
-76
lines changed

9 files changed

+100
-76
lines changed

src/app/app.component.ts

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,14 @@ import { TranslateService } from '@ngx-translate/core';
44

55
import { DialogService } from 'primeng/dynamicdialog';
66

7-
import { filter } from 'rxjs/operators';
8-
9-
import { ChangeDetectionStrategy, Component, DestroyRef, effect, inject, OnInit } from '@angular/core';
10-
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
11-
import { NavigationEnd, Router, RouterOutlet } from '@angular/router';
7+
import { ChangeDetectionStrategy, Component, effect, inject, OnInit } from '@angular/core';
8+
import { Router, RouterOutlet } from '@angular/router';
129

1310
import { GetCurrentUser } from '@core/store/user';
1411
import { GetEmails, UserEmailsSelectors } from '@core/store/user-emails';
1512
import { ConfirmEmailComponent } from '@shared/components';
1613

1714
import { FullScreenLoaderComponent, ToastComponent } from './shared/components';
18-
import { MetaTagsService } from './shared/services';
1915

2016
@Component({
2117
selector: 'osf-root',
@@ -26,18 +22,15 @@ import { MetaTagsService } from './shared/services';
2622
providers: [DialogService],
2723
})
2824
export class AppComponent implements OnInit {
29-
private readonly destroyRef = inject(DestroyRef);
3025
private readonly dialogService = inject(DialogService);
3126
private readonly router = inject(Router);
3227
private readonly translateService = inject(TranslateService);
33-
private readonly metaTagsService = inject(MetaTagsService);
3428

3529
private readonly actions = createDispatchMap({ getCurrentUser: GetCurrentUser, getEmails: GetEmails });
3630

3731
unverifiedEmails = select(UserEmailsSelectors.getUnverifiedEmails);
3832

3933
constructor() {
40-
this.setupMetaTagsCleanup();
4134
effect(() => {
4235
if (this.unverifiedEmails().length) {
4336
this.showEmailDialog();
@@ -50,15 +43,6 @@ export class AppComponent implements OnInit {
5043
this.actions.getEmails();
5144
}
5245

53-
private setupMetaTagsCleanup(): void {
54-
this.router.events
55-
.pipe(
56-
filter((event) => event instanceof NavigationEnd),
57-
takeUntilDestroyed(this.destroyRef)
58-
)
59-
.subscribe((event: NavigationEnd) => this.metaTagsService.clearMetaTagsIfNeeded(event.url));
60-
}
61-
6246
private showEmailDialog() {
6347
this.dialogService.open(ConfirmEmailComponent, {
6448
width: '448px',

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

Lines changed: 41 additions & 3 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 } from '@ngx-translate/core';
3+
import { TranslatePipe, TranslateService } from '@ngx-translate/core';
44

55
import { Button } from 'primeng/button';
66
import { Menu } from 'primeng/menu';
@@ -9,6 +9,7 @@ import { Tab, TabList, Tabs } from 'primeng/tabs';
99

1010
import { switchMap } from 'rxjs';
1111

12+
import { DatePipe } from '@angular/common';
1213
import {
1314
ChangeDetectionStrategy,
1415
Component,
@@ -37,8 +38,9 @@ import {
3738
} from '@osf/features/metadata/store';
3839
import { LoadingSpinnerComponent, MetadataTabsComponent, SubHeaderComponent } from '@osf/shared/components';
3940
import { MetadataResourceEnum, ResourceType } from '@osf/shared/enums';
41+
import { pathJoin } from '@osf/shared/helpers';
4042
import { MetadataTabsModel, OsfFile } from '@osf/shared/models';
41-
import { CustomConfirmationService, ToastService } from '@osf/shared/services';
43+
import { CustomConfirmationService, MetaTagsService, ToastService } from '@osf/shared/services';
4244

4345
import {
4446
FileKeywordsComponent,
@@ -58,6 +60,8 @@ import {
5860
GetFileRevisions,
5961
} from '../../store';
6062

63+
import { environment } from 'src/environments/environment';
64+
6165
@Component({
6266
selector: 'osf-file-detail',
6367
imports: [
@@ -80,6 +84,7 @@ import {
8084
templateUrl: './file-detail.component.html',
8185
styleUrl: './file-detail.component.scss',
8286
changeDetection: ChangeDetectionStrategy.OnPush,
87+
providers: [DatePipe],
8388
})
8489
export class FileDetailComponent {
8590
@HostBinding('class') classes = 'flex flex-column flex-1 w-full h-full';
@@ -91,6 +96,9 @@ export class FileDetailComponent {
9196
readonly sanitizer = inject(DomSanitizer);
9297
readonly toastService = inject(ToastService);
9398
readonly customConfirmationService = inject(CustomConfirmationService);
99+
private readonly metaTags = inject(MetaTagsService);
100+
private readonly datePipe = inject(DatePipe);
101+
private readonly translateService = inject(TranslateService);
94102

95103
private readonly actions = createDispatchMap({
96104
getFile: GetFile,
@@ -110,8 +118,11 @@ export class FileDetailComponent {
110118
isFileLoading = select(FilesSelectors.isOpenedFileLoading);
111119
cedarRecords = select(MetadataSelectors.getCedarRecords);
112120
cedarTemplates = select(MetadataSelectors.getCedarTemplates);
113-
114121
isAnonymous = select(FilesSelectors.isFilesAnonymous);
122+
fileCustomMetadata = select(FilesSelectors.getFileCustomMetadata);
123+
resourceMetadata = select(FilesSelectors.getResourceMetadata);
124+
resourceContributors = select(FilesSelectors.getContributors);
125+
115126
safeLink: SafeResourceUrl | null = null;
116127
resourceId = '';
117128
resourceType = '';
@@ -162,6 +173,33 @@ export class FileDetailComponent {
162173
selectedCedarTemplate = signal<CedarMetadataDataTemplateJsonApi | null>(null);
163174
cedarFormReadonly = signal<boolean>(true);
164175

176+
private readonly effectMetaTags = effect(() => {
177+
const metaTagsData = this.metaTagsData();
178+
if (metaTagsData) {
179+
this.metaTags.updateMetaTags(metaTagsData, this.destroyRef);
180+
}
181+
});
182+
183+
private readonly metaTagsData = computed(() => {
184+
const file = this.file();
185+
if (!file) return null;
186+
return {
187+
title: this.fileCustomMetadata()?.title || file.name,
188+
description:
189+
this.fileCustomMetadata()?.description ??
190+
this.translateService.instant('files.metaTagDescriptionPlaceholder'),
191+
url: pathJoin(environment.webUrl, this.fileGuid),
192+
publishedDate: this.datePipe.transform(file.dateCreated, 'yyyy-MM-dd'),
193+
modifiedDate: this.datePipe.transform(file.dateModified, 'yyyy-MM-dd'),
194+
language: this.fileCustomMetadata()?.language,
195+
contributors: this.resourceContributors()?.map((contributor) => ({
196+
fullName: contributor.fullName,
197+
givenName: contributor.givenName,
198+
familyName: contributor.familyName,
199+
})),
200+
};
201+
});
202+
165203
constructor() {
166204
this.route.params
167205
.pipe(

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

Lines changed: 20 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -363,25 +363,26 @@ export class PreprintDetailsComponent extends DataciteTrackerComponent implement
363363
}
364364

365365
private setMetaTags() {
366-
const image = 'engines-dist/registries/assets/img/osf-sharing.png';
367-
368-
this.metaTags.updateMetaTags({
369-
title: this.preprint()?.title,
370-
description: this.preprint()?.description,
371-
publishedDate: this.datePipe.transform(this.preprint()?.datePublished, 'yyyy-MM-dd'),
372-
modifiedDate: this.datePipe.transform(this.preprint()?.dateModified, 'yyyy-MM-dd'),
373-
url: pathJoin(environment.webUrl, this.preprint()?.id ?? ''),
374-
image,
375-
identifier: this.preprint()?.id,
376-
doi: this.preprint()?.doi,
377-
keywords: this.preprint()?.tags,
378-
siteName: 'OSF',
379-
license: this.preprint()?.embeddedLicense?.name,
380-
contributors: this.contributors().map((contributor) => ({
381-
givenName: contributor.fullName,
382-
familyName: contributor.familyName,
383-
})),
384-
});
366+
this.metaTags.updateMetaTags(
367+
{
368+
title: this.preprint()?.title,
369+
description: this.preprint()?.description,
370+
publishedDate: this.datePipe.transform(this.preprint()?.datePublished, 'yyyy-MM-dd'),
371+
modifiedDate: this.datePipe.transform(this.preprint()?.dateModified, 'yyyy-MM-dd'),
372+
url: pathJoin(environment.webUrl, this.preprint()?.id ?? ''),
373+
identifier: this.preprint()?.id,
374+
doi: this.preprint()?.doi,
375+
keywords: this.preprint()?.tags,
376+
siteName: 'OSF',
377+
license: this.preprint()?.embeddedLicense?.name,
378+
contributors: this.contributors().map((contributor) => ({
379+
fullName: contributor.fullName,
380+
givenName: contributor.givenName,
381+
familyName: contributor.familyName,
382+
})),
383+
},
384+
this.destroyRef,
385+
);
385386
}
386387

387388
private hasReadWriteAccess(): boolean {

src/app/features/registry/registry.component.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ describe('RegistryComponent', () => {
4141
{ provide: DataciteService, useValue: dataciteService },
4242
{
4343
provide: MetaTagsService,
44-
useValue: { updateMetaTagsForRoute: jest.fn() },
44+
useValue: { updateMetaTags: jest.fn() },
4545
},
4646
],
4747
}).compileComponents();

src/app/features/registry/registry.component.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { select } from '@ngxs/store';
33
import { Observable } from 'rxjs';
44

55
import { DatePipe } from '@angular/common';
6-
import { ChangeDetectionStrategy, Component, effect, HostBinding, inject } from '@angular/core';
6+
import { ChangeDetectionStrategy, Component, DestroyRef, effect, HostBinding, inject } from '@angular/core';
77
import { toObservable } from '@angular/core/rxjs-interop';
88
import { RouterOutlet } from '@angular/router';
99

@@ -29,6 +29,7 @@ export class RegistryComponent extends DataciteTrackerComponent {
2929

3030
private readonly metaTags = inject(MetaTagsService);
3131
private readonly datePipe = inject(DatePipe);
32+
private readonly destroyRef = inject(DestroyRef);
3233

3334
readonly registry = select(RegistryOverviewSelectors.getRegistry);
3435
readonly registry$ = toObservable(select(RegistryOverviewSelectors.getRegistry));
@@ -48,28 +49,26 @@ export class RegistryComponent extends DataciteTrackerComponent {
4849
}
4950

5051
private setMetaTags(): void {
51-
const image = 'engines-dist/registries/assets/img/osf-sharing.png';
52-
53-
this.metaTags.updateMetaTagsForRoute(
52+
this.metaTags.updateMetaTags(
5453
{
5554
title: this.registry()?.title,
5655
description: this.registry()?.description,
5756
publishedDate: this.datePipe.transform(this.registry()?.dateRegistered, 'yyyy-MM-dd'),
5857
modifiedDate: this.datePipe.transform(this.registry()?.dateModified, 'yyyy-MM-dd'),
5958
url: pathJoin(environment.webUrl, this.registry()?.id ?? ''),
60-
image,
6159
identifier: this.registry()?.id,
6260
doi: this.registry()?.doi,
6361
keywords: this.registry()?.tags,
6462
siteName: 'OSF',
6563
license: this.registry()?.license?.name,
6664
contributors:
6765
this.registry()?.contributors?.map((contributor) => ({
66+
fullName: contributor.fullName,
6867
givenName: contributor.givenName,
6968
familyName: contributor.familyName,
7069
})) ?? [],
7170
},
72-
'registries'
71+
this.destroyRef,
7372
);
7473
}
7574
}
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export interface MetaTagAuthor {
2-
givenName: string;
3-
familyName: string;
2+
fullName?: string;
3+
givenName?: string;
4+
familyName?: string;
45
}

src/app/shared/services/meta-tags.service.ts

Lines changed: 27 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { DOCUMENT } from '@angular/common';
2-
import { Inject, Injectable } from '@angular/core';
2+
import { DestroyRef, Inject, Injectable } from '@angular/core';
33
import { Meta, MetaDefinition, Title } from '@angular/platform-browser';
44

55
import { Content, DataContent, HeadTagDef, MetaTagAuthor, MetaTagsData } from '../models/meta-tags';
@@ -14,7 +14,7 @@ export class MetaTagsService {
1414
type: 'article',
1515
description: 'Hosted on the OSF',
1616
language: 'en-US',
17-
image: `${environment.webUrl}/static/img/preprints_assets/osf/sharing.png`,
17+
image: `${environment.webUrl}/assets/images/osf-sharing.png`,
1818
imageType: 'image/png',
1919
imageWidth: 1200,
2020
imageHeight: 630,
@@ -27,25 +27,23 @@ export class MetaTagsService {
2727
};
2828

2929
private readonly metaTagClass = 'osf-dynamic-meta';
30-
private currentRouteGroup: string | null = null;
30+
31+
// data from all active routed components that set meta tags
32+
private metaTagStack: Array<{ metaTagsData: MetaTagsData; componentDestroyRef: DestroyRef }> = [];
3133

3234
constructor(
3335
private meta: Meta,
3436
private title: Title,
3537
@Inject(DOCUMENT) private document: Document
3638
) {}
3739

38-
updateMetaTags(metaTagsData: MetaTagsData): void {
39-
const combinedData = { ...this.defaultMetaTags, ...metaTagsData };
40-
const headTags = this.getHeadTags(combinedData);
41-
42-
this.applyHeadTags(headTags);
43-
this.dispatchZoteroEvent();
44-
}
45-
46-
updateMetaTagsForRoute(metaTagsData: MetaTagsData, routeGroup: string): void {
47-
this.currentRouteGroup = routeGroup;
48-
this.updateMetaTags(metaTagsData);
40+
updateMetaTags(metaTagsData: MetaTagsData, componentDestroyRef: DestroyRef): void {
41+
this.metaTagStack = [...this.metaTagStackWithout(componentDestroyRef), { metaTagsData, componentDestroyRef }];
42+
componentDestroyRef.onDestroy(() => {
43+
this.metaTagStack = this.metaTagStackWithout(componentDestroyRef);
44+
this.applyNearestMetaTags();
45+
});
46+
this.applyNearestMetaTags();
4947
}
5048

5149
clearMetaTags(): void {
@@ -62,27 +60,28 @@ export class MetaTagsService {
6260
});
6361

6462
this.title.setTitle(String(this.defaultMetaTags.siteName));
65-
this.currentRouteGroup = null;
6663
}
6764

68-
shouldClearMetaTags(newUrl: string): boolean {
69-
if (!this.currentRouteGroup) return true;
70-
return !newUrl.startsWith(`/${this.currentRouteGroup}`);
65+
private metaTagStackWithout(destroyRefToRemove: DestroyRef) {
66+
// get a copy of `this.metaTagStack` minus any entries with the given destroyRef
67+
return this.metaTagStack.filter(({ componentDestroyRef }) => componentDestroyRef !== destroyRefToRemove);
7168
}
7269

73-
clearMetaTagsIfNeeded(newUrl: string): void {
74-
if (this.shouldClearMetaTags(newUrl)) {
70+
private applyNearestMetaTags() {
71+
// apply the meta tags for the nearest active route that called `updateMetaTags` (if any)
72+
const nearest = this.metaTagStack.at(-1);
73+
if (nearest) {
74+
this.applyMetaTagsData(nearest.metaTagsData);
75+
} else {
7576
this.clearMetaTags();
7677
}
77-
}
78-
79-
resetToDefaults(): void {
80-
this.updateMetaTags({});
81-
}
78+
};
8279

83-
getHeadTagsPublic(metaTagsData: MetaTagsData): HeadTagDef[] {
80+
private applyMetaTagsData(metaTagsData: MetaTagsData) {
8481
const combinedData = { ...this.defaultMetaTags, ...metaTagsData };
85-
return this.getHeadTags(combinedData);
82+
const headTags = this.getHeadTags(combinedData);
83+
this.applyHeadTags(headTags);
84+
this.dispatchZoteroEvent();
8685
}
8786

8887
private getHeadTags(metaTagsData: MetaTagsData): HeadTagDef[] {
@@ -173,6 +172,7 @@ export class MetaTagsService {
173172
.filter((person): person is MetaTagAuthor => typeof person === 'object' && person !== null)
174173
.map((person) => ({
175174
'@type': 'schema:Person',
175+
name: person.fullName,
176176
givenName: person.givenName,
177177
familyName: person.familyName,
178178
}));

src/assets/i18n/en.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -956,6 +956,7 @@
956956
"title": "Files",
957957
"storageLocation": "OSF Storage",
958958
"searchPlaceholder": "Search your projects",
959+
"metaTagDescriptionPlaceholder": "Presented by OSF",
959960
"sort": {
960961
"placeholder": "Sort",
961962
"nameAZ": "Name: A-Z",

src/assets/images/osf-sharing.png

68.8 KB
Loading

0 commit comments

Comments
 (0)