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,
},
};
}
}
}