import type { PublicCategory, PublicProductDetail, SiteSettings } from "@imen/shared"; import { siteConfig } from "../config/site"; type Json = Record; /** Values every schema builder needs: editable settings + the absolute site URL */ export interface SeoContext { settings: SiteSettings; siteUrl: string; } const abs = (ctx: SeoContext, p: string) => new URL(p, ctx.siteUrl).toString(); /** Social profile links worth listing in `sameAs` (not mail/phone/own site) */ const PROFILE_TYPES = new Set(["telegram", "instagram", "linkedin", "whatsapp", "bale", "eitaa", "aparat", "youtube"]); const clean = (o: T): Json => JSON.parse( JSON.stringify(o, (_, v) => v === "" || (Array.isArray(v) && !v.length) ? undefined : v, ), ); export const organizationSchema = (ctx: SeoContext) => { const s = ctx.settings; return clean({ "@context": "https://schema.org", "@type": "LocalBusiness", "@id": `${ctx.siteUrl}/#organization`, name: s.name, alternateName: s.nameEn, url: abs(ctx, "/"), logo: abs(ctx, siteConfig.logo), image: abs(ctx, s.ogImage.url), description: s.description, email: s.email, telephone: s.phone, foundingDate: s.foundingDate, areaServed: siteConfig.areaServed, address: { "@type": "PostalAddress", streetAddress: s.address.street, addressLocality: s.address.locality, addressCountry: s.address.country, }, openingHoursSpecification: { "@type": "OpeningHoursSpecification", dayOfWeek: s.openingHours.days, opens: s.openingHours.opens, closes: s.openingHours.closes, }, sameAs: s.socials.filter((x) => PROFILE_TYPES.has(x.type)).map((x) => x.url), }); }; export const websiteSchema = (ctx: SeoContext) => ({ "@context": "https://schema.org", "@type": "WebSite", "@id": `${ctx.siteUrl}/#website`, url: abs(ctx, "/"), name: ctx.settings.name, inLanguage: siteConfig.lang, publisher: { "@id": `${ctx.siteUrl}/#organization` }, }); export const serviceSchema = ( ctx: SeoContext, opts: { name: string; description: string; areaServed?: string }, ) => clean({ "@context": "https://schema.org", "@type": "Service", name: opts.name, description: opts.description, areaServed: opts.areaServed ?? siteConfig.areaServed, provider: { "@id": `${ctx.siteUrl}/#organization` }, }); export const categoryPageSchema = (ctx: SeoContext, category: PublicCategory) => clean({ "@context": "https://schema.org", "@type": "CollectionPage", "@id": `${ctx.siteUrl}/categories/${category.slug}`, name: category.name, description: category.seoDescription || category.fullDesc, url: abs(ctx, `/categories/${category.slug}`), isPartOf: { "@id": `${ctx.siteUrl}/#website` }, provider: { "@id": `${ctx.siteUrl}/#organization` }, mainEntity: { "@type": "ItemList", numberOfItems: category.products.length, itemListElement: category.products.map((p, i) => ({ "@type": "ListItem", position: i + 1, item: { "@type": "Product", name: p.name, description: p.desc, sku: p.code, url: abs(ctx, `/products/${p.slug}`), ...(p.image && { image: abs(ctx, p.image) }), }, })), }, }); export const productSchema = (ctx: SeoContext, p: PublicProductDetail) => clean({ "@context": "https://schema.org", "@type": "Product", "@id": `${ctx.siteUrl}/products/${p.slug}#product`, name: p.name, alternateName: p.nameEn, sku: p.code, mpn: p.code, description: p.seoDescription || p.desc || p.details.slice(0, 300), url: abs(ctx, `/products/${p.slug}`), image: p.images.map((i) => abs(ctx, i.url)), category: p.category.name, ...(p.brand && { brand: { "@type": "Brand", name: p.brand } }), countryOfOrigin: p.origin, additionalProperty: [ ...p.attributes.map((a) => ({ "@type": "PropertyValue", name: a.name, value: a.value })), ...p.standards.map((s) => ({ "@type": "PropertyValue", name: "استاندارد", value: s })), ], seller: { "@id": `${ctx.siteUrl}/#organization` }, }); export const breadcrumbSchema = (ctx: SeoContext, items: { name: string; path: string }[]) => ({ "@context": "https://schema.org", "@type": "BreadcrumbList", itemListElement: items.map((it, i) => ({ "@type": "ListItem", position: i + 1, name: it.name, item: abs(ctx, it.path), })), }); /** Serialize JSON-LD safely for an inline