diff --git a/src/ui/seo/SeoHead.tsx b/src/ui/seo/SeoHead.tsx new file mode 100644 index 00000000..fe2b50f8 --- /dev/null +++ b/src/ui/seo/SeoHead.tsx @@ -0,0 +1,83 @@ +import type { Language } from "@/i18n/resolver"; +import { JsonLdRenderer } from "@/shared/seo/json-ld.js"; +import type { Thing } from "schema-dts"; + +export interface SeoHeadProps { + title: string; + description: string; + canonical: string; + hreflang: Array<{ lang: Language | "x-default"; href: string }>; + og: { + title: string; + description: string; + url: string; + image: string; + type: "website" | "article"; + locale: string; + siteName: string; + }; + twitter?: { + card: "summary" | "summary_large_image"; + title?: string; + description?: string; + image?: string; + }; + jsonLd?: Thing | Thing[]; + noindex?: boolean; +} + +/** + * Renders the full SEO fragment: title, meta description, canonical, + * hreflang alternates, Open Graph tags, Twitter Card tags, and JSON-LD. + */ +export function SeoHead({ + title, + description, + canonical, + hreflang, + og, + twitter, + jsonLd, + noindex, +}: SeoHeadProps): JSX.Element { + return ( + <> + {title} + + + {noindex && } + + {/* Hreflang alternates */} + {hreflang.map((entry) => ( + + ))} + + {/* Open Graph */} + + + + + + + + + {/* Twitter Card */} + {twitter && ( + <> + + {twitter.title && } + {twitter.description && } + {twitter.image && } + + )} + + {/* JSON-LD */} + {jsonLd && } + + ); +}