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
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,10 @@ export class FileDetailComponent {
cedarTemplates = select(MetadataSelectors.getCedarTemplates);
isAnonymous = select(FilesSelectors.isFilesAnonymous);
fileCustomMetadata = select(FilesSelectors.getFileCustomMetadata);
isFileCustomMetadataLoading = select(FilesSelectors.isFileMetadataLoading);
resourceMetadata = select(FilesSelectors.getResourceMetadata);
resourceContributors = select(FilesSelectors.getContributors);
isResourceContributorsLoading = select(FilesSelectors.isResourceContributorsLoading);

safeLink: SafeResourceUrl | null = null;
resourceId = '';
Expand Down Expand Up @@ -184,10 +186,15 @@ export class FileDetailComponent {
});

private readonly metaTagsData = computed(() => {
if (this.isFileLoading() || this.isFileCustomMetadataLoading() || this.isResourceContributorsLoading()) {
return null;
}
const file = this.file();
if (!file) return null;
return {
osfGuid: file.guid,
title: this.fileCustomMetadata()?.title || file.name,
type: this.fileCustomMetadata()?.resourceTypeGeneral,
description:
this.fileCustomMetadata()?.description ?? this.translateService.instant('files.metaTagDescriptionPlaceholder'),
url: pathJoin(environment.webUrl, this.fileGuid),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { ShareAndDownloadComponent } from '@osf/features/preprints/components/pr
import { PreprintSelectors } from '@osf/features/preprints/store/preprint';
import { PreprintProvidersSelectors } from '@osf/features/preprints/store/preprint-providers';
import { MOCK_PROVIDER, MOCK_STORE, TranslateServiceMock } from '@shared/mocks';
import { MetaTagsService } from '@shared/services';
import { DataciteService } from '@shared/services/datacite/datacite.service';

import { PreprintDetailsComponent } from './preprint-details.component';
Expand Down Expand Up @@ -71,6 +72,7 @@ describe('PreprintDetailsComponent', () => {
MockProvider(Router),
MockProvider(ActivatedRoute, mockRoute),
TranslateServiceMock,
MockProvider(MetaTagsService),
],
}).compileComponents();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -361,12 +361,12 @@ export class PreprintDetailsComponent implements OnInit, OnDestroy {
private setMetaTags() {
this.metaTags.updateMetaTags(
{
osfGuid: this.preprint()?.id,
title: this.preprint()?.title,
description: this.preprint()?.description,
publishedDate: this.datePipe.transform(this.preprint()?.datePublished, 'yyyy-MM-dd'),
modifiedDate: this.datePipe.transform(this.preprint()?.dateModified, 'yyyy-MM-dd'),
url: pathJoin(environment.webUrl, this.preprint()?.id ?? ''),
identifier: this.preprint()?.id,
doi: this.preprint()?.doi,
keywords: this.preprint()?.tags,
siteName: 'OSF',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { provideHttpClientTesting } from '@angular/common/http/testing';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ActivatedRoute } from '@angular/router';

import { ToastService } from '@osf/shared/services';
import { MetaTagsService, ToastService } from '@osf/shared/services';
import { GetActivityLogs } from '@shared/stores/activity-logs';

import { ProjectOverviewComponent } from './project-overview.component';
Expand All @@ -37,6 +37,7 @@ describe('ProjectOverviewComponent', () => {
{ provide: DialogService, useValue: { open: () => ({ onClose: of(null) }) } },
{ provide: TranslateService, useValue: { instant: (k: string) => k } },
{ provide: ToastService, useValue: { showSuccess: jest.fn() } },
{ provide: MetaTagsService, useValue: { updateMetaTags: jest.fn() } },
],
}).compileComponents();

Expand Down
43 changes: 40 additions & 3 deletions src/app/features/project/overview/project-overview.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { DialogService } from 'primeng/dynamicdialog';
import { Message } from 'primeng/message';
import { TagModule } from 'primeng/tag';

import { CommonModule } from '@angular/common';
import { CommonModule, DatePipe } from '@angular/common';
import {
ChangeDetectionStrategy,
Component,
Expand All @@ -32,7 +32,7 @@ import {
import { Mode, ResourceType, UserPermissions } from '@osf/shared/enums';
import { hasViewOnlyParam, IS_XSMALL } from '@osf/shared/helpers';
import { MapProjectOverview } from '@osf/shared/mappers';
import { ToastService } from '@osf/shared/services';
import { MetaTagsService, ToastService } from '@osf/shared/services';
import {
ClearCollections,
ClearWiki,
Expand Down Expand Up @@ -95,7 +95,7 @@ import {
ViewOnlyLinkMessageComponent,
ViewOnlyLinkMessageComponent,
],
providers: [DialogService],
providers: [DialogService, DatePipe],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ProjectOverviewComponent implements OnInit {
Expand All @@ -108,6 +108,8 @@ export class ProjectOverviewComponent implements OnInit {
private readonly dialogService = inject(DialogService);
private readonly translateService = inject(TranslateService);
private readonly dataciteService = inject(DataciteService);
private readonly metaTags = inject(MetaTagsService);
private readonly datePipe = inject(DatePipe);

isMobile = toSignal(inject(IS_XSMALL));
submissions = select(CollectionsModerationSelectors.getCollectionSubmissions);
Expand Down Expand Up @@ -210,6 +212,41 @@ export class ProjectOverviewComponent implements OnInit {
};
});

private readonly effectMetaTags = effect(() => {
if (!this.isProjectLoading()) {
const metaTagsData = this.metaTagsData();
if (metaTagsData) {
this.metaTags.updateMetaTags(metaTagsData, this.destroyRef);
}
}
});

private readonly metaTagsData = computed(() => {
const project = this.currentProject();
if (!project) return null;
const keywords = [...(project.tags || [])];
if (project.category) {
keywords.push(project.category);
}
return {
osfGuid: project.id,
title: project.title,
description: project.description,
url: project.links?.iri,
doi: project.doi,
license: project.license?.name,
publishedDate: this.datePipe.transform(project.dateCreated, 'yyyy-MM-dd'),
modifiedDate: this.datePipe.transform(project.dateModified, 'yyyy-MM-dd'),
keywords,
institution: project.affiliatedInstitutions?.map((institution) => institution.name),
contributors: project.contributors.map((contributor) => ({
fullName: contributor.fullName,
givenName: contributor.givenName,
familyName: contributor.familyName,
})),
};
});

constructor() {
this.setupCollectionsEffects();
this.setupCleanup();
Expand Down
4 changes: 3 additions & 1 deletion src/app/features/registry/registry.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,12 @@ export class RegistryComponent {
private readonly destroyRef = inject(DestroyRef);

readonly registry = select(RegistryOverviewSelectors.getRegistry);
readonly isRegistryLoading = select(RegistryOverviewSelectors.isRegistryLoading);
readonly registry$ = toObservable(select(RegistryOverviewSelectors.getRegistry));

constructor() {
effect(() => {
if (this.registry()) {
if (!this.isRegistryLoading() && this.registry()) {
this.setMetaTags();
}
});
Expand All @@ -44,6 +45,7 @@ export class RegistryComponent {
private setMetaTags(): void {
this.metaTags.updateMetaTags(
{
osfGuid: this.registry()?.id,
title: this.registry()?.title,
description: this.registry()?.description,
publishedDate: this.datePipe.transform(this.registry()?.dateRegistered, 'yyyy-MM-dd'),
Expand Down
1 change: 1 addition & 0 deletions src/app/shared/enums/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export * from './field-type.enum';
export * from './file-menu-type.enum';
export * from './filter-type.enum';
export * from './get-resources-request-type.enum';
export * from './metadata-record-format.enum';
export * from './metadata-resource.enum';
export * from './mode.enum';
export * from './moderation-decision-form-controls.enum';
Expand Down
8 changes: 8 additions & 0 deletions src/app/shared/enums/metadata-record-format.enum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// metadata formats available from osf backend -- see METADATA_SERIALIZER_REGISTRY:
// https://github.com/CenterForOpenScience/osf.io/blob/develop/osf/metadata/serializers/__init__.py
export enum MetadataRecordFormat {
Turtle = 'turtle',
DataciteJson = 'datacite-json',
DataciteXml = 'datacite-xml',
SchemaDotOrgDataset = 'google-dataset-json-ld',
}
1 change: 1 addition & 0 deletions src/app/shared/models/meta-tags/meta-tags-data.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export type Content = string | number | null | undefined | MetaTagAuthor;
export type DataContent = Content | Content[];

export interface MetaTagsData {
osfGuid?: string;
title?: DataContent;
type?: DataContent;
description?: DataContent;
Expand Down
1 change: 1 addition & 0 deletions src/app/shared/services/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export { JsonApiService } from './json-api.service';
export { LicensesService } from './licenses.service';
export { LoaderService } from './loader.service';
export { MetaTagsService } from './meta-tags.service';
export { MetadataRecordsService } from './metadata-records.service';
export { MyResourcesService } from './my-resources.service';
export { NodeLinksService } from './node-links.service';
export { ProjectRedirectDialogService } from './project-redirect-dialog.service';
Expand Down
87 changes: 44 additions & 43 deletions src/app/shared/services/meta-tags.service.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { catchError, map, Observable, of, switchMap, tap } from 'rxjs';

import { DOCUMENT } from '@angular/common';
import { DestroyRef, Inject, Injectable } from '@angular/core';
import { DestroyRef, Inject, inject, Injectable } from '@angular/core';
import { Meta, MetaDefinition, Title } from '@angular/platform-browser';

import { MetadataRecordFormat } from '@osf/shared/enums';
import { MetadataRecordsService } from '@osf/shared/services';

import { Content, DataContent, HeadTagDef, MetaTagAuthor, MetaTagsData } from '../models/meta-tags';

import { environment } from 'src/environments/environment';
Expand All @@ -10,6 +15,8 @@ import { environment } from 'src/environments/environment';
providedIn: 'root',
})
export class MetaTagsService {
metadataRecords: MetadataRecordsService = inject(MetadataRecordsService);

private readonly defaultMetaTags: MetaTagsData = {
type: 'article',
description: 'Hosted on the OSF',
Expand Down Expand Up @@ -80,13 +87,42 @@ export class MetaTagsService {
private applyMetaTagsData(metaTagsData: MetaTagsData) {
const combinedData = { ...this.defaultMetaTags, ...metaTagsData };
const headTags = this.getHeadTags(combinedData);
this.applyHeadTags(headTags);
this.dispatchZoteroEvent();
of(metaTagsData.osfGuid)
.pipe(
switchMap(
(osfid) =>
osfid // with an osf id, try getting schema.org json-ld from backend
? this.getSchemaDotOrgJsonLdHeadTag(osfid).pipe(
tap((jsonLdHeadTag) => {
if (jsonLdHeadTag) {
headTags.push(jsonLdHeadTag);
}
}),
catchError(() => of(null)) // if it doesn't work, ignore and continue with given head tags
)
: of(null) // without osfid, continue with only given head tags
),
tap(() => this.applyHeadTags(headTags)),
tap(() => this.dispatchZoteroEvent())
)
.subscribe();
}

private getHeadTags(metaTagsData: MetaTagsData): HeadTagDef[] {
const headTags: HeadTagDef[] = [];
private getSchemaDotOrgJsonLdHeadTag(osfid: string): Observable<HeadTagDef | null> {
return this.metadataRecords.getMetadataRecord(osfid, MetadataRecordFormat.SchemaDotOrgDataset).pipe(
map((jsonLd) =>
jsonLd
? {
type: 'script' as const,
attrs: { type: 'application/ld+json' },
content: jsonLd,
}
: null
)
);
}

private getHeadTags(metaTagsData: MetaTagsData): HeadTagDef[] {
const identifiers = this.toArray(metaTagsData.url)
.concat(this.toArray(metaTagsData.doi))
.concat(this.toArray(metaTagsData.identifier));
Expand All @@ -112,7 +148,7 @@ export class MetaTagsService {
'dct.created': metaTagsData.publishedDate,
'dc.publisher': metaTagsData.siteName,
'dc.language': metaTagsData.language,
'dc.contributor': metaTagsData.contributors,
'dc.creator': metaTagsData.contributors,
'dc.subject': metaTagsData.keywords,

// Open Graph/Facebook
Expand Down Expand Up @@ -140,7 +176,7 @@ export class MetaTagsService {
'twitter:image:alt': metaTagsData.imageAlt,
};

const metaTagsHeadTags = Object.entries(metaTagsDefs)
return Object.entries(metaTagsDefs)
.reduce((acc: HeadTagDef[], [name, content]) => {
if (content) {
const contentArray = this.toArray(content);
Expand All @@ -156,45 +192,10 @@ export class MetaTagsService {
return acc;
}, [])
.filter((tag) => tag.attrs.content);

headTags.push(...metaTagsHeadTags);

if (metaTagsData.contributors) {
headTags.push(this.buildPersonScriptTag(metaTagsData.contributors));
}

return headTags;
}

private buildPersonScriptTag(contributors: DataContent): HeadTagDef {
const contributorArray = this.toArray(contributors);
const contributor = contributorArray
.filter((person): person is MetaTagAuthor => typeof person === 'object' && person !== null)
.map((person) => ({
'@type': 'schema:Person',
name: person.fullName,
givenName: person.givenName,
familyName: person.familyName,
}));

return {
type: 'script',
content: JSON.stringify({
'@context': {
dc: 'http://purl.org/dc/elements/1.1/',
schema: 'http://schema.org',
},
'@type': 'schema:CreativeWork',
contributor,
}),
attrs: {
type: 'application/ld+json',
},
};
}

private buildMetaTagContent(name: string, content: Content): Content {
if (['citation_author', 'dc.contributor'].includes(name) && typeof content === 'object') {
if (['citation_author', 'dc.creator'].includes(name) && typeof content === 'object') {
const author = content as MetaTagAuthor;
return `${author.familyName}, ${author.givenName}`;
}
Expand Down
23 changes: 23 additions & 0 deletions src/app/shared/services/metadata-records.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Observable } from 'rxjs';

import { HttpClient } from '@angular/common/http';
import { inject, Injectable } from '@angular/core';

import { MetadataRecordFormat } from '../enums';

import { environment } from 'src/environments/environment';

@Injectable({
providedIn: 'root',
})
export class MetadataRecordsService {
private readonly http: HttpClient = inject(HttpClient);

metadataRecordUrl(osfid: string, format: MetadataRecordFormat): string {
return `${environment.webUrl}/metadata/${osfid}/?format=${format}`;
}

getMetadataRecord(osfid: string, format: MetadataRecordFormat): Observable<string> {
return this.http.get(this.metadataRecordUrl(osfid, format), { responseType: 'text' });
}
}