import { ChangeDetectionStrategy, Component, computed, input, } from '@angular/core'; import { copyFor, type Copy } from '../core/copy'; import { listingAlternates, listingPath } from '../core/routes'; import { memberLinks } from './member-card'; import { isBlogPost } from '../core/blog'; import type { Locale } from '../core/api'; import { RadarPage } from './radar-page'; import { ServiceDetail } from './service-detail'; import { BlogPage } from './blog-page'; import { CallDetail } from './call-detail'; import type { RoutedPageData, RoutedRecord } from '../core/routed-page'; import { ContentPage } from './content-page'; import { ContributePage } from './contribute-page'; import { MemberDetail } from './member-detail'; import { PageShell, type PageSeo } from './page-shell'; import { ProjectDetail } from './project-detail'; /** * A page reached by its address, whatever it holds: the shell around it, and * the body for its type inside. */ @Component({ selector: 'app-routed-page', changeDetection: ChangeDetectionStrategy.OnPush, imports: [ BlogPage, CallDetail, ContentPage, ContributePage, MemberDetail, PageShell, ProjectDetail, RadarPage, ServiceDetail, ], template: ` @if (data().found; as found) { @switch (found.type) { @case ('page') { } @case ('project') { } @case ('call') { } @case ('service') { } @case ('blog') { } @case ('contribute') { } @case ('radar') { } @case ('member') { } } } `, }) export class RoutedPage { readonly data = input.required(); protected readonly copy = computed(() => copyFor(this.data().locale)); protected readonly seo = computed(() => seoFor(this.data().found, this.copy(), this.data().locale), ); } /** What each type puts in the document head. Null when nothing was found. */ export function seoFor( found: RoutedRecord | null, copy: Copy, locale: Locale, ): PageSeo | null { if (!found) { return null; } switch (found.type) { case 'page': { const page = found.record; const post = isBlogPost(page, locale); return { title: page.meta_title || `Pireon — ${page.title}`, description: page.meta_description || page.excerpt || '', path: page.path, alternates: page.alternates ?? {}, type: 'article', image: page.featured_image?.url, feed: post ? `/blog/rss.xml?lang=${locale}` : undefined, jsonLd: post ? { '@type': 'BlogPosting', headline: page.title, description: page.excerpt, inLanguage: page.locale, datePublished: page.published_at, dateModified: page.updated_at, image: page.featured_image?.url, publisher: { '@type': 'Organization', name: 'Pireon' }, } : { '@type': 'WebPage', name: page.title, description: page.excerpt, inLanguage: page.locale, }, }; } case 'project': { const project = found.record; return { title: project.meta_title || `Pireon — ${project.title}`, description: project.meta_description || project.tagline || '', path: project.path, alternates: project.alternates ?? {}, type: 'article', image: project.cover_image?.url, jsonLd: { '@type': 'CreativeWork', name: project.title, description: project.tagline, inLanguage: project.locale, dateCreated: project.started_at, dateModified: project.updated_at, keywords: (project.technologies ?? []) .map((technology) => technology.name) .join(', ') || undefined, creator: (project.members ?? []).map((member) => ({ '@type': 'Person', name: member.name, })), ...(project.client ? { funder: { '@type': 'Organization', name: project.client.name } } : {}), }, }; } case 'member': { const member = found.record; return { title: `Pireon — ${member.name}`, description: member.headline || member.bio?.slice(0, 160) || '', path: member.path, alternates: member.alternates ?? {}, type: 'profile', image: member.avatar?.url, jsonLd: { '@type': 'Person', name: member.name, jobTitle: member.headline, description: member.bio, image: member.avatar?.url, sameAs: memberLinks(member).map((link) => link.url), memberOf: { '@type': 'Organization', name: 'Pireon' }, }, }; } case 'service': { const service = found.record; return { title: service.meta_title || `Pireon — ${service.title}`, description: service.meta_description || service.tagline || '', path: service.path, alternates: service.alternates ?? {}, type: 'article', jsonLd: { '@type': 'Service', name: service.title, description: service.tagline, provider: { '@type': 'Organization', name: 'Pireon' }, }, }; } case 'blog': return { title: `Pireon — ${copy.blog.title}`, description: copy.blog.lead, path: listingPath('blog', locale), alternates: listingAlternates('blog'), type: 'website', feed: `/blog/rss.xml?lang=${locale}`, jsonLd: { '@type': 'Blog', name: copy.blog.title, description: copy.blog.lead, }, }; case 'contribute': return { title: `Pireon — ${copy.contribute.title}`, description: copy.contribute.lead, path: listingPath('contribute', locale), alternates: listingAlternates('contribute'), type: 'website', jsonLd: { '@type': 'WebPage', name: copy.contribute.title, description: copy.contribute.lead, }, }; case 'radar': return { title: `Pireon — ${copy.sections.radar.title}`, description: copy.radar.lead, path: listingPath('radar', locale), alternates: listingAlternates('radar'), type: 'website', jsonLd: { '@type': 'WebPage', name: copy.sections.radar.title, description: copy.radar.lead, }, }; case 'call': { const call = found.record; const project = call.project?.title; return { title: `Pireon — ${call.title}${project ? ` · ${project}` : ''}`, description: call.description?.slice(0, 160) || '', path: call.path, alternates: call.alternates ?? {}, type: 'article', image: call.project?.cover_image?.url, jsonLd: { '@type': 'WebPage', name: call.title, description: call.description, }, }; } } }