import { accentHex } from "./accent"; import type { ChannelGroup } from "./channelGroups"; import { CONTRACT } from "./corpus"; import type { Manifest } from "./manifest"; import type { Site } from "./site"; import type { SocialLink } from "./settings"; // The public site descriptor served at `/site.json` on every export bundle. // // This is the federation contract: a hub PWA reads this from any export-site // origin (same-origin for the built-in pool, CORS cross-origin for user-added // externals) to learn a site's branding, channels, and freshness WITHOUT a // second roundtrip. It is emitted regardless of whether the instance ships a // PWA — a "dumb instance" (no service worker / not installable) is still fully // federatable through this file. // // Deliberately excludes operational/internal config (cloudflareProject, // relatedSites): only public presentation fields belong here. export const SITE_DESCRIPTOR_VERSION = CONTRACT.siteDescriptor; // One channel a site exposes, with the slug that gives it a collision-free // identity across sites (display names collide, slugs don't). export type PublicChannel = { slug: string; name: string; count: number; groupId?: string; }; export type PublicSiteDescriptor = { // Contract version — a hub rejects a descriptor whose contract it doesn't // understand. Bump when the shape changes incompatibly. contract: number; siteId: string; siteTitle: string; siteDescription: string; headerTitle: string; homeTagline: string; accent?: string; // Absolute public URL of this deployment (self-reference), when configured. siteUrl?: string; // The intended hub parent this site belongs under (resolved: site override // ?? family default). Lets a standalone site link to its hub and lets a hub // tell member sites (that name it) from arbitrary added origins. hubUrl?: string; // Whether this instance ships an installable PWA (service worker + manifest). // A hub can badge federated sites that are independently installable. pwa: boolean; // Resolved social links (site override ?? global default). socialLinks: SocialLink[]; groups: ChannelGroup[]; defaultGroupId: string; channels: PublicChannel[]; // Mirror of the summaries manifest's generatedAt — the freshness/staleness // signal a hub uses to invalidate cached cross-origin shards. generatedAt: string; // The summaries MANIFEST_VERSION at build time, so a hub can detect a shard // shape it doesn't understand. summariesVersion: number; }; // Build the public descriptor from a Site plus the site's just-composed // summaries manifest (source of channels/groups/generatedAt) and the resolved // social links. `pwa`/`hubUrl` are resolved by the caller (they depend on // build-time config + settings) and passed in. export function buildSiteDescriptor( site: Site, manifest: Manifest, socialLinks: SocialLink[], opts: { pwa: boolean; hubUrl?: string }, ): PublicSiteDescriptor { const channels: PublicChannel[] = manifest.channels .filter((c): c is typeof c & { slug: string } => typeof c.slug === "string") .map((c) => ({ slug: c.slug, name: c.name, count: c.count, ...(c.groupId ? { groupId: c.groupId } : {}), })); return { contract: SITE_DESCRIPTOR_VERSION, siteId: site.siteId, siteTitle: site.siteTitle, siteDescription: site.siteDescription, headerTitle: site.headerTitle, homeTagline: site.homeTagline, // Always a hex on the wire: an accent id means nothing to another hub. ...(accentHex(site.accent) ? { accent: accentHex(site.accent) } : {}), ...(site.siteUrl ? { siteUrl: site.siteUrl } : {}), ...(opts.hubUrl ? { hubUrl: opts.hubUrl } : {}), pwa: opts.pwa, socialLinks, groups: site.groups, defaultGroupId: site.defaultGroupId, channels, generatedAt: manifest.generatedAt, summariesVersion: manifest.version, }; }