Files

58 lines
2.0 KiB
TypeScript

import type { Breadcrumb, PageDocument } from '#web-runtime/types';
import { toPublicHref } from '#i18n/content-href';
export type JsonLd = Record<string, unknown>;
/**
* 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 (
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(data) }} />
);
}