UNPKG

next-seo

Version:

SEO plugin for Next.js projects

1 lines 229 kB
{"version":3,"sources":["../src/utils/stringify.ts","../src/core/JsonLdScript.tsx","../src/utils/processors.export.ts","../src/utils/processors.ts","../src/components/ArticleJsonLd.tsx","../src/components/ClaimReviewJsonLd.tsx","../src/components/CreativeWorkJsonLd.tsx","../src/components/RecipeJsonLd.tsx","../src/components/HowToJsonLd.tsx","../src/components/OrganizationJsonLd.tsx","../src/components/LocalBusinessJsonLd.tsx","../src/components/MerchantReturnPolicyJsonLd.tsx","../src/components/MovieCarouselJsonLd.tsx","../src/components/BreadcrumbJsonLd.tsx","../src/components/CarouselJsonLd.tsx","../src/components/CourseJsonLd.tsx","../src/components/EventJsonLd.tsx","../src/components/FAQJsonLd.tsx","../src/components/ImageJsonLd.tsx","../src/components/QuizJsonLd.tsx","../src/components/DatasetJsonLd.tsx","../src/components/JobPostingJsonLd.tsx","../src/components/DiscussionForumPostingJsonLd.tsx","../src/components/EmployerAggregateRatingJsonLd.tsx","../src/components/VacationRentalJsonLd.tsx","../src/components/VideoJsonLd.tsx","../src/components/ProfilePageJsonLd.tsx","../src/components/SoftwareApplicationJsonLd.tsx","../src/components/ProductJsonLd.tsx","../src/components/ReviewJsonLd.tsx","../src/components/AggregateRatingJsonLd.tsx"],"sourcesContent":["/* eslint-disable */\n// Some of the code below is borrowed from react-schemaorg after the author of the package\n// kindly reached out to let me know this was a better way of doing things. ❤️\n// https://github.com/google/react-schemaorg/blob/main/src/json-ld.tsx#L173\n\ntype JsonValueScalar = string | boolean | number;\ntype JsonValue =\n | JsonValueScalar\n | Array<JsonValue>\n | { [key: string]: JsonValue };\ntype JsonReplacer = (_: string, value: JsonValue) => JsonValue | undefined;\n\n/**\n * A replacer for JSON.stringify to omit null values from JSON-LD.\n * The actual script tag safety escaping is done in post-processing.\n */\nconst safeJsonLdReplacer: JsonReplacer = (() => {\n return (_: string, value: JsonValue): JsonValue | undefined => {\n switch (typeof value) {\n case \"object\":\n // Omit null values.\n if (value === null) {\n return undefined;\n }\n return value; // JSON.stringify will recursively call replacer.\n case \"number\":\n case \"boolean\":\n case \"bigint\":\n case \"string\":\n return value; // Return all primitive values as-is\n default: {\n // We shouldn't expect other types.\n isNever(value);\n // JSON.stringify will remove this element.\n return undefined;\n }\n }\n };\n})();\n\n/**\n * Type guard to ensure exhaustive type checking.\n * @internal\n */\nfunction isNever(_: never): void {}\n\n/**\n * Stringify data for safe embedding in HTML script elements.\n *\n * Per W3C specifications and security best practices, we need to escape sequences\n * that could break out of the script tag:\n * - </script> sequences (case-insensitive)\n * - <!-- and --> sequences (HTML comments)\n *\n * We do NOT escape standard HTML entities like &, <, >, \", ' as they are valid\n * within script tag content and escaping them breaks URLs with query parameters.\n *\n * The escaping is done on the final JSON string to ensure the JSON remains valid\n * and parseable while being safe for HTML embedding.\n *\n * References:\n * - https://www.w3.org/TR/json-ld11/#restrictions-for-contents-of-json-ld-script-elements\n * - https://github.com/w3c/json-ld-syntax/issues/100\n */\nexport const stringify = (data: unknown) => {\n const jsonString = JSON.stringify(data, safeJsonLdReplacer);\n\n // Post-process the JSON string to escape dangerous sequences\n // This ensures the JSON remains valid while being safe for script tags\n // Use Unicode escape sequences to break up dangerous patterns\n // This prevents the HTML parser from recognizing them while keeping valid JSON\n return jsonString\n .replace(/<\\/script>/gi, \"\\\\u003C/script>\") // Unicode escape for <\n .replace(/<!--/g, \"\\\\u003C!--\") // Unicode escape for <\n .replace(/-->/g, \"--\\\\u003E\"); // Unicode escape for >\n};\n","import { stringify } from \"~/utils/stringify\";\n\ninterface JsonLdScriptProps<T = Record<string, unknown>> {\n data: T;\n id?: string;\n scriptKey: string; // For React key\n}\n\nexport function JsonLdScript<T = Record<string, unknown>>({\n data,\n id,\n scriptKey,\n}: JsonLdScriptProps<T>): React.JSX.Element | null {\n if (data === null || data === undefined) {\n // Explicitly check for null/undefined\n return null;\n }\n\n const jsonString = stringify(data);\n\n return (\n <script\n type=\"application/ld+json\"\n id={id || scriptKey}\n data-testid={id}\n dangerouslySetInnerHTML={{ __html: jsonString }}\n key={scriptKey}\n />\n );\n}\n","/**\n * Public API for custom component creation\n * These processors help maintain the @type optional pattern\n * and provide flexible input handling for structured data\n */\n\n// Core utility for generic schema type processing\nexport { processSchemaType } from \"./processors\";\n\n// People & Organizations\nexport {\n processAuthor,\n processPublisher,\n processOrganization,\n processOrganizer,\n processPerformer,\n processDirector,\n processCreator,\n processFunder,\n processProvider,\n processHiringOrganization,\n} from \"./processors\";\n\n// Media & Content\nexport {\n processImage,\n processVideo,\n processLogo,\n processScreenshot,\n processClip,\n processBroadcastEvent,\n processSeekToAction,\n processThreeDModel,\n} from \"./processors\";\n\n// Locations & Places\nexport {\n processAddress,\n processPlace,\n processGeo,\n processJobLocation,\n processSpatialCoverage,\n processApplicantLocationRequirements,\n processDefinedRegion,\n} from \"./processors\";\n\n// Commerce & Offers\nexport {\n processOffer,\n processProductOffer,\n processAggregateOffer,\n processMerchantReturnPolicy,\n processReturnPolicySeasonalOverride,\n processPriceSpecification,\n processUnitPriceSpecification,\n processSimpleMonetaryAmount,\n processMonetaryAmount,\n processOfferShippingDetails,\n processShippingDeliveryTime,\n} from \"./processors\";\n\n// Reviews & Ratings\nexport {\n processReview,\n processProductReview,\n processAggregateRating,\n processRating,\n processClaimReviewRating,\n processItemReviewed,\n} from \"./processors\";\n\n// Structured Content\nexport {\n processInstruction,\n processNutrition,\n processBreadcrumbItem,\n processComment,\n processWebPageElement,\n processCertification,\n} from \"./processors\";\n\n// HowTo Content\nexport {\n processStep,\n processHowToStep,\n processHowToSection,\n processHowToSupply,\n processHowToTool,\n processHowToDirection,\n processHowToTip,\n processEstimatedCost,\n processHowToYield,\n} from \"./processors\";\n\n// Membership & Loyalty\nexport {\n processMemberProgram,\n processMemberProgramTier,\n processTierRequirement,\n processTierBenefit,\n processMembershipPointsEarned,\n} from \"./processors\";\n\n// Specifications & Values\nexport {\n processQuantitativeValue,\n processNumberOfEmployees,\n processContactPoint,\n processOpeningHours,\n processJobPropertyValue,\n processIdentifier,\n processPeopleAudience,\n processSizeSpecification,\n} from \"./processors\";\n\n// Products\nexport {\n processProductVariant,\n processVariesBy,\n processBrand,\n processProductItemList,\n} from \"./processors\";\n\n// Education & Requirements\nexport {\n processEducationRequirements,\n processExperienceRequirements,\n} from \"./processors\";\n\n// Data & Creative Works\nexport {\n processLicense,\n processDataDownload,\n processDataCatalog,\n processIsPartOf,\n processMainEntityOfPage,\n processAppearance,\n processSharedContent,\n processClaim,\n} from \"./processors\";\n\n// Accommodation & Rental\nexport {\n processAccommodation,\n processBedDetails,\n processLocationFeatureSpecification,\n} from \"./processors\";\n\n// Interaction & Statistics\nexport { processInteractionStatistic, processFeatureList } from \"./processors\";\n\n// Re-export types that users might need\nexport type { ItemReviewedType } from \"./processors\";\n","import type {\n Author,\n Person,\n Organization,\n ImageObject,\n PostalAddress,\n ContactPoint,\n QuantitativeValue,\n GeoCoordinates,\n OpeningHoursSpecification,\n Review,\n AggregateRating,\n MerchantReturnPolicy,\n MerchantReturnPolicySeasonalOverride,\n SimpleMonetaryAmount,\n Rating,\n VideoObject,\n InteractionCounter,\n Brand,\n BedDetails,\n LocationFeatureSpecification,\n Accommodation,\n MemberProgram,\n MemberProgramTier,\n CreditCard,\n UnitPriceSpecification,\n TierRequirement,\n TierBenefit,\n Certification,\n PeopleAudience,\n SizeSpecification,\n ThreeDModel,\n DefinedRegion,\n ShippingDeliveryTime,\n OfferShippingDetails,\n} from \"~/types/common.types\";\nimport type { Director } from \"~/types/movie-carousel.types\";\nimport type { Provider } from \"~/types/course.types\";\nimport type { BreadcrumbListItem, ListItem } from \"~/types/breadcrumb.types\";\nimport type {\n Place,\n Performer,\n Organizer,\n Offer,\n PerformingGroup,\n} from \"~/types/event.types\";\nimport type {\n NutritionInformation,\n HowToStep,\n HowToSection,\n} from \"~/types/recipe.types\";\nimport type {\n GeoShape,\n PropertyValue,\n CreativeWork,\n DatasetPlace,\n DataDownload,\n DataCatalog,\n} from \"~/types/dataset.types\";\nimport type {\n Place as JobPlace,\n PropertyValue as JobPropertyValue,\n MonetaryAmount,\n Country,\n State,\n AdministrativeArea,\n EducationalOccupationalCredential,\n OccupationalExperienceRequirements,\n} from \"~/types/jobposting.types\";\nimport type {\n Comment,\n SharedContent,\n WebPage as ForumWebPage,\n CreativeWork as ForumCreativeWork,\n} from \"~/types/discussionforum.types\";\nimport type {\n Claim,\n ClaimReviewRating,\n ClaimCreativeWork,\n} from \"~/types/claimreview.types\";\nimport type {\n BroadcastEvent,\n Clip,\n PotentialAction,\n} from \"~/types/video.types\";\nimport type { WebPageElement } from \"~/types/creativework.types\";\nimport type {\n ProductOffer,\n AggregateOffer,\n PriceSpecification,\n ProductItemList,\n ProductListItem,\n ProductReview,\n Product,\n VariesBy,\n} from \"~/types/product.types\";\nimport type {\n HowToSupply,\n HowToTool,\n HowToDirection,\n HowToTip,\n HowToStep as HowToStepType,\n HowToSection as HowToSectionType,\n Step,\n Supply,\n Tool,\n EstimatedCost,\n HowToYield,\n} from \"~/types/howto.types\";\n\n// Schema.org type constants\nconst SCHEMA_TYPES = {\n PERSON: \"Person\",\n ORGANIZATION: \"Organization\",\n IMAGE_OBJECT: \"ImageObject\",\n POSTAL_ADDRESS: \"PostalAddress\",\n CONTACT_POINT: \"ContactPoint\",\n QUANTITATIVE_VALUE: \"QuantitativeValue\",\n GEO_COORDINATES: \"GeoCoordinates\",\n GEO_SHAPE: \"GeoShape\",\n OPENING_HOURS: \"OpeningHoursSpecification\",\n REVIEW: \"Review\",\n RATING: \"Rating\",\n AGGREGATE_RATING: \"AggregateRating\",\n MERCHANT_RETURN_POLICY: \"MerchantReturnPolicy\",\n MERCHANT_RETURN_POLICY_SEASONAL_OVERRIDE:\n \"MerchantReturnPolicySeasonalOverride\",\n MONETARY_AMOUNT: \"MonetaryAmount\",\n VIDEO_OBJECT: \"VideoObject\",\n INTERACTION_COUNTER: \"InteractionCounter\",\n BRAND: \"Brand\",\n CREDIT_CARD: \"CreditCard\",\n UNIT_PRICE_SPECIFICATION: \"UnitPriceSpecification\",\n MEMBER_PROGRAM: \"MemberProgram\",\n MEMBER_PROGRAM_TIER: \"MemberProgramTier\",\n BED_DETAILS: \"BedDetails\",\n LOCATION_FEATURE: \"LocationFeatureSpecification\",\n ACCOMMODATION: \"Accommodation\",\n PLACE: \"Place\",\n PERFORMING_GROUP: \"PerformingGroup\",\n OFFER: \"Offer\",\n AGGREGATE_OFFER: \"AggregateOffer\",\n PRICE_SPECIFICATION: \"PriceSpecification\",\n ITEM_LIST: \"ItemList\",\n LIST_ITEM: \"ListItem\",\n PRODUCT: \"Product\",\n PRODUCT_GROUP: \"ProductGroup\",\n NUTRITION_INFORMATION: \"NutritionInformation\",\n HOW_TO_STEP: \"HowToStep\",\n HOW_TO_SECTION: \"HowToSection\",\n HOW_TO_SUPPLY: \"HowToSupply\",\n HOW_TO_TOOL: \"HowToTool\",\n HOW_TO_DIRECTION: \"HowToDirection\",\n HOW_TO_TIP: \"HowToTip\",\n PROPERTY_VALUE: \"PropertyValue\",\n CREATIVE_WORK: \"CreativeWork\",\n DATA_DOWNLOAD: \"DataDownload\",\n DATA_CATALOG: \"DataCatalog\",\n COUNTRY: \"Country\",\n STATE: \"State\",\n EDUCATIONAL_CREDENTIAL: \"EducationalOccupationalCredential\",\n OCCUPATIONAL_EXPERIENCE: \"OccupationalExperienceRequirements\",\n COMMENT: \"Comment\",\n WEB_PAGE: \"WebPage\",\n WEB_PAGE_ELEMENT: \"WebPageElement\",\n CLAIM: \"Claim\",\n CERTIFICATION: \"Certification\",\n PEOPLE_AUDIENCE: \"PeopleAudience\",\n SIZE_SPECIFICATION: \"SizeSpecification\",\n THREE_D_MODEL: \"3DModel\",\n DEFINED_REGION: \"DefinedRegion\",\n SHIPPING_DELIVERY_TIME: \"ShippingDeliveryTime\",\n OFFER_SHIPPING_DETAILS: \"OfferShippingDetails\",\n} as const;\n\n// Type guard utilities\nfunction hasType<T extends { \"@type\": string }>(obj: unknown): obj is T {\n return obj !== null && typeof obj === \"object\" && \"@type\" in obj;\n}\n\nfunction isString(value: unknown): value is string {\n return typeof value === \"string\";\n}\n\n// Generic processor for simple schema types\nexport function processSchemaType<T extends { \"@type\": string }>(\n value: unknown,\n schemaType: string,\n stringHandler?: (str: string) => Omit<T, \"@type\">,\n numberHandler?: (num: number) => Omit<T, \"@type\">,\n): T {\n if (isString(value) && stringHandler) {\n return { \"@type\": schemaType, ...stringHandler(value) } as T;\n }\n\n if (typeof value === \"number\" && numberHandler) {\n return { \"@type\": schemaType, ...numberHandler(value) } as T;\n }\n\n if (hasType<T>(value)) {\n return value;\n }\n\n // Ensure value is an object before spreading\n if (typeof value === \"object\" && value !== null) {\n return { \"@type\": schemaType, ...value } as T;\n }\n\n // Fallback for non-object values\n return { \"@type\": schemaType } as T;\n}\n\n// Helper to process nested organization fields\nfunction processOrganizationFields(org: Organization): void {\n if (org.logo && !isString(org.logo)) {\n org.logo = processLogo(org.logo);\n }\n\n if (org.address && !isString(org.address)) {\n if (Array.isArray(org.address)) {\n org.address = org.address.map((addr) =>\n isString(addr) ? addr : processAddress(addr),\n );\n } else {\n org.address = processAddress(org.address);\n }\n }\n\n if (org.contactPoint) {\n if (Array.isArray(org.contactPoint)) {\n org.contactPoint = org.contactPoint.map(processContactPoint);\n } else {\n org.contactPoint = processContactPoint(org.contactPoint);\n }\n }\n}\n\n/**\n * Processes author input into a Person or Organization schema type\n * @param author - String name, Person object, or Organization object\n * @returns Processed Person or Organization with @type\n * @example\n * processAuthor(\"John Doe\") // { \"@type\": \"Person\", name: \"John Doe\" }\n * processAuthor({ name: \"ACME Corp\", logo: \"logo.jpg\" }) // { \"@type\": \"Organization\", ... }\n */\nexport function processAuthor(author: Author): Person | Organization {\n if (isString(author)) {\n // Check if the string appears to be an organization name\n const orgIndicators = [\n \"magazine\",\n \"publication\",\n \"company\",\n \"corporation\",\n \"corp\",\n \"inc\",\n \"llc\",\n \"ltd\",\n \"limited\",\n \"group\",\n \"foundation\",\n \"institute\",\n \"association\",\n \"society\",\n \"union\",\n \"times\",\n \"news\",\n \"press\",\n \"media\",\n \"network\",\n \"agency\",\n \"studio\",\n ];\n\n const lowerName = author.toLowerCase();\n const isLikelyOrg = orgIndicators.some((indicator) =>\n lowerName.includes(indicator),\n );\n\n if (isLikelyOrg) {\n return {\n \"@type\": SCHEMA_TYPES.ORGANIZATION,\n name: author,\n };\n }\n\n return {\n \"@type\": SCHEMA_TYPES.PERSON,\n name: author,\n };\n }\n\n if (hasType<Person | Organization>(author)) {\n return author;\n }\n\n // Determine if it's Person or Organization based on properties\n const hasOrgProperties =\n \"logo\" in author || \"address\" in author || \"contactPoint\" in author;\n\n if (hasOrgProperties) {\n const org: Organization = {\n \"@type\": SCHEMA_TYPES.ORGANIZATION,\n ...author,\n };\n processOrganizationFields(org);\n return org;\n }\n\n // Default to Person\n return {\n \"@type\": SCHEMA_TYPES.PERSON,\n ...author,\n } as Person;\n}\n\n/**\n * Processes image input into ImageObject schema type\n * @param image - URL string or ImageObject\n * @returns URL string or ImageObject with @type\n * @example\n * processImage(\"https://example.com/image.jpg\") // \"https://example.com/image.jpg\"\n * processImage({ url: \"image.jpg\", width: 800 }) // { \"@type\": \"ImageObject\", ... }\n */\nexport function processImage(\n image: string | ImageObject | Omit<ImageObject, \"@type\">,\n): string | ImageObject {\n if (isString(image)) {\n return image;\n }\n\n return processSchemaType<ImageObject>(image, SCHEMA_TYPES.IMAGE_OBJECT);\n}\n\n/**\n * Processes address input into PostalAddress schema type\n * @param address - String address or PostalAddress object\n * @returns PostalAddress with @type\n * @example\n * processAddress(\"123 Main St\") // { \"@type\": \"PostalAddress\", streetAddress: \"123 Main St\" }\n */\nexport function processAddress(\n address: string | PostalAddress | Omit<PostalAddress, \"@type\">,\n): PostalAddress {\n return processSchemaType<PostalAddress>(\n address,\n SCHEMA_TYPES.POSTAL_ADDRESS,\n (str) => ({ streetAddress: str }),\n undefined,\n );\n}\n\n/**\n * Processes contact point into ContactPoint schema type\n * @param contactPoint - ContactPoint object with or without @type\n * @returns ContactPoint with @type\n */\nexport function processContactPoint(\n contactPoint: ContactPoint | Omit<ContactPoint, \"@type\">,\n): ContactPoint {\n return processSchemaType<ContactPoint>(\n contactPoint,\n SCHEMA_TYPES.CONTACT_POINT,\n );\n}\n\n/**\n * Processes logo the same way as images\n * @param logo - URL string or ImageObject\n * @returns URL string or ImageObject with @type\n */\nexport function processLogo(\n logo: string | ImageObject | Omit<ImageObject, \"@type\">,\n): string | ImageObject {\n return processImage(logo);\n}\n\n/**\n * Processes number of employees into QuantitativeValue schema type\n * @param numberOfEmployees - Number or QuantitativeValue object\n * @returns QuantitativeValue with @type\n */\nexport function processNumberOfEmployees(\n numberOfEmployees:\n | number\n | QuantitativeValue\n | Omit<QuantitativeValue, \"@type\">,\n): QuantitativeValue {\n return processSchemaType<QuantitativeValue>(\n numberOfEmployees,\n SCHEMA_TYPES.QUANTITATIVE_VALUE,\n undefined,\n (num) => ({ value: num }),\n );\n}\n\n/**\n * Processes geographic coordinates into GeoCoordinates schema type\n * @param geo - GeoCoordinates object with or without @type\n * @returns GeoCoordinates with @type\n */\nexport function processGeo(\n geo: GeoCoordinates | Omit<GeoCoordinates, \"@type\">,\n): GeoCoordinates {\n return processSchemaType<GeoCoordinates>(geo, SCHEMA_TYPES.GEO_COORDINATES);\n}\n\n/**\n * Processes opening hours into OpeningHoursSpecification schema type\n * @param hours - OpeningHoursSpecification object with or without @type\n * @returns OpeningHoursSpecification with @type\n */\nexport function processOpeningHours(\n hours: OpeningHoursSpecification | Omit<OpeningHoursSpecification, \"@type\">,\n): OpeningHoursSpecification {\n return processSchemaType<OpeningHoursSpecification>(\n hours,\n SCHEMA_TYPES.OPENING_HOURS,\n );\n}\n\n/**\n * Processes review into Review schema type with nested rating processing\n * @param review - Review object with or without @type\n * @returns Review with @type and processed nested fields\n */\nexport function processReview(review: Review | Omit<Review, \"@type\">): Review {\n const processed: Review = processSchemaType<Review>(\n review,\n SCHEMA_TYPES.REVIEW,\n );\n\n // Process nested rating\n if (review.reviewRating) {\n processed.reviewRating = processSchemaType<Rating>(\n review.reviewRating,\n SCHEMA_TYPES.RATING,\n );\n }\n\n // Process nested author\n if (review.author) {\n processed.author = processAuthor(review.author);\n }\n\n return processed;\n}\n\n/**\n * Processes breadcrumb item into ListItem schema type\n * @param item - BreadcrumbListItem object\n * @param position - Position in the breadcrumb trail\n * @returns ListItem with @type and position\n */\nexport function processBreadcrumbItem(\n item: BreadcrumbListItem,\n position: number,\n): ListItem {\n return {\n \"@type\": SCHEMA_TYPES.LIST_ITEM,\n position,\n ...(item.name && { name: item.name }),\n ...(item.item && { item: item.item }),\n };\n}\n\n/**\n * Processes location/place into Place schema type\n * @param location - String location or Place object\n * @returns Place with @type\n */\nexport function processPlace(\n location: string | Place | Omit<Place, \"@type\">,\n): Place {\n return processSchemaType<Place>(\n location,\n SCHEMA_TYPES.PLACE,\n (str) => ({\n name: str,\n address: {\n \"@type\": SCHEMA_TYPES.POSTAL_ADDRESS,\n streetAddress: str,\n },\n }),\n undefined,\n );\n}\n\n/**\n * Processes performer into Person or PerformingGroup schema type\n * @param performer - String name or Performer object\n * @returns Person or PerformingGroup with @type\n */\nexport function processPerformer(\n performer: Performer,\n): Person | PerformingGroup {\n if (isString(performer)) {\n return {\n \"@type\": SCHEMA_TYPES.PERFORMING_GROUP,\n name: performer,\n };\n }\n\n if (hasType<Person | PerformingGroup>(performer)) {\n return performer;\n }\n\n // Check for Person-specific properties\n const hasPersonProperties =\n \"familyName\" in performer ||\n \"givenName\" in performer ||\n \"additionalName\" in performer;\n\n return hasPersonProperties\n ? ({ \"@type\": SCHEMA_TYPES.PERSON, ...performer } as Person)\n : ({\n \"@type\": SCHEMA_TYPES.PERFORMING_GROUP,\n ...performer,\n } as PerformingGroup);\n}\n\n/**\n * Processes organizer into Person or Organization schema type\n * @param organizer - String name or Organizer object\n * @returns Person or Organization with @type\n */\nexport function processOrganizer(organizer: Organizer): Person | Organization {\n if (isString(organizer)) {\n return {\n \"@type\": SCHEMA_TYPES.ORGANIZATION,\n name: organizer,\n };\n }\n\n if (hasType<Person | Organization>(organizer)) {\n return organizer;\n }\n\n // Check for Person-specific properties\n const hasPersonProperties =\n \"familyName\" in organizer ||\n \"givenName\" in organizer ||\n \"additionalName\" in organizer;\n\n return hasPersonProperties\n ? ({ \"@type\": SCHEMA_TYPES.PERSON, ...organizer } as Person)\n : ({ \"@type\": SCHEMA_TYPES.ORGANIZATION, ...organizer } as Organization);\n}\n\n/**\n * Processes generic organization input into Organization schema type\n * @param org - String name or Organization object\n * @returns Organization with @type and processed nested fields\n */\nexport function processOrganization(\n org: string | Organization | Omit<Organization, \"@type\">,\n): Organization {\n if (isString(org)) {\n return {\n \"@type\": SCHEMA_TYPES.ORGANIZATION,\n name: org,\n };\n }\n\n const processed = processSchemaType<Organization>(\n org,\n SCHEMA_TYPES.ORGANIZATION,\n );\n\n // Process nested fields if present\n processOrganizationFields(processed);\n\n return processed;\n}\n\n/**\n * Processes offer into Offer schema type\n * @param offer - Offer object with or without @type\n * @returns Offer with @type\n */\nexport function processOffer(offer: Offer | Omit<Offer, \"@type\">): Offer {\n return processSchemaType<Offer>(offer, SCHEMA_TYPES.OFFER);\n}\n\n/**\n * Processes publisher into Person or Organization schema type\n * @param publisher - String name, Person, or Organization object\n * @returns Person or Organization with @type and processed nested fields\n */\nexport function processPublisher(\n publisher:\n | string\n | Organization\n | Person\n | Omit<Organization, \"@type\">\n | Omit<Person, \"@type\">,\n): Person | Organization {\n if (isString(publisher)) {\n return {\n \"@type\": SCHEMA_TYPES.ORGANIZATION,\n name: publisher,\n };\n }\n\n if (\n hasType<Organization>(publisher) &&\n publisher[\"@type\"] === SCHEMA_TYPES.ORGANIZATION\n ) {\n const org = { ...publisher };\n processOrganizationFields(org);\n return org;\n }\n\n if (hasType<Person | Organization>(publisher)) {\n return publisher;\n }\n\n // Default to Organization for publishers\n const org: Organization = {\n \"@type\": SCHEMA_TYPES.ORGANIZATION,\n ...publisher,\n };\n processOrganizationFields(org);\n return org;\n}\n\n/**\n * Processes nutrition information into NutritionInformation schema type\n * @param nutrition - NutritionInformation object without @type\n * @returns NutritionInformation with @type\n */\nexport function processNutrition(\n nutrition: Omit<NutritionInformation, \"@type\">,\n): NutritionInformation {\n return {\n \"@type\": SCHEMA_TYPES.NUTRITION_INFORMATION,\n ...nutrition,\n };\n}\n\n/**\n * Processes aggregate rating into AggregateRating schema type\n * @param rating - AggregateRating object with or without @type\n * @returns AggregateRating with @type\n */\nexport function processAggregateRating(\n rating: AggregateRating | Omit<AggregateRating, \"@type\">,\n): AggregateRating {\n return processSchemaType<AggregateRating>(\n rating,\n SCHEMA_TYPES.AGGREGATE_RATING,\n );\n}\n\ntype WebPage = {\n \"@type\": \"WebPage\";\n \"@id\": string;\n};\n\n/**\n * Processes main entity of page into string URL or WebPage schema type\n * @param mainEntityOfPage - String URL or WebPage object\n * @returns String URL or WebPage with @type\n */\nexport function processMainEntityOfPage(\n mainEntityOfPage: string | WebPage | Omit<WebPage, \"@type\">,\n): string | WebPage {\n if (isString(mainEntityOfPage)) {\n return mainEntityOfPage;\n }\n\n return processSchemaType<WebPage>(mainEntityOfPage, SCHEMA_TYPES.WEB_PAGE);\n}\n\n/**\n * Processes simple monetary amount into MonetaryAmount schema type for return policies\n * @param amount - SimpleMonetaryAmount object with or without @type, or a number\n * @returns SimpleMonetaryAmount with @type or undefined\n */\nexport function processSimpleMonetaryAmount(\n amount:\n | number\n | SimpleMonetaryAmount\n | Omit<SimpleMonetaryAmount, \"@type\">\n | undefined,\n): SimpleMonetaryAmount | undefined {\n if (!amount) return undefined;\n\n // Handle number input\n if (typeof amount === \"number\") {\n return {\n \"@type\": SCHEMA_TYPES.MONETARY_AMOUNT,\n value: amount,\n currency: \"USD\", // Default currency, should be overridden in component\n };\n }\n\n return processSchemaType<SimpleMonetaryAmount>(\n amount,\n SCHEMA_TYPES.MONETARY_AMOUNT,\n );\n}\n\n/**\n * Processes seasonal override into MerchantReturnPolicySeasonalOverride schema type\n * @param override - MerchantReturnPolicySeasonalOverride object with or without @type\n * @returns MerchantReturnPolicySeasonalOverride with @type\n */\nexport function processReturnPolicySeasonalOverride(\n override:\n | MerchantReturnPolicySeasonalOverride\n | Omit<MerchantReturnPolicySeasonalOverride, \"@type\">,\n): MerchantReturnPolicySeasonalOverride {\n return processSchemaType<MerchantReturnPolicySeasonalOverride>(\n override,\n SCHEMA_TYPES.MERCHANT_RETURN_POLICY_SEASONAL_OVERRIDE,\n );\n}\n\n/**\n * Processes merchant return policy into MerchantReturnPolicy schema type\n * Enhanced to handle nested properties like MonetaryAmount and seasonal overrides\n * @param policy - MerchantReturnPolicy object with or without @type\n * @returns MerchantReturnPolicy with @type and processed nested properties\n */\nexport function processMerchantReturnPolicy(\n policy: MerchantReturnPolicy | Omit<MerchantReturnPolicy, \"@type\">,\n): MerchantReturnPolicy {\n if (!policy) return policy as MerchantReturnPolicy;\n\n const processed = processSchemaType<MerchantReturnPolicy>(\n policy,\n SCHEMA_TYPES.MERCHANT_RETURN_POLICY,\n );\n\n // Normalize string values to arrays for consistency\n if (\n processed.applicableCountry &&\n !Array.isArray(processed.applicableCountry)\n ) {\n processed.applicableCountry = [processed.applicableCountry];\n }\n\n if (\n processed.returnPolicyCountry &&\n !Array.isArray(processed.returnPolicyCountry)\n ) {\n processed.returnPolicyCountry = [processed.returnPolicyCountry];\n }\n\n if (processed.returnMethod && !Array.isArray(processed.returnMethod)) {\n processed.returnMethod = [processed.returnMethod];\n }\n\n if (processed.refundType && !Array.isArray(processed.refundType)) {\n processed.refundType = [processed.refundType];\n }\n\n if (processed.itemCondition && !Array.isArray(processed.itemCondition)) {\n processed.itemCondition = [processed.itemCondition];\n }\n\n // Process nested MonetaryAmount fields\n if (processed.returnShippingFeesAmount) {\n processed.returnShippingFeesAmount = processSimpleMonetaryAmount(\n processed.returnShippingFeesAmount,\n );\n }\n\n if (processed.customerRemorseReturnShippingFeesAmount) {\n processed.customerRemorseReturnShippingFeesAmount =\n processSimpleMonetaryAmount(\n processed.customerRemorseReturnShippingFeesAmount,\n );\n }\n\n if (processed.itemDefectReturnShippingFeesAmount) {\n processed.itemDefectReturnShippingFeesAmount = processSimpleMonetaryAmount(\n processed.itemDefectReturnShippingFeesAmount,\n );\n }\n\n // Process restocking fee (can be number or SimpleMonetaryAmount)\n if (processed.restockingFee && typeof processed.restockingFee === \"object\") {\n processed.restockingFee = processSimpleMonetaryAmount(\n processed.restockingFee,\n );\n }\n\n // Process seasonal overrides\n if (processed.returnPolicySeasonalOverride) {\n if (Array.isArray(processed.returnPolicySeasonalOverride)) {\n processed.returnPolicySeasonalOverride =\n processed.returnPolicySeasonalOverride.map(\n processReturnPolicySeasonalOverride,\n );\n } else {\n processed.returnPolicySeasonalOverride =\n processReturnPolicySeasonalOverride(\n processed.returnPolicySeasonalOverride,\n );\n }\n }\n\n return processed;\n}\n\n/**\n * Processes tier requirement into appropriate schema type\n * @param requirement - Tier requirement that can be CreditCard, MonetaryAmount, UnitPriceSpecification, or string\n * @returns Processed tier requirement with @type\n */\nexport function processTierRequirement(\n requirement: TierRequirement,\n): TierRequirement {\n if (!requirement) return requirement;\n\n // If it's a string, return as-is (text description)\n if (isString(requirement)) {\n return requirement;\n }\n\n // If it already has @type, return as-is\n if (hasType(requirement)) {\n return requirement;\n }\n\n // Determine type based on properties\n if (\"priceCurrency\" in requirement && \"price\" in requirement) {\n // UnitPriceSpecification has both price and priceCurrency\n if (\n \"billingDuration\" in requirement ||\n \"billingIncrement\" in requirement ||\n \"unitCode\" in requirement\n ) {\n return {\n \"@type\": SCHEMA_TYPES.UNIT_PRICE_SPECIFICATION,\n ...requirement,\n } as UnitPriceSpecification;\n }\n }\n\n if (\"value\" in requirement && \"currency\" in requirement) {\n // MonetaryAmount\n return {\n \"@type\": SCHEMA_TYPES.MONETARY_AMOUNT,\n ...requirement,\n } as SimpleMonetaryAmount;\n }\n\n if (\"name\" in requirement) {\n // CreditCard\n return {\n \"@type\": SCHEMA_TYPES.CREDIT_CARD,\n ...requirement,\n } as CreditCard;\n }\n\n // Default to returning as-is if we can't determine the type\n return requirement;\n}\n\n/**\n * Processes tier benefit, normalizing short names to full URLs\n * @param benefit - Tier benefit string or array\n * @returns Normalized tier benefit\n */\nexport function processTierBenefit(\n benefit: TierBenefit | TierBenefit[],\n): TierBenefit | TierBenefit[] {\n const normalizeBenefit = (b: TierBenefit): TierBenefit => {\n // If it's already a full URL, return as-is\n if (b.startsWith(\"https://schema.org/\")) {\n return b;\n }\n // Convert short name to full URL\n if (b === \"TierBenefitLoyaltyPoints\") {\n return \"https://schema.org/TierBenefitLoyaltyPoints\";\n }\n if (b === \"TierBenefitLoyaltyPrice\") {\n return \"https://schema.org/TierBenefitLoyaltyPrice\";\n }\n // Return as-is if unrecognized\n return b;\n };\n\n if (Array.isArray(benefit)) {\n return benefit.map(normalizeBenefit);\n }\n return normalizeBenefit(benefit);\n}\n\n/**\n * Processes membership points earned into QuantitativeValue\n * @param points - Number or QuantitativeValue\n * @returns QuantitativeValue with @type\n */\nexport function processMembershipPointsEarned(\n points: number | QuantitativeValue | Omit<QuantitativeValue, \"@type\">,\n): QuantitativeValue {\n if (typeof points === \"number\") {\n return {\n \"@type\": SCHEMA_TYPES.QUANTITATIVE_VALUE,\n value: points,\n };\n }\n return processSchemaType<QuantitativeValue>(\n points,\n SCHEMA_TYPES.QUANTITATIVE_VALUE,\n );\n}\n\n/**\n * Processes member program tier into MemberProgramTier schema type\n * @param tier - MemberProgramTier with or without @type\n * @returns MemberProgramTier with @type\n */\nexport function processMemberProgramTier(\n tier: MemberProgramTier | Omit<MemberProgramTier, \"@type\">,\n): MemberProgramTier {\n const processed = processSchemaType<MemberProgramTier>(\n tier,\n SCHEMA_TYPES.MEMBER_PROGRAM_TIER,\n );\n\n // Process hasTierBenefit\n if (processed.hasTierBenefit) {\n processed.hasTierBenefit = processTierBenefit(processed.hasTierBenefit);\n }\n\n // Process hasTierRequirement\n if (processed.hasTierRequirement) {\n processed.hasTierRequirement = processTierRequirement(\n processed.hasTierRequirement,\n );\n }\n\n // Process membershipPointsEarned\n if (processed.membershipPointsEarned !== undefined) {\n processed.membershipPointsEarned = processMembershipPointsEarned(\n processed.membershipPointsEarned,\n );\n }\n\n return processed;\n}\n\n/**\n * Processes member program into MemberProgram schema type\n * @param program - MemberProgram with or without @type\n * @returns MemberProgram with @type\n */\nexport function processMemberProgram(\n program: MemberProgram | Omit<MemberProgram, \"@type\">,\n): MemberProgram {\n const processed = processSchemaType<MemberProgram>(\n program,\n SCHEMA_TYPES.MEMBER_PROGRAM,\n );\n\n // Process hasTiers\n if (processed.hasTiers) {\n if (Array.isArray(processed.hasTiers)) {\n processed.hasTiers = processed.hasTiers.map(processMemberProgramTier);\n } else {\n processed.hasTiers = processMemberProgramTier(processed.hasTiers);\n }\n }\n\n return processed;\n}\n\n/**\n * Processes video into VideoObject schema type\n * @param video - VideoObject with or without @type\n * @returns VideoObject with @type\n */\nexport function processVideo(\n video: VideoObject | Omit<VideoObject, \"@type\">,\n): VideoObject {\n return processSchemaType<VideoObject>(video, SCHEMA_TYPES.VIDEO_OBJECT);\n}\n\n/**\n * Processes broadcast event into BroadcastEvent schema type\n * @param broadcast - BroadcastEvent with or without @type\n * @returns BroadcastEvent with @type\n */\nexport function processBroadcastEvent(\n broadcast: BroadcastEvent | Omit<BroadcastEvent, \"@type\">,\n): BroadcastEvent {\n if (!broadcast) return broadcast as BroadcastEvent;\n\n if (typeof broadcast === \"object\" && !(\"@type\" in broadcast)) {\n return {\n \"@type\": \"BroadcastEvent\",\n ...broadcast,\n };\n }\n\n return broadcast as BroadcastEvent;\n}\n\n/**\n * Processes clip into Clip schema type\n * @param clip - Clip with or without @type\n * @returns Clip with @type\n */\nexport function processClip(clip: Clip | Omit<Clip, \"@type\">): Clip {\n if (!clip) return clip as Clip;\n\n if (typeof clip === \"object\" && !(\"@type\" in clip)) {\n return {\n \"@type\": \"Clip\",\n ...clip,\n };\n }\n\n return clip as Clip;\n}\n\n/**\n * Processes seek action into SeekToAction schema type\n * @param action - SeekToAction with or without @type\n * @returns SeekToAction with @type\n */\nexport function processSeekToAction(\n action: PotentialAction | Omit<PotentialAction, \"@type\">,\n): PotentialAction {\n if (!action) return action as PotentialAction;\n\n if (typeof action === \"object\" && !(\"@type\" in action)) {\n return {\n \"@type\": \"SeekToAction\",\n ...action,\n };\n }\n\n return action as PotentialAction;\n}\n\n/**\n * Processes instruction into string, HowToStep, or HowToSection schema type\n * @param instruction - String instruction or HowTo object\n * @returns String, HowToStep, or HowToSection with @type\n */\nexport function processInstruction(\n instruction:\n | string\n | HowToStep\n | HowToSection\n | Omit<HowToStep, \"@type\">\n | Omit<HowToSection, \"@type\">,\n): string | HowToStep | HowToSection {\n if (isString(instruction)) {\n return instruction;\n }\n\n if (hasType<HowToStep | HowToSection>(instruction)) {\n // Process nested items if it's a section\n if (\n instruction[\"@type\"] === SCHEMA_TYPES.HOW_TO_SECTION &&\n \"itemListElement\" in instruction\n ) {\n return {\n ...instruction,\n itemListElement: instruction.itemListElement.map((item) =>\n processInstruction(item),\n ),\n } as HowToSection;\n }\n return instruction;\n }\n\n // Determine type based on properties\n if (\"itemListElement\" in instruction) {\n return {\n \"@type\": SCHEMA_TYPES.HOW_TO_SECTION,\n ...instruction,\n itemListElement: instruction.itemListElement.map((item) =>\n processInstruction(item),\n ),\n } as HowToSection;\n }\n\n return {\n \"@type\": SCHEMA_TYPES.HOW_TO_STEP,\n ...instruction,\n } as HowToStep;\n}\n\n/**\n * Processes director into Person schema type\n * @param director - String name or Director object\n * @returns Person with @type\n */\nexport function processDirector(director: Director): Person {\n return processSchemaType<Person>(\n director,\n SCHEMA_TYPES.PERSON,\n (str) => ({ name: str }),\n undefined,\n );\n}\n\n// Dataset-specific processors\n\n/**\n * Processes creator(s) into Person or Organization schema type(s)\n * @param creator - Author or array of Authors\n * @returns Person/Organization or array of them with @type\n */\nexport function processCreator(\n creator: Author | Author[],\n): Person | Organization | (Person | Organization)[] {\n return Array.isArray(creator)\n ? creator.map(processAuthor)\n : processAuthor(creator);\n}\n\n/**\n * Processes identifier into string or PropertyValue schema type\n * @param identifier - String identifier or PropertyValue object\n * @returns String or PropertyValue with @type\n */\nexport function processIdentifier(\n identifier: string | PropertyValue | Omit<PropertyValue, \"@type\">,\n): string | PropertyValue {\n if (isString(identifier)) {\n return identifier;\n }\n\n return processSchemaType<PropertyValue>(\n identifier,\n SCHEMA_TYPES.PROPERTY_VALUE,\n );\n}\n\n/**\n * Processes spatial coverage into string or Place schema type\n * @param spatial - String location or DatasetPlace object\n * @returns String or DatasetPlace with @type and processed geo\n */\nexport function processSpatialCoverage(\n spatial: string | DatasetPlace | Omit<DatasetPlace, \"@type\">,\n): string | DatasetPlace {\n if (isString(spatial)) {\n return spatial;\n }\n\n const processed: DatasetPlace = processSchemaType<DatasetPlace>(\n spatial,\n SCHEMA_TYPES.PLACE,\n );\n\n // Process nested geo if present\n if (spatial.geo && typeof spatial.geo === \"object\" && !hasType(spatial.geo)) {\n if (\"latitude\" in spatial.geo && \"longitude\" in spatial.geo) {\n processed.geo = processSchemaType<GeoCoordinates>(\n spatial.geo,\n SCHEMA_TYPES.GEO_COORDINATES,\n );\n } else if (\n \"box\" in spatial.geo ||\n \"circle\" in spatial.geo ||\n \"line\" in spatial.geo ||\n \"polygon\" in spatial.geo\n ) {\n processed.geo = processSchemaType<GeoShape>(\n spatial.geo,\n SCHEMA_TYPES.GEO_SHAPE,\n );\n }\n }\n\n return processed;\n}\n\n/**\n * Processes data download into DataDownload schema type\n * @param download - DataDownload object with or without @type\n * @returns DataDownload with @type\n */\nexport function processDataDownload(\n download: DataDownload | Omit<DataDownload, \"@type\">,\n): DataDownload {\n return processSchemaType<DataDownload>(download, SCHEMA_TYPES.DATA_DOWNLOAD);\n}\n\n/**\n * Processes license into string URL or CreativeWork schema type\n * @param license - String URL or CreativeWork object\n * @returns String URL or CreativeWork with @type\n */\nexport function processLicense(\n license: string | CreativeWork | Omit<CreativeWork, \"@type\">,\n): string | CreativeWork {\n if (isString(license)) {\n return license;\n }\n\n return processSchemaType<CreativeWork>(license, SCHEMA_TYPES.CREATIVE_WORK);\n}\n\n/**\n * Processes data catalog into DataCatalog schema type\n * @param catalog - DataCatalog object with or without @type\n * @returns DataCatalog with @type\n */\nexport function processDataCatalog(\n catalog: DataCatalog | Omit<DataCatalog, \"@type\">,\n): DataCatalog {\n return processSchemaType<DataCatalog>(catalog, SCHEMA_TYPES.DATA_CATALOG);\n}\n\n// JobPosting-specific processors\n\n/**\n * Processes hiring organization into Organization schema type\n * @param org - String name or Organization object\n * @returns Organization with @type and processed logo\n */\nexport function processHiringOrganization(\n org: string | Organization | Omit<Organization, \"@type\">,\n): Organization {\n if (isString(org)) {\n return {\n \"@type\": SCHEMA_TYPES.ORGANIZATION,\n name: org,\n };\n }\n\n const processed = processSchemaType<Organization>(\n org,\n SCHEMA_TYPES.ORGANIZATION,\n );\n\n // Process nested logo if present\n if (processed.logo && !isString(processed.logo)) {\n processed.logo = processImage(processed.logo);\n }\n\n return processed;\n}\n\n/**\n * Processes job location into Place schema type\n * @param location - String location or JobPlace object\n * @returns JobPlace with @type and processed address\n */\nexport function processJobLocation(\n location: string | JobPlace | Omit<JobPlace, \"@type\">,\n): JobPlace {\n if (isString(location)) {\n return {\n \"@type\": SCHEMA_TYPES.PLACE,\n address: {\n \"@type\": SCHEMA_TYPES.POSTAL_ADDRESS,\n streetAddress: location,\n },\n };\n }\n\n const processed = processSchemaType<JobPlace>(location, SCHEMA_TYPES.PLACE);\n\n // Process nested address\n if (processed.address && !isString(processed.address)) {\n processed.address = processAddress(processed.address);\n }\n\n return processed;\n}\n\n/**\n * Processes monetary amount into MonetaryAmount schema type\n * @param amount - MonetaryAmount object with or without @type\n * @returns MonetaryAmount with @type and processed value\n */\nexport function processMonetaryAmount(\n amount: MonetaryAmount | Omit<MonetaryAmount, \"@type\">,\n): MonetaryAmount {\n const processed = processSchemaType<MonetaryAmount>(amount, \"MonetaryAmount\");\n\n // Process nested value as QuantitativeValue\n processed.value = processSchemaType<QuantitativeValue>(\n amount.value,\n SCHEMA_TYPES.QUANTITATIVE_VALUE,\n );\n\n return processed;\n}\n\n/**\n * Processes rating into Rating schema type\n * @param rating - Rating object with or without @type\n * @returns Rating with @type\n */\nexport function processRating(rating: Rating | Omit<Rating, \"@type\">): Rating {\n return processSchemaType<Rating>(rating, SCHEMA_TYPES.RATING);\n}\n\n/**\n * Processes job property value into PropertyValue schema type\n * @param identifier - String identifier or JobPropertyValue object\n * @returns JobPropertyValue with @type\n */\nexport function processJobPropertyValue(\n identifier: string | JobPropertyValue | Omit<JobPropertyValue, \"@type\">,\n): JobPropertyValue {\n return processSchemaType<JobPropertyValue>(\n identifier,\n SCHEMA_TYPES.PROPERTY_VALUE,\n (str) => ({ value: str }),\n undefined,\n );\n}\n\n/**\n * Processes applicant location requirements into Country or State schema type\n * @param location - Location requirement object\n * @returns AdministrativeArea (Country or State) with @type\n */\nexport function processApplicantLocationRequirements(\n location: Omit<Country, \"@type\"> | Omit<State, \"@type\"> | Country | State,\n): AdministrativeArea {\n if (hasType<Country | State>(location)) {\n return location;\n }\n\n // Improved detection logic\n const name = location.name;\n const statePatterns = [\n /\\b[A-Z]{2}\\b/, // Two-letter state codes\n /\\bstate\\b/i, // Contains \"state\"\n /,/, // Contains comma (often \"City, State\")\n /\\b(AL|AK|AZ|AR|CA|CO|CT|DE|FL|GA|HI|ID|IL|IN|IA|KS|KY|LA|ME|MD|MA|MI|MN|MS|MO|MT|NE|NV|NH|NJ|NM|NY|NC|ND|OH|OK|OR|PA|RI|SC|SD|TN|TX|UT|VT|VA|WA|WV|WI|WY)\\b/, // US state codes\n ];\n\n const isState = statePatterns.some((pattern) => pattern.test(name));\n\n return {\n \"@type\": isState ? SCHEMA_TYPES.STATE : SCHEMA_TYPES.COUNTRY,\n ...location,\n } as AdministrativeArea;\n}\n\n/**\n * Processes education requirements into string or EducationalOccupationalCredential\n * @param education - String description or credential object\n * @returns String or EducationalOccupationalCredential with @type\n */\nexport function processEducationRequirements(\n education:\n | string\n | EducationalOccupationalCredential\n | Omit<EducationalOccupationalCredential, \"@type\">,\n): string | EducationalOccupationalCredential {\n if (isString(education)) {\n return education;\n }\n\n return processSchemaType<EducationalOccupationalCredential>(\n education,\n SCHEMA_TYPES.EDUCATIONAL_CREDENTIAL,\n );\n}\n\n/**\n * Processes experience requirements into string or OccupationalExperienceRequirements\n * @param experience - String description or experience object\n * @returns String or OccupationalExperienceRequirements with @type\n */\nexport function processExperienceRequirements(\n experience:\n | string\n | OccupationalExperienceRequirements\n | Omit<OccupationalExperienceRequirements, \"@type\">,\n): string | OccupationalExperienceRequirements {\n if (isString(experience)) {\n return experience;\n }\n\n return processSchemaType<OccupationalExperienceRequirements>(\n experience,\n SCHEMA_TYPES.OCCUPATIONAL_EXPERIENCE,\n );\n}\n\n// DiscussionForumPosting-specific processors\n\n/**\n * Processes interaction statistic into InteractionCounter schema type\n * @param statistic - InteractionCounter object with or without @type\n * @returns InteractionCounter with @type\n */\nexport function processInteractionStatistic(\n statistic: InteractionCounter | Omit<InteractionCounter, \"@type\">,\n): InteractionCounter {\n return processSchemaType<InteractionCounter>(\n statistic,\n SCHEMA_TYPES.INTERACTION_COUNTER,\n );\n}\n\n/**\n * Processes shared content into WebPage, ImageObject, or VideoObject schema type\n * @param content - SharedContent (string URL or object)\n * @returns ForumWebPage, ImageObject, or VideoObject with @type\n */\nexport function processSharedContent(\n content: SharedContent,\n): ForumWebPage | ImageObject | VideoObject {\n if (isString(content)) {\n return {\n \"@type\": SCHEMA_TYPES.WEB_PAGE,\n url: content,\n };\n }\n\n if (hasType<ForumWebPage | ImageObject | VideoObject>(content)) {\n return content;\n }\n\n // Improved type detection\n const hasVideoProperties =\n \"uploadDate\" in content && \"thumbnailUrl\" in content;\n const hasImageProperties =\n \"url\" in content && (\"width\" in content || \"height\" in content);\n\n if (hasVideoProperties) {\n return processSchemaType<VideoObject>(content, SCHEMA_TYPES.VIDEO_OBJECT);\n }\n\n if (hasImageProperties) {\n return processSchemaType<ImageObject>(content, SCHEMA_TYPES.IMAGE_OBJECT);\n }\n\n // Default to WebPage\n return processSchemaType<ForumWebPage>(content, SCHEMA_TYPES.WEB_PAGE);\n}\n\n/**\n * Processes comment into Comment schema type with nested fields\n * @param comment - Comment object with or without @type\n * @returns Comment with @type and processed nested fields\n */\nexport function processComment(\n comment: Comment | Omit<Comment, \"@type\">,\n): Comment {\n const processed: Comment = processSchemaType<Comment>(\n comment,\n SCHEMA_TYPES.COMMENT,\n );\n\n // Process nested fields\n if (comment.author) {\n processed.author = processAuthor(comment.author);\n }\n\n if (comment.i