import type { Breadcrumb, PageDocument } from '#web-runtime/types'; import { toPublicHref } from '#i18n/content-href'; export type JsonLd = Record; /** * schema.org `BreadcrumbList` for a resolved breadcrumb trail. Positions are * 1-based. The current page (the last crumb) intentionally omits `item` — per * Google's guidance it is where the user already is, so it carries no URL. */ export function breadcrumbListJsonLd(items: Breadcrumb[]): JsonLd { return { '@context': 'https://schema.org', '@type': 'BreadcrumbList', 'itemListElement': items.map((item, index) => ({ '@type': 'ListItem', 'position': index + 1, 'name': item.label, ...(index < items.length - 1 ? { item: toPublicHref(item.href) } : {}) })) }; } /** * schema.org `Article` for an editorial page. Only the fields the backend actually knows are * emitted — a date or image invented here would be markup that contradicts the page. * * `dateModified` is omitted when it equals `datePublished`, since repeating it tells a crawler * nothing, and the whole object is skipped by the caller when there is no publication date: * an `Article` without one is markup Google will ignore anyway. */ export function articleJsonLd(page: PageDocument): JsonLd | null { if (!page.publishedAt) { return null; } const image = page.hero?.media?.url; return { '@context': 'https://schema.org', '@type': 'Article', 'headline': page.seo.title || page.title, ...(page.seo.description ? { description: page.seo.description } : {}), 'datePublished': page.publishedAt, ...(page.updatedAt && page.updatedAt !== page.publishedAt ? { dateModified: page.updatedAt } : {}), ...(image ? { image: [image] } : {}), 'inLanguage': page.locale }; } export function JsonLdScript({ data }: { data: JsonLd }) { return (