/* Maintains packages/share/src/semantic/jsonld/webpage.ts as an authored share authored module so agents can evolve it without rediscovering local boundaries. Do not parse raw content. RFC-0372: speakable reads from unified page.blocks instead of answerBlocks. RFC-1097: step 6 — compass.migrate codemod run Mechanical v1 to v2 header migration across the workspace: 942 files rewritten — CHANGE_SUMMARY windows collapsed into history, forbidden v1 blocks stripped, KEY_DECISIONS seeded from @ai-invariant comments (5 files) or TODO placeholders (103 files), blocks reordered to canonical order. RFC-1097: sweep — werkstatt-engine clean Sweep batch 4: 73 Compass headers on headerless engine files (certification, component-runtime, isolation, evolution, testing), real KEY_DECISIONS on 75 files (kernel, cache, dht, swim, gitmesh, runtime), ~80 purpose expansions (CONTRACT-02/PURPOSE-02), non-goals on 13 CONTRACT-03 files, CS-07 history literal fix repo-wide (253 files). Policy: .template.ts/.template.astro excludedPaths. werkstatt-engine now 0 diagnostics. */ import type { JsonLdContext } from "./context.ts"; import type { SemanticPageModel } from "../models.ts"; import type { JsonLdNode } from "./types.ts"; function getWebPageTypes(page: SemanticPageModel): string[] { switch (page.type) { case "about": return ["WebPage", "AboutPage"]; case "projects": return ["WebPage", "CollectionPage"]; case "openSource": return ["WebPage", "CollectionPage"]; case "donationContact": return ["WebPage", "ContactPage"]; case "person": // RFC-0200: a per-member profile page is a schema.org ProfilePage. return ["WebPage", "ProfilePage"]; case "collection": // RFC-0490: a pillar hub page is a schema.org CollectionPage. return ["WebPage", "CollectionPage"]; default: return ["WebPage"]; } } export function buildWebPageNode(context: JsonLdContext): JsonLdNode { const { page, ids, webpageId, breadcrumbId, initiativesListId, servicesListId } = context; return { "@type": getWebPageTypes(page), "@id": webpageId, url: page.url, name: page.title, headline: page.heading ?? page.title, description: page.description, inLanguage: page.lang, isPartOf: { "@id": ids.website }, about: { "@id": ids.organization }, publisher: { "@id": ids.organization }, ...(page.type === "about" ? { mainEntity: { "@id": ids.organization } } : {}), ...(page.type === "projects" && page.initiatives?.length ? { mainEntity: { "@id": initiativesListId } } : {}), // RFC-0200: a profile page's subject is the profiled Person (attached to page.people). // RFC-0512: for team profile pages, prefer the extended Person/SoftwareApplication node // from extraGraphNodes as mainEntity — this covers AI-agent profiles (where page.people // is empty) and human profiles (where the extended Person node has a different @id). ...(page.type === "person" && page.extraGraphNodes?.[0] ? { mainEntity: { "@id": page.extraGraphNodes[0]["@id"] } } : page.type === "person" && page.people?.[0] ? { mainEntity: { "@id": ids.person(page.people[0].name) } } : {}), ...(page.organization.services?.length && servicesListId ? { mentions: { "@id": servicesListId } } : {}), ...(page.breadcrumbs.length > 1 ? { breadcrumb: { "@id": breadcrumbId } } : {}), ...(page.lead ? { abstract: page.lead } : {}), // RFC-0163: primary image of the page (consumed from RFC-0162 primaryImage). ...(page.primaryImage ? { primaryImageOfPage: { "@type": "ImageObject", url: page.primaryImage.url, ...(page.primaryImage.width ? { width: page.primaryImage.width } : {}), ...(page.primaryImage.height ? { height: page.primaryImage.height } : {}), }, } : {}), // RFC-0227: link credited materials from the page entity for licensable-images treatment. ...(page.materialCreditAtIds?.length ? { associatedMedia: page.materialCreditAtIds.map((id) => ({ "@id": id })) } : {}), // RFC-0163: speakable answer surface for voice assistants / AI Overviews. The // page heading and lead are always-present, extractable answer anchors. // RFC-0372: reads from unified page.blocks (any block with a heading counts as speakable). ...(page.blocks.some((b) => b.heading) ? { speakable: { "@type": "SpeakableSpecification", cssSelector: page.lead ? ["h1", ".section-header__subheading"] : ["h1"], }, } : {}), }; }