Give the tech radar its own page

/radar (/en/radar, /ca/radar) shows every technology the collective has
placed, by ring: logo, category, what it is, why it sits there, its
website and how many projects use it. The landing's radar summary links
to it.

It is the first listing — a page this site composes rather than one
record's — so resolveRoute learns about those (LISTINGS), and builds
their addresses and hreflang alternates from the same table.

Logos sit on a light tile in both themes. A brand's mark is not ours to
recolour, and Rust's, which is black, would vanish in dark mode.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
pigui
2026-09-29 15:07:19 +02:00
parent b5681a12ac
commit 06eeefc144
11 changed files with 558 additions and 13 deletions

View File

@@ -5,7 +5,10 @@ import {
input,
} from '@angular/core';
import { copyFor } from '../core/copy';
import { copyFor, type Copy } from '../core/copy';
import { listingAlternates, listingPath } from '../core/routes';
import type { Locale } from '../core/api';
import { RadarPage } from './radar-page';
import { CallDetail } from './call-detail';
import type { RoutedPageData, RoutedRecord } from '../core/routed-page';
import { ContentPage } from './content-page';
@@ -20,7 +23,14 @@ import { ProjectDetail } from './project-detail';
@Component({
selector: 'app-routed-page',
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [CallDetail, ContentPage, MemberDetail, PageShell, ProjectDetail],
imports: [
CallDetail,
ContentPage,
MemberDetail,
PageShell,
ProjectDetail,
RadarPage,
],
template: `
<app-page-shell [data]="data()" [seo]="seo()">
@if (data().found; as found) {
@@ -47,6 +57,9 @@ import { ProjectDetail } from './project-detail';
[locale]="data().locale"
/>
}
@case ('radar') {
<app-radar-page [technologies]="found.record" [copy]="copy()" />
}
@case ('member') {
<app-member-detail
[member]="found.record"
@@ -64,11 +77,17 @@ export class RoutedPage {
protected readonly copy = computed(() => copyFor(this.data().locale));
protected readonly seo = computed(() => seoFor(this.data().found));
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): PageSeo | null {
export function seoFor(
found: RoutedRecord | null,
copy: Copy,
locale: Locale,
): PageSeo | null {
if (!found) {
return null;
}
@@ -107,6 +126,14 @@ export function seoFor(found: RoutedRecord | null): PageSeo | null {
type: 'profile',
};
}
case 'radar':
return {
title: `Pireon — ${copy.sections.radar.title}`,
description: copy.radar.lead,
path: listingPath('radar', locale),
alternates: listingAlternates('radar'),
type: 'website',
};
case 'call': {
const call = found.record;
const project = call.project?.title;