diff --git a/src/app/features/preprints/pages/preprint-details/preprint-details.component.ts b/src/app/features/preprints/pages/preprint-details/preprint-details.component.ts index 0f02c16dc..52e1e6ad3 100644 --- a/src/app/features/preprints/pages/preprint-details/preprint-details.component.ts +++ b/src/app/features/preprints/pages/preprint-details/preprint-details.component.ts @@ -35,6 +35,7 @@ import { FixSpecialCharPipe } from '@osf/shared/pipes/fix-special-char.pipe'; import { CustomDialogService } from '@osf/shared/services/custom-dialog.service'; import { DataciteService } from '@osf/shared/services/datacite/datacite.service'; import { MetaTagsService } from '@osf/shared/services/meta-tags.service'; +import { SignpostingService } from '@osf/shared/services/signposting.service'; import { ToastService } from '@osf/shared/services/toast.service'; import { ContributorsSelectors } from '@osf/shared/stores/contributors'; @@ -104,6 +105,7 @@ export class PreprintDetailsComponent implements OnInit, OnDestroy { private readonly prerenderReady = inject(PrerenderReadyService); private readonly platformId = inject(PLATFORM_ID); private readonly isBrowser = isPlatformBrowser(this.platformId); + private readonly signpostingService = inject(SignpostingService); private readonly environment = inject(ENVIRONMENT); @@ -304,6 +306,8 @@ export class PreprintDetailsComponent implements OnInit, OnDestroy { this.actions.getPreprintProviderById(this.providerId()); this.fetchPreprint(this.preprintId()); + this.signpostingService.addSignpostingHeaders(); + this.dataciteService.logIdentifiableView(this.preprint$).pipe(takeUntilDestroyed(this.destroyRef)).subscribe(); } @@ -413,6 +417,7 @@ export class PreprintDetailsComponent implements OnInit, OnDestroy { givenName: contributor.givenName, familyName: contributor.familyName, })), + signpostingLinks: this.signpostingService.mockSignpostingLinks, }, this.destroyRef ); diff --git a/src/app/shared/models/meta-tags/meta-tags-data.model.ts b/src/app/shared/models/meta-tags/meta-tags-data.model.ts index 20af37136..5f56e7da9 100644 --- a/src/app/shared/models/meta-tags/meta-tags-data.model.ts +++ b/src/app/shared/models/meta-tags/meta-tags-data.model.ts @@ -2,6 +2,13 @@ import { MetaTagAuthor } from './meta-tag-author.model'; export type Content = string | number | null | undefined | MetaTagAuthor; +export interface SignpostingLink { + rel: string; + href: string; + type?: string; + title?: string; +} + export type DataContent = Content | Content[]; export interface MetaTagsData { @@ -28,4 +35,5 @@ export interface MetaTagsData { twitterCreator?: DataContent; contributors?: DataContent; keywords?: DataContent; + signpostingLinks?: SignpostingLink[]; } diff --git a/src/app/shared/services/meta-tags.service.ts b/src/app/shared/services/meta-tags.service.ts index a3bc9e727..cc983a736 100644 --- a/src/app/shared/services/meta-tags.service.ts +++ b/src/app/shared/services/meta-tags.service.ts @@ -11,7 +11,7 @@ import { replaceBadEncodedChars } from '@osf/shared/helpers/format-bad-encoding. import { MetadataRecordFormat } from '../enums/metadata-record-format.enum'; import { HeadTagDef } from '../models/meta-tags/head-tag-def.model'; import { MetaTagAuthor } from '../models/meta-tags/meta-tag-author.model'; -import { Content, DataContent, MetaTagsData } from '../models/meta-tags/meta-tags-data.model'; +import { Content, DataContent, MetaTagsData, SignpostingLink } from '../models/meta-tags/meta-tags-data.model'; import { MetadataRecordsService } from './metadata-records.service'; @@ -123,6 +123,11 @@ export class MetaTagsService { this.prerenderReady.setNotReady(); const combinedData = { ...this.defaultMetaTags, ...metaTagsData }; const headTags = this.getHeadTags(combinedData); + + if (metaTagsData.signpostingLinks) { + headTags.push(...this.getSignpostingLinkTags(metaTagsData.signpostingLinks)); + } + of(metaTagsData.osfGuid) .pipe( switchMap((osfid) => @@ -231,6 +236,19 @@ export class MetaTagsService { .filter((tag) => tag.attrs.content); } + private getSignpostingLinkTags(signpostingLinks: SignpostingLink[]): HeadTagDef[] { + return signpostingLinks.map((link) => ({ + type: 'link' as const, + attrs: { + rel: link.rel, + href: link.href, + ...(link.type && { type: link.type }), + ...(link.title && { title: link.title }), + class: this.metaTagClass, + }, + })); + } + private buildMetaTagContent(name: string, content: Content): Content { if (['citation_author', 'dc.creator'].includes(name) && typeof content === 'object') { const author = content as MetaTagAuthor; diff --git a/src/app/shared/services/signposting.service.ts b/src/app/shared/services/signposting.service.ts new file mode 100644 index 000000000..dff51bb15 --- /dev/null +++ b/src/app/shared/services/signposting.service.ts @@ -0,0 +1,68 @@ +import { of } from 'rxjs'; + +import { inject, Injectable, RESPONSE_INIT } from '@angular/core'; + +import { SignpostingLink } from '../models/meta-tags/meta-tags-data.model'; + +@Injectable({ + providedIn: 'root', +}) +export class SignpostingService { + private readonly responseInit = inject(RESPONSE_INIT, { optional: true }); + + mockSignpostingLinks: SignpostingLink[] = [ + { + rel: 'describedby', + href: '/api/descriptions/project-123', + type: 'application/json', + }, + { + rel: 'cite-as', + href: 'https://doi.org/10.1234/example', + type: 'text/html', + }, + { + rel: 'item', + href: '/project/123/files/', + type: 'text/html', + title: 'Project Files', + }, + { + rel: 'collection', + href: '/user/projects/', + type: 'text/html', + title: 'User Projects', + }, + ]; + + addSignpostingHeaders(): void { + of(this.mockSignpostingLinks).subscribe({ + next: (links) => { + if (!this.responseInit || !this.responseInit.headers) { + return; + } + + const headers = + this.responseInit?.headers instanceof Headers + ? this.responseInit.headers + : new Headers(this.responseInit?.headers); + + const linkHeader = this.formatLinkHeader(links); + headers.set('Link', linkHeader); + + this.responseInit.headers = headers; + }, + }); + } + + formatLinkHeader(links: SignpostingLink[]): string { + return links + .map((link) => { + const parts = [`<${link.href}>`, `rel="${link.rel}"`]; + if (link.type) parts.push(`type="${link.type}"`); + if (link.title) parts.push(`title="${link.title}"`); + return parts.join('; '); + }) + .join(', '); + } +}