import { DEFAULT_WEBSITE_SCHEMA_SETTINGS, WEBSITE_SCHEMA_TYPE_IDS, type WebsiteSchemaSettings, type WebsiteSchemaType, } from './business-settings.types'; import type { BusinessSocialMedia } from '../business-profile/business-profile.types'; function isRecord(value: unknown): value is Record { return typeof value === 'object' && value !== null && !Array.isArray(value); } function readTrimmed(value: unknown): string | null { if (typeof value !== 'string') return null; const trimmed = value.trim(); return trimmed.length > 0 ? trimmed : null; } function normalizeSameAs(raw: unknown): string[] { if (!Array.isArray(raw)) return []; const seen = new Set(); const out: string[] = []; for (const item of raw) { if (typeof item !== 'string') continue; const url = item.trim(); if (!url || seen.has(url)) continue; if (!/^https?:\/\//i.test(url)) continue; seen.add(url); out.push(url); if (out.length >= 20) break; } return out; } export function normalizeWebsiteSchemaSettings( raw: unknown, ): WebsiteSchemaSettings { const source = isRecord(raw) ? raw : {}; const typeRaw = typeof source.type === 'string' ? source.type : ''; const type = (WEBSITE_SCHEMA_TYPE_IDS as readonly string[]).includes(typeRaw) ? (typeRaw as WebsiteSchemaType) : DEFAULT_WEBSITE_SCHEMA_SETTINGS.type; return { enabled: typeof source.enabled === 'boolean' ? source.enabled : DEFAULT_WEBSITE_SCHEMA_SETTINGS.enabled, type, name: readTrimmed(source.name), nameFa: readTrimmed(source.nameFa), description: readTrimmed(source.description), priceRange: readTrimmed(source.priceRange), sameAs: normalizeSameAs(source.sameAs), }; } /** Turn social handles / URLs into absolute profile URLs for schema.org sameAs. */ export function socialMediaToSameAsUrls(social: BusinessSocialMedia): string[] { const urls: string[] = []; const push = (value: string, build: (handle: string) => string) => { const trimmed = value.trim(); if (!trimmed) return; if (/^https?:\/\//i.test(trimmed)) { urls.push(trimmed); return; } const handle = trimmed.replace(/^@/, '').replace(/^\/+/, ''); if (!handle) return; urls.push(build(handle)); }; push(social.instagram, (h) => `https://www.instagram.com/${h}`); push(social.telegram, (h) => h.startsWith('t.me/') ? `https://${h}` : `https://t.me/${h}`, ); push(social.linkedin, (h) => h.includes('linkedin.com') ? (h.startsWith('http') ? h : `https://${h}`) : `https://www.linkedin.com/in/${h}`, ); push(social.youtube, (h) => h.includes('youtube.com') || h.includes('youtu.be') ? h.startsWith('http') ? h : `https://${h}` : `https://www.youtube.com/@${h}`, ); push(social.aparat, (h) => h.includes('aparat.com') ? h.startsWith('http') ? h : `https://${h}` : `https://www.aparat.com/${h}`, ); // WhatsApp is a phone link, not a public profile — skip for sameAs. return normalizeSameAs(urls); } export type WebsiteSchemaJsonLd = Record; export type BuildWebsiteSchemaJsonLdInput = { settings: WebsiteSchemaSettings; host: string; name: string; nameFa: string; about: string; logoUrl: string | null; emails: string[]; phoneNumbers: Array<{ type: string; number: string }>; socialMedia: BusinessSocialMedia; addresses: Array<{ province: string; city: string; address: string; postalCode: string | null; landline: string | null; }>; }; /** * Build site-wide Organization / LocalBusiness JSON-LD for storefronts. * Returns null when schema is disabled. */ export function buildWebsiteSchemaJsonLd( input: BuildWebsiteSchemaJsonLdInput, ): WebsiteSchemaJsonLd | null { const { settings } = input; if (!settings.enabled) return null; const displayName = settings.nameFa || input.nameFa?.trim() || settings.name || input.name?.trim() || input.host; const description = settings.description || input.about?.trim() || undefined; const sameAs = normalizeSameAs([ ...socialMediaToSameAsUrls(input.socialMedia), ...settings.sameAs, ]); const telephone = input.phoneNumbers.find((p) => p.number.trim())?.number.trim() || input.addresses.find((a) => a.landline?.trim())?.landline?.trim() || undefined; const email = input.emails.find((e) => e.trim())?.trim() || undefined; const url = `https://${input.host.replace(/^https?:\/\//i, '').replace(/\/+$/, '')}`; const jsonLd: WebsiteSchemaJsonLd = { '@context': 'https://schema.org', '@type': settings.type, name: displayName, url, }; if (description) jsonLd.description = description; if (input.logoUrl) { jsonLd.logo = input.logoUrl; jsonLd.image = input.logoUrl; } if (telephone) jsonLd.telephone = telephone; if (email) jsonLd.email = email; if (sameAs.length > 0) jsonLd.sameAs = sameAs; const primaryAddress = input.addresses[0]; if (primaryAddress) { const street = primaryAddress.address?.trim(); const city = primaryAddress.city?.trim(); const province = primaryAddress.province?.trim(); const postalCode = primaryAddress.postalCode?.trim() || undefined; if (street || city || province || postalCode) { jsonLd.address = { '@type': 'PostalAddress', ...(street ? { streetAddress: street } : {}), ...(city ? { addressLocality: city } : {}), ...(province ? { addressRegion: province } : {}), ...(postalCode ? { postalCode } : {}), addressCountry: 'IR', }; } } if (settings.type === 'LocalBusiness' && settings.priceRange) { jsonLd.priceRange = settings.priceRange; } return jsonLd; }