diff --git a/src/app/features/files/pages/file-detail/file-detail.component.spec.ts b/src/app/features/files/pages/file-detail/file-detail.component.spec.ts index 551a56853..f9500eea8 100644 --- a/src/app/features/files/pages/file-detail/file-detail.component.spec.ts +++ b/src/app/features/files/pages/file-detail/file-detail.component.spec.ts @@ -41,8 +41,8 @@ describe('FileDetailComponent', () => { } as unknown as jest.Mocked; const mockRoute: Partial = { - params: of({ providerId: 'osf', preprintId: 'p1' }), - queryParams: of({ providerId: 'osf', preprintId: 'p1' }), + params: of({ providerId: 'osf', fileGuid: 'file-1' }), + queryParams: of({ providerId: 'osf', fileGuid: 'file-1' }), }; (MOCK_STORE.selectSignal as jest.Mock).mockImplementation((selector) => { switch (selector) { @@ -79,6 +79,7 @@ describe('FileDetailComponent', () => { }).compileComponents(); fixture = TestBed.createComponent(FileDetailComponent); component = fixture.componentInstance; + document.head.innerHTML = ''; fixture.detectChanges(); }); @@ -95,4 +96,15 @@ describe('FileDetailComponent', () => { it('should call dataciteService.logIdentifiableView on start ', () => { expect(dataciteService.logIdentifiableView).toHaveBeenCalledWith(component.fileMetadata$); }); + + it('should add signposting tags during SSR', () => { + fixture.detectChanges(); + + const linkTags = Array.from(document.head.querySelectorAll('link[rel="linkset"]')); + expect(linkTags.length).toBe(2); + expect(linkTags[0].getAttribute('href')).toBe('http://localhost:4200/metadata/file-1/?format=linkset'); + expect(linkTags[0].getAttribute('type')).toBe('application/linkset'); + expect(linkTags[1].getAttribute('href')).toBe('http://localhost:4200/metadata/file-1/?format=linkset%2Bjson'); + expect(linkTags[1].getAttribute('type')).toBe('application/linkset+json'); + }); }); diff --git a/src/app/features/files/pages/file-detail/file-detail.component.ts b/src/app/features/files/pages/file-detail/file-detail.component.ts index 9dc06ff04..80afa822d 100644 --- a/src/app/features/files/pages/file-detail/file-detail.component.ts +++ b/src/app/features/files/pages/file-detail/file-detail.component.ts @@ -19,6 +19,7 @@ import { effect, HostBinding, inject, + OnInit, signal, } from '@angular/core'; import { takeUntilDestroyed, toObservable } from '@angular/core/rxjs-interop'; @@ -47,6 +48,7 @@ import { pathJoin } from '@osf/shared/helpers/path-join.helper'; import { CustomConfirmationService } from '@osf/shared/services/custom-confirmation.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 { ViewOnlyLinkHelperService } from '@osf/shared/services/view-only-link-helper.service'; import { FileDetailsModel } from '@shared/models/files/file.model'; @@ -94,7 +96,7 @@ import { changeDetection: ChangeDetectionStrategy.OnPush, providers: [DatePipe], }) -export class FileDetailComponent { +export class FileDetailComponent implements OnInit { @HostBinding('class') classes = 'flex flex-column flex-1 w-full h-full'; readonly store = inject(Store); @@ -111,6 +113,7 @@ export class FileDetailComponent { private readonly translateService = inject(TranslateService); private readonly environment = inject(ENVIRONMENT); private readonly clipboard = inject(Clipboard); + private readonly signpostingService = inject(SignpostingService); readonly dataciteService = inject(DataciteService); @@ -284,6 +287,10 @@ export class FileDetailComponent { this.dataciteService.logIdentifiableView(this.fileMetadata$).pipe(takeUntilDestroyed(this.destroyRef)).subscribe(); } + ngOnInit(): void { + this.signpostingService.addSignposting(this.fileGuid); + } + getIframeLink(version: string) { const url = this.getMfrUrlWithVersion(version); if (url) { diff --git a/src/app/features/preprints/pages/preprint-details/preprint-details.component.spec.ts b/src/app/features/preprints/pages/preprint-details/preprint-details.component.spec.ts index 1fbcfc53f..354f5a204 100644 --- a/src/app/features/preprints/pages/preprint-details/preprint-details.component.spec.ts +++ b/src/app/features/preprints/pages/preprint-details/preprint-details.component.spec.ts @@ -466,6 +466,7 @@ describe('PreprintDetailsComponent SSR Tests', () => { store = TestBed.inject(Store); fixture = TestBed.createComponent(PreprintDetailsComponent); component = fixture.componentInstance; + document.head.innerHTML = ''; }); it('should render PreprintDetailsComponent server-side without errors', () => { @@ -475,6 +476,17 @@ describe('PreprintDetailsComponent SSR Tests', () => { expect(component).toBeTruthy(); }); + it('should add signposting tags during SSR', () => { + fixture.detectChanges(); + + const linkTags = Array.from(document.head.querySelectorAll('link[rel="linkset"]')); + expect(linkTags.length).toBe(2); + expect(linkTags[0].getAttribute('href')).toBe('http://localhost:4200/metadata/preprint-1/?format=linkset'); + expect(linkTags[0].getAttribute('type')).toBe('application/linkset'); + expect(linkTags[1].getAttribute('href')).toBe('http://localhost:4200/metadata/preprint-1/?format=linkset%2Bjson'); + expect(linkTags[1].getAttribute('type')).toBe('application/linkset+json'); + }); + it('should not access browser-only APIs during SSR', () => { const platformId = TestBed.inject(PLATFORM_ID); expect(platformId).toBe('server'); 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..abb998716 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.addSignposting(this.preprintId()); + this.dataciteService.logIdentifiableView(this.preprint$).pipe(takeUntilDestroyed(this.destroyRef)).subscribe(); } diff --git a/src/app/features/project/overview/project-overview.component.spec.ts b/src/app/features/project/overview/project-overview.component.spec.ts index 61373020d..cb492aa78 100644 --- a/src/app/features/project/overview/project-overview.component.spec.ts +++ b/src/app/features/project/overview/project-overview.component.spec.ts @@ -262,6 +262,7 @@ describe('ProjectOverviewComponent SSR Tests', () => { store = TestBed.inject(Store); fixture = TestBed.createComponent(ProjectOverviewComponent); component = fixture.componentInstance; + document.head.innerHTML = ''; }); it('should render ProjectOverviewComponent server-side without errors', () => { @@ -285,6 +286,17 @@ describe('ProjectOverviewComponent SSR Tests', () => { expect(component).toBeTruthy(); }); + it('should add signposting tags during SSR', () => { + fixture.detectChanges(); + + const linkTags = Array.from(document.head.querySelectorAll('link[rel="linkset"]')); + expect(linkTags.length).toBe(2); + expect(linkTags[0].getAttribute('href')).toBe('http://localhost:4200/metadata/project-123/?format=linkset'); + expect(linkTags[0].getAttribute('type')).toBe('application/linkset'); + expect(linkTags[1].getAttribute('href')).toBe('http://localhost:4200/metadata/project-123/?format=linkset%2Bjson'); + expect(linkTags[1].getAttribute('type')).toBe('application/linkset+json'); + }); + it('should not call browser-only actions in ngOnDestroy during SSR', () => { const dispatchSpy = jest.spyOn(store, 'dispatch'); diff --git a/src/app/features/project/overview/project-overview.component.ts b/src/app/features/project/overview/project-overview.component.ts index 209c33834..7b9324e14 100644 --- a/src/app/features/project/overview/project-overview.component.ts +++ b/src/app/features/project/overview/project-overview.component.ts @@ -35,6 +35,7 @@ import { ViewOnlyLinkMessageComponent } from '@osf/shared/components/view-only-l import { Mode } from '@osf/shared/enums/mode.enum'; import { ResourceType } from '@osf/shared/enums/resource-type.enum'; import { CustomDialogService } from '@osf/shared/services/custom-dialog.service'; +import { SignpostingService } from '@osf/shared/services/signposting.service'; import { ToastService } from '@osf/shared/services/toast.service'; import { ViewOnlyLinkHelperService } from '@osf/shared/services/view-only-link-helper.service'; import { @@ -104,6 +105,7 @@ export class ProjectOverviewComponent implements OnInit { private readonly customDialogService = inject(CustomDialogService); private readonly platformId = inject(PLATFORM_ID); private readonly isBrowser = isPlatformBrowser(this.platformId); + private readonly signpostingService = inject(SignpostingService); submissions = select(CollectionsModerationSelectors.getCollectionSubmissions); collectionProvider = select(CollectionsSelectors.getCollectionProvider); @@ -193,6 +195,7 @@ export class ProjectOverviewComponent implements OnInit { this.actions.getBookmarksId(); this.actions.getComponents(projectId); this.actions.getLinkedProjects(projectId); + this.signpostingService.addSignposting(projectId); } } diff --git a/src/app/features/registry/pages/registry-overview/registry-overview.component.spec.ts b/src/app/features/registry/pages/registry-overview/registry-overview.component.spec.ts index ed58b1a97..35a8671aa 100644 --- a/src/app/features/registry/pages/registry-overview/registry-overview.component.spec.ts +++ b/src/app/features/registry/pages/registry-overview/registry-overview.component.spec.ts @@ -629,6 +629,7 @@ describe('RegistryOverviewComponent', () => { }).compileComponents(); fixture = TestBed.createComponent(RegistryOverviewComponent); component = fixture.componentInstance; + document.head.innerHTML = ''; }); it('should render server-side without errors', () => { @@ -638,6 +639,16 @@ describe('RegistryOverviewComponent', () => { expect(component).toBeTruthy(); }); + it('should add signposting tags during SSR', () => { + fixture.detectChanges(); + const linkTags = Array.from(document.head.querySelectorAll('link[rel="linkset"]')); + expect(linkTags.length).toBe(2); + expect(linkTags[0].getAttribute('href')).toBe('http://localhost:4200/metadata/registry-1/?format=linkset'); + expect(linkTags[0].getAttribute('type')).toBe('application/linkset'); + expect(linkTags[1].getAttribute('href')).toBe('http://localhost:4200/metadata/registry-1/?format=linkset%2Bjson'); + expect(linkTags[1].getAttribute('type')).toBe('application/linkset+json'); + }); + it('should not access browser-only APIs during SSR', () => { const platformId = TestBed.inject(PLATFORM_ID); expect(platformId).toBe('server'); diff --git a/src/app/features/registry/pages/registry-overview/registry-overview.component.ts b/src/app/features/registry/pages/registry-overview/registry-overview.component.ts index 69466b6e5..d6199402c 100644 --- a/src/app/features/registry/pages/registry-overview/registry-overview.component.ts +++ b/src/app/features/registry/pages/registry-overview/registry-overview.component.ts @@ -15,6 +15,7 @@ import { effect, HostBinding, inject, + OnInit, signal, } from '@angular/core'; import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop'; @@ -32,6 +33,7 @@ import { toCamelCase } from '@osf/shared/helpers/camel-case'; import { SchemaResponse } from '@osf/shared/models/registration/schema-response.model'; import { CustomDialogService } from '@osf/shared/services/custom-dialog.service'; import { LoaderService } from '@osf/shared/services/loader.service'; +import { SignpostingService } from '@osf/shared/services/signposting.service'; import { ToastService } from '@osf/shared/services/toast.service'; import { ViewOnlyLinkHelperService } from '@osf/shared/services/view-only-link-helper.service'; import { GetBookmarksCollectionId } from '@osf/shared/stores/bookmarks'; @@ -75,7 +77,7 @@ import { styleUrl: './registry-overview.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, }) -export class RegistryOverviewComponent { +export class RegistryOverviewComponent implements OnInit { @HostBinding('class') classes = 'flex-1 flex flex-column w-full h-full'; private readonly route = inject(ActivatedRoute); private readonly router = inject(Router); @@ -84,6 +86,7 @@ export class RegistryOverviewComponent { private readonly viewOnlyService = inject(ViewOnlyLinkHelperService); private readonly customDialogService = inject(CustomDialogService); private readonly loaderService = inject(LoaderService); + private readonly signpostingService = inject(SignpostingService); readonly registry = select(RegistrySelectors.getRegistry); readonly isRegistryLoading = select(RegistrySelectors.isRegistryLoading); @@ -169,6 +172,10 @@ export class RegistryOverviewComponent { .subscribe(); } + ngOnInit(): void { + this.signpostingService.addSignposting(this.registryId()); + } + openRevision(revisionIndex: number): void { this.selectedRevisionIndex.set(revisionIndex); }