UNPKG

@unhead/schema-org

Version:

Unhead Schema.org for Simple and Automated Google Rich Results

1,646 lines (1,587 loc) 65.3 kB
import { hasProtocol, withBase, withoutTrailingSlash, hasTrailingSlash, withTrailingSlash, joinURL } from 'ufo'; import { defineHeadPlugin, TemplateParamsPlugin } from 'unhead/plugins'; import { processTemplateParams } from 'unhead/utils'; function defineSchemaOrgResolver(schema) { return schema; } function idReference(node) { return { "@id": typeof node !== "string" ? node["@id"] : node }; } function resolvableDateToDate(val) { try { const date = val instanceof Date ? val : new Date(Date.parse(val)); const month = String(date.getMonth() + 1).padStart(2, "0"); const day = String(date.getDate()).padStart(2, "0"); return `${date.getFullYear()}-${month}-${day}`; } catch { } return typeof val === "string" ? val : val.toString(); } const IS_VALID_W3C_DATE = [ /(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z))/, /^\d{4}-[01]\d-[0-3]\d$/, /^\d{4}-[01]\d$/, /^\d{4}$/ ]; function isValidW3CDate(d) { return IS_VALID_W3C_DATE.some((r) => r.test(d)); } function resolvableDateToIso(val) { if (!val) return val; try { if (val instanceof Date) return val.toISOString(); else if (isValidW3CDate(val)) return val; else return new Date(Date.parse(val)).toISOString(); } catch { } return typeof val === "string" ? val : val.toString(); } const IdentityId = "#identity"; function setIfEmpty(node, field, value) { if (node?.[field] === void 0 && value != null) node[field] = value; } function asArray(input) { return Array.isArray(input) ? input : [input]; } function dedupeMerge(node, field, value) { const data = new Set(asArray(node[field])); data.add(value); node[field] = [...data].filter(Boolean); } function prefixId(url, id) { if (hasProtocol(id)) return id; if (!id.includes("#")) id = `#${id}`; return `${url || ""}${id}`; } function trimLength(val, length) { if (!val) return val; if (val.length > length) { const trimmedString = val.substring(0, length); return trimmedString.substring(0, Math.min(trimmedString.length, trimmedString.lastIndexOf(" "))); } return val; } function resolveDefaultType(node, defaultType) { const val = node["@type"]; if (val === defaultType) return; if (typeof val === "string" && typeof defaultType === "string") { if (val !== defaultType) node["@type"] = [defaultType, val]; return; } const types = new Set(asArray(defaultType)); for (const t of asArray(val)) types.add(t); node["@type"] = types.size === 1 ? val : [...types]; } function resolveWithBase(base, urlOrPath) { if (!urlOrPath || hasProtocol(urlOrPath) || urlOrPath[0] !== "/" && urlOrPath[0] !== "#") return urlOrPath; return withBase(urlOrPath, base); } function resolveAsGraphKey(key) { if (!key) return key; return key.substring(key.lastIndexOf("#")); } function stripEmptyProperties(obj) { for (const k in obj) { if (!Object.hasOwn(obj, k)) continue; const v = obj[k]; if (v === "" || v === void 0) { delete obj[k]; } else if (typeof v === "object" && v !== null) { if (v.__v_isReadonly || v.__v_isRef) continue; stripEmptyProperties(v); } } return obj; } function stripNullProperties(obj) { if (Array.isArray(obj)) { for (let i = obj.length - 1; i >= 0; i--) { const v = obj[i]; if (v === null) obj.splice(i, 1); else if (typeof v === "object" && v !== null) stripNullProperties(v); } return obj; } for (const k in obj) { if (!Object.hasOwn(obj, k)) continue; const v = obj[k]; if (v === null) { delete obj[k]; } else if (typeof v === "object") { if (v.__v_isReadonly || v.__v_isRef) continue; stripNullProperties(v); } } return obj; } const quantitativeValueResolver = defineSchemaOrgResolver({ cast(node) { if (typeof node === "number") { return { value: node }; } return node; }, defaults: { "@type": "QuantitativeValue" } }); const monetaryAmountResolver = defineSchemaOrgResolver({ defaults: { "@type": "MonetaryAmount" }, resolve(node, ctx) { if (typeof node.value !== "number") node.value = resolveRelation(node.value, ctx, quantitativeValueResolver); return node; } }); const merchantReturnPolicyResolver = defineSchemaOrgResolver({ defaults: { "@type": "MerchantReturnPolicy" }, resolve(node, ctx) { if (node.returnPolicyCategory) node.returnPolicyCategory = withBase(node.returnPolicyCategory, "https://schema.org/"); if (node.returnFees) node.returnFees = withBase(node.returnFees, "https://schema.org/"); if (node.returnMethod) node.returnMethod = withBase(node.returnMethod, "https://schema.org/"); node.returnShippingFeesAmount = resolveRelation(node.returnShippingFeesAmount, ctx, monetaryAmountResolver); return node; } }); const definedRegionResolver = defineSchemaOrgResolver({ defaults: { "@type": "DefinedRegion" } }); const shippingDeliveryTimeResolver = defineSchemaOrgResolver({ defaults: { "@type": "ShippingDeliveryTime" }, resolve(node, ctx) { node.handlingTime = resolveRelation(node.handlingTime, ctx, quantitativeValueResolver); node.transitTime = resolveRelation(node.transitTime, ctx, quantitativeValueResolver); return node; } }); const offerShippingDetailsResolver = defineSchemaOrgResolver({ defaults: { "@type": "OfferShippingDetails" }, resolve(node, ctx) { node.deliveryTime = resolveRelation(node.deliveryTime, ctx, shippingDeliveryTimeResolver); node.shippingDestination = resolveRelation(node.shippingDestination, ctx, definedRegionResolver); node.shippingRate = resolveRelation(node.shippingRate, ctx, monetaryAmountResolver); return node; } }); const offerResolver = defineSchemaOrgResolver({ cast(node) { if (typeof node === "number" || typeof node === "string") { return { price: node }; } return node; }, defaults: { "@type": "Offer", "availability": "InStock" }, resolve(node, ctx) { setIfEmpty(node, "priceCurrency", ctx.meta.currency); setIfEmpty(node, "priceValidUntil", new Date(Date.UTC((/* @__PURE__ */ new Date()).getFullYear() + 1, 12, -1, 0, 0, 0))); if (node.url) resolveWithBase(ctx.meta.host, node.url); if (node.availability) node.availability = withBase(node.availability, "https://schema.org/"); if (node.itemCondition) node.itemCondition = withBase(node.itemCondition, "https://schema.org/"); if (node.priceValidUntil) node.priceValidUntil = resolvableDateToIso(node.priceValidUntil); node.hasMerchantReturnPolicy = resolveRelation(node.hasMerchantReturnPolicy, ctx, merchantReturnPolicyResolver); node.shippingDetails = resolveRelation(node.shippingDetails, ctx, offerShippingDetailsResolver); return node; } }); const aggregateOfferResolver = defineSchemaOrgResolver({ defaults: { "@type": "AggregateOffer" }, inheritMeta: [ { meta: "currency", key: "priceCurrency" } ], resolve(node, ctx) { node.offers = resolveRelation(node.offers, ctx, offerResolver); if (node.offers) setIfEmpty(node, "offerCount", asArray(node.offers).length); return node; } }); const aggregateRatingResolver = defineSchemaOrgResolver({ defaults: { "@type": "AggregateRating" } }); const listItemResolver = defineSchemaOrgResolver({ cast(node) { if (typeof node === "string") { node = { name: node }; } return node; }, defaults: { "@type": "ListItem" }, resolve(node, ctx) { if (typeof node.item === "string") node.item = resolveWithBase(ctx.meta.host, node.item); else if (typeof node.item === "object") node.item = resolveRelation(node.item, ctx); return node; } }); const PrimaryBreadcrumbId = "#breadcrumb"; const breadcrumbResolver = defineSchemaOrgResolver({ defaults: { "@type": "BreadcrumbList" }, idPrefix: ["url", PrimaryBreadcrumbId], resolve(breadcrumb, ctx) { if (breadcrumb.itemListElement) { let index = 1; breadcrumb.itemListElement = resolveRelation(breadcrumb.itemListElement, ctx, listItemResolver, { array: true, afterResolve(node) { setIfEmpty(node, "position", index++); } }); } return breadcrumb; }, resolveRootNode(node, { find }) { const webPage = find(PrimaryWebPageId); if (webPage) setIfEmpty(webPage, "breadcrumb", idReference(node)); } }); const imageResolver = defineSchemaOrgResolver({ alias: "image", cast(input) { if (typeof input === "string") { input = { url: input }; } return input; }, defaults: { "@type": "ImageObject" }, inheritMeta: [ // @todo possibly only do if there's a caption "inLanguage" ], idPrefix: "host", resolve(image, { meta }) { image.url = resolveWithBase(meta.host, image.url); setIfEmpty(image, "contentUrl", image.url); if (image.height && !image.width) delete image.height; if (image.width && !image.height) delete image.width; return image; } }); const addressResolver = defineSchemaOrgResolver({ defaults: { "@type": "PostalAddress" } }); const searchActionResolver = defineSchemaOrgResolver({ defaults: { "@type": "SearchAction", "target": { "@type": "EntryPoint" }, "query-input": { "@type": "PropertyValueSpecification", "valueRequired": true, "valueName": "search_term_string" } }, resolve(node, ctx) { if (typeof node.target === "string") { node.target = { "@type": "EntryPoint", "urlTemplate": resolveWithBase(ctx.meta.host, node.target) }; } return node; } }); const PrimaryWebSiteId = "#website"; const webSiteResolver = defineSchemaOrgResolver({ defaults: { "@type": "WebSite" }, inheritMeta: [ "inLanguage", { meta: "host", key: "url" } ], idPrefix: ["host", PrimaryWebSiteId], resolve(node, ctx) { node.potentialAction = resolveRelation(node.potentialAction, ctx, searchActionResolver, { array: true }); node.publisher = resolveRelation(node.publisher, ctx); node.dateModified = resolvableDateToIso(node.dateModified); node.datePublished = resolvableDateToIso(node.datePublished); return node; }, resolveRootNode(node, { find }) { if (resolveAsGraphKey(node["@id"]) === PrimaryWebSiteId) { const identity = find(IdentityId); if (identity) setIfEmpty(node, "publisher", idReference(identity)); const webPage = find(PrimaryWebPageId); if (webPage) setIfEmpty(webPage, "isPartOf", idReference(node)); } return node; } }); const organizationResolver = defineSchemaOrgResolver({ defaults: { "@type": "Organization" }, idPrefix: ["host", IdentityId], inheritMeta: [ { meta: "host", key: "url" } ], resolve(node, ctx) { resolveDefaultType(node, "Organization"); node.address = resolveRelation(node.address, ctx, addressResolver); return node; }, resolveRootNode(node, ctx) { const isIdentity = resolveAsGraphKey(node["@id"]) === IdentityId; const webPage = ctx.find(PrimaryWebPageId); if (node.logo && isIdentity) { if (!ctx.find("#organization")) { const logoNode = resolveRelation(node.logo, ctx, imageResolver, { root: true, afterResolve(logo) { logo["@id"] = prefixId(ctx.meta.host, "#logo"); setIfEmpty(logo, "caption", node.name); } }); if (webPage && logoNode) setIfEmpty(webPage, "primaryImageOfPage", idReference(logoNode)); ctx.nodes.push({ // we want to make a simple node that has the essentials, this will allow parent nodes to inject // as well without inserting invalid data (i.e LocalBusiness operatingHours) "@type": "Organization", "name": node.name, "url": node.url, "sameAs": node.sameAs, // 'image': idReference(logoNode), "address": node.address, // needs to be a URL "logo": resolveRelation(node.logo, ctx, imageResolver, { root: false }).url, "_priority": -1, "@id": prefixId(ctx.meta.host, "#organization") // avoid the id so nothing can link to it }); } delete node.logo; } if (isIdentity && webPage) setIfEmpty(webPage, "about", idReference(node)); const webSite = ctx.find(PrimaryWebSiteId); if (webSite) setIfEmpty(webSite, "publisher", idReference(node)); } }); const readActionResolver = defineSchemaOrgResolver({ defaults: { "@type": "ReadAction" }, resolve(node, ctx) { if (!node.target.includes(ctx.meta.url)) node.target.unshift(ctx.meta.url); return node; } }); const PrimaryWebPageId = "#webpage"; const webPageResolver = defineSchemaOrgResolver({ defaults({ meta }) { const endPath = withoutTrailingSlash(meta.url.substring(meta.url.lastIndexOf("/") + 1)); let type = "WebPage"; switch (endPath) { case "about": case "about-us": type = "AboutPage"; break; case "search": type = "SearchResultsPage"; break; case "checkout": type = "CheckoutPage"; break; case "contact": case "get-in-touch": case "contact-us": type = "ContactPage"; break; case "faq": type = "FAQPage"; break; } const defaults = { "@type": type }; return defaults; }, idPrefix: ["url", PrimaryWebPageId], inheritMeta: [ { meta: "title", key: "name" }, "description", "datePublished", "dateModified", "url" ], resolve(node, ctx) { node.dateModified = resolvableDateToIso(node.dateModified); node.datePublished = resolvableDateToIso(node.datePublished); resolveDefaultType(node, "WebPage"); node.about = resolveRelation(node.about, ctx, organizationResolver); node.breadcrumb = resolveRelation(node.breadcrumb, ctx, breadcrumbResolver); node.author = resolveRelation(node.author, ctx, personResolver); node.primaryImageOfPage = resolveRelation(node.primaryImageOfPage, ctx, imageResolver); node.potentialAction = resolveRelation(node.potentialAction, ctx, readActionResolver); if (node["@type"] === "WebPage" && ctx.meta.url) { setIfEmpty(node, "potentialAction", [ { "@type": "ReadAction", "target": [ctx.meta.url] } ]); } return node; }, resolveRootNode(webPage, { find, meta }) { const identity = find(IdentityId); const webSite = find(PrimaryWebSiteId); const logo = find("#logo"); if (identity && meta.url === meta.host) setIfEmpty(webPage, "about", idReference(identity)); if (logo) setIfEmpty(webPage, "primaryImageOfPage", idReference(logo)); if (webSite) setIfEmpty(webPage, "isPartOf", idReference(webSite)); const breadcrumb = find(PrimaryBreadcrumbId); if (breadcrumb) setIfEmpty(webPage, "breadcrumb", idReference(breadcrumb)); return webPage; } }); const personResolver = defineSchemaOrgResolver({ cast(node) { if (typeof node === "string") { return { name: node }; } return node; }, defaults: { "@type": "Person" }, idPrefix: ["host", IdentityId], resolve(node, ctx) { if (node.url) node.url = resolveWithBase(ctx.meta.host, node.url); return node; }, resolveRootNode(node, { find, meta }) { if (resolveAsGraphKey(node["@id"]) === IdentityId) { setIfEmpty(node, "url", meta.host); const webPage = find(PrimaryWebPageId); if (webPage) setIfEmpty(webPage, "about", idReference(node)); const webSite = find(PrimaryWebSiteId); if (webSite) setIfEmpty(webSite, "publisher", idReference(node)); } const article = find(PrimaryArticleId); if (article) setIfEmpty(article, "author", idReference(node)); } }); const PrimaryArticleId = "#article"; const articleResolver = defineSchemaOrgResolver({ defaults: { "@type": "Article" }, inheritMeta: [ "inLanguage", "description", "image", "dateModified", "datePublished", { meta: "title", key: "headline" } ], idPrefix: ["url", PrimaryArticleId], resolve(node, ctx) { node.author = resolveRelation(node.author, ctx, personResolver, { root: true }); node.publisher = resolveRelation(node.publisher, ctx); node.dateModified = resolvableDateToIso(node.dateModified); node.datePublished = resolvableDateToIso(node.datePublished); resolveDefaultType(node, "Article"); node.headline = trimLength(node.headline, 110); return node; }, resolveRootNode(node, { find, meta }) { const webPage = find(PrimaryWebPageId); const identity = find(IdentityId); if (node.image && !node.thumbnailUrl) { const firstImage = asArray(node.image)[0]; if (typeof firstImage === "string") setIfEmpty(node, "thumbnailUrl", resolveWithBase(meta.host, firstImage)); else if (firstImage?.["@id"]) setIfEmpty(node, "thumbnailUrl", find(firstImage["@id"])?.url); } if (identity) { setIfEmpty(node, "publisher", idReference(identity)); setIfEmpty(node, "author", idReference(identity)); } if (webPage) { setIfEmpty(node, "isPartOf", idReference(webPage)); setIfEmpty(node, "mainEntityOfPage", idReference(webPage)); setIfEmpty(webPage, "potentialAction", [ { "@type": "ReadAction", "target": [meta.url] } ]); setIfEmpty(webPage, "dateModified", node.dateModified); setIfEmpty(webPage, "datePublished", node.datePublished); } return node; } }); const bookEditionResolver = defineSchemaOrgResolver({ defaults: { "@type": "Book" }, inheritMeta: [ "inLanguage" ], resolve(node, ctx) { if (node.bookFormat) node.bookFormat = withBase(node.bookFormat, "https://schema.org/"); if (node.datePublished) node.datePublished = resolvableDateToDate(node.datePublished); node.author = resolveRelation(node.author, ctx); return node; }, resolveRootNode(node, { find }) { const identity = find(IdentityId); if (identity) setIfEmpty(node, "provider", idReference(identity)); return node; } }); const PrimaryBookId = "#book"; const bookResolver = defineSchemaOrgResolver({ defaults: { "@type": "Book" }, inheritMeta: [ "description", "url", { meta: "title", key: "name" } ], idPrefix: ["url", PrimaryBookId], resolve(node, ctx) { node.workExample = resolveRelation(node.workExample, ctx, bookEditionResolver); node.author = resolveRelation(node.author, ctx); if (node.url) withBase(node.url, ctx.meta.host); return node; }, resolveRootNode(node, { find }) { const identity = find(IdentityId); if (identity) setIfEmpty(node, "author", idReference(identity)); return node; } }); const commentResolver = defineSchemaOrgResolver({ defaults: { "@type": "Comment" }, idPrefix: "url", resolve(node, ctx) { node.author = resolveRelation(node.author, ctx, personResolver, { root: true }); node.dateCreated = resolvableDateToIso(node.dateCreated); node.dateModified = resolvableDateToIso(node.dateModified); return node; }, resolveRootNode(node, { find }) { const article = find(PrimaryArticleId); if (article) setIfEmpty(node, "about", idReference(article)); } }); const courseResolver = defineSchemaOrgResolver({ defaults: { "@type": "Course" }, resolve(node, ctx) { node.provider = resolveRelation(node.provider, ctx, organizationResolver, { root: true }); return node; }, resolveRootNode(node, { find }) { const identity = find(IdentityId); if (identity) setIfEmpty(node, "provider", idReference(identity)); return node; } }); const PrimaryDatasetId = "#dataset"; const datasetResolver = defineSchemaOrgResolver({ defaults: { "@type": "Dataset" }, inheritMeta: [ "description", "url", "dateModified", "datePublished", { meta: "title", key: "name" } ], idPrefix: ["url", PrimaryDatasetId], resolve(node, ctx) { resolveDefaultType(node, "Dataset"); node.creator = resolveRelation(node.creator, ctx, personResolver, { root: true }); node.dateModified = resolvableDateToIso(node.dateModified); node.datePublished = resolvableDateToIso(node.datePublished); return node; } }); const placeResolver = defineSchemaOrgResolver({ defaults: { "@type": "Place" }, resolve(node, ctx) { if (typeof node.address !== "string") node.address = resolveRelation(node.address, ctx, addressResolver); return node; } }); const virtualLocationResolver = defineSchemaOrgResolver({ cast(node) { if (typeof node === "string") { return { url: node }; } return node; }, defaults: { "@type": "VirtualLocation" } }); const PrimaryEventId = "#event"; const eventResolver = defineSchemaOrgResolver({ defaults: { "@type": "Event" }, inheritMeta: [ "inLanguage", "description", "image", { meta: "title", key: "name" } ], idPrefix: ["url", PrimaryEventId], resolve(node, ctx) { if (node.location) { const isVirtual = node.location === "string" || node.location?.url !== "undefined"; node.location = resolveRelation(node.location, ctx, isVirtual ? virtualLocationResolver : placeResolver); } node.performer = resolveRelation(node.performer, ctx, personResolver, { root: true }); node.organizer = resolveRelation(node.organizer, ctx, organizationResolver, { root: true }); node.offers = resolveRelation(node.offers, ctx, offerResolver); if (node.eventAttendanceMode) node.eventAttendanceMode = withBase(node.eventAttendanceMode, "https://schema.org/"); if (node.eventStatus) node.eventStatus = withBase(node.eventStatus, "https://schema.org/"); const isOnline = node.eventStatus === "https://schema.org/EventMovedOnline"; const dates = ["startDate", "previousStartDate", "endDate"]; dates.forEach((date) => { if (!isOnline) { if (node[date] instanceof Date && node[date].getHours() === 0 && node[date].getMinutes() === 0) node[date] = resolvableDateToDate(node[date]); } else { node[date] = resolvableDateToIso(node[date]); } }); setIfEmpty(node, "endDate", node.startDate); return node; }, resolveRootNode(node, { find }) { const identity = find(IdentityId); if (identity) setIfEmpty(node, "organizer", idReference(identity)); } }); const openingHoursResolver = defineSchemaOrgResolver({ defaults: { "@type": "OpeningHoursSpecification", "opens": "00:00", "closes": "23:59" } }); const localBusinessResolver = defineSchemaOrgResolver({ defaults: { "@type": ["Organization", "LocalBusiness"] }, inheritMeta: [ { key: "url", meta: "host" }, { key: "currenciesAccepted", meta: "currency" } ], idPrefix: ["host", IdentityId], resolve(node, ctx) { resolveDefaultType(node, ["Organization", "LocalBusiness"]); node.address = resolveRelation(node.address, ctx, addressResolver); node.openingHoursSpecification = resolveRelation(node.openingHoursSpecification, ctx, openingHoursResolver); node = resolveNode({ ...node }, ctx, organizationResolver); return node; }, resolveRootNode(node, ctx) { organizationResolver.resolveRootNode(node, ctx); return node; } }); const ratingResolver = defineSchemaOrgResolver({ cast(node) { if (node === "number") { return { ratingValue: node }; } return node; }, defaults: { "@type": "Rating", "bestRating": 5, "worstRating": 1 } }); const foodEstablishmentResolver = defineSchemaOrgResolver({ defaults: { "@type": ["Organization", "LocalBusiness", "FoodEstablishment"] }, inheritMeta: [ { key: "url", meta: "host" }, { key: "currenciesAccepted", meta: "currency" } ], idPrefix: ["host", IdentityId], resolve(node, ctx) { resolveDefaultType(node, ["Organization", "LocalBusiness", "FoodEstablishment"]); node.starRating = resolveRelation(node.starRating, ctx, ratingResolver); node = resolveNode(node, ctx, localBusinessResolver); return node; }, resolveRootNode(node, ctx) { localBusinessResolver.resolveRootNode(node, ctx); return node; } }); const howToStepDirectionResolver = defineSchemaOrgResolver({ cast(node) { if (typeof node === "string") { return { text: node }; } return node; }, defaults: { "@type": "HowToDirection" } }); const howToStepResolver = defineSchemaOrgResolver({ cast(node) { if (typeof node === "string") { return { text: node }; } return node; }, defaults: { "@type": "HowToStep" }, resolve(step, ctx) { if (step.url) step.url = resolveWithBase(ctx.meta.url, step.url); if (step.image) { step.image = resolveRelation(step.image, ctx, imageResolver, { root: true }); } if (step.itemListElement) step.itemListElement = resolveRelation(step.itemListElement, ctx, howToStepDirectionResolver); return step; } }); const HowToId = "#howto"; const howToResolver = defineSchemaOrgResolver({ defaults: { "@type": "HowTo" }, inheritMeta: [ "description", "image", "inLanguage", { meta: "title", key: "name" } ], idPrefix: ["url", HowToId], resolve(node, ctx) { node.step = resolveRelation(node.step, ctx, howToStepResolver); return node; }, resolveRootNode(node, { find }) { const webPage = find(PrimaryWebPageId); if (webPage) setIfEmpty(node, "mainEntityOfPage", idReference(webPage)); } }); const itemListResolver = defineSchemaOrgResolver({ defaults: { "@type": "ItemList" }, resolve(node, ctx) { if (node.itemListElement) { let index = 1; node.itemListElement = resolveRelation(node.itemListElement, ctx, listItemResolver, { array: true, afterResolve(node2) { setIfEmpty(node2, "position", index++); } }); } return node; } }); const jobPostingResolver = defineSchemaOrgResolver({ defaults: { "@type": "JobPosting" }, idPrefix: ["url", "#job-posting"], resolve(node, ctx) { node.datePosted = resolvableDateToIso(node.datePosted); node.hiringOrganization = resolveRelation(node.hiringOrganization, ctx, organizationResolver); node.jobLocation = resolveRelation(node.jobLocation, ctx, placeResolver); node.baseSalary = resolveRelation(node.baseSalary, ctx, monetaryAmountResolver); node.validThrough = resolvableDateToIso(node.validThrough); return node; }, resolveRootNode(jobPosting, { find }) { const webPage = find(PrimaryWebPageId); const identity = find(IdentityId); if (identity) setIfEmpty(jobPosting, "hiringOrganization", idReference(identity)); if (webPage) setIfEmpty(jobPosting, "mainEntityOfPage", idReference(webPage)); return jobPosting; } }); const reviewResolver = defineSchemaOrgResolver({ defaults: { "@type": "Review" }, inheritMeta: [ "inLanguage" ], resolve(review, ctx) { review.reviewRating = resolveRelation(review.reviewRating, ctx, ratingResolver); review.author = resolveRelation(review.author, ctx, personResolver); return review; } }); const videoResolver = defineSchemaOrgResolver({ cast(input) { if (typeof input === "string") { input = { url: input }; } return input; }, alias: "video", defaults: { "@type": "VideoObject" }, inheritMeta: [ { meta: "title", key: "name" }, "description", "image", "inLanguage", { meta: "datePublished", key: "uploadDate" } ], idPrefix: "host", resolve(video, ctx) { if (video.uploadDate) video.uploadDate = resolvableDateToIso(video.uploadDate); video.url = resolveWithBase(ctx.meta.host, video.url); if (video.caption && !video.description) video.description = video.caption; if (!video.description) video.description = "No description"; if (video.thumbnailUrl && (typeof video.thumbnailUrl === "string" || Array.isArray(video.thumbnailUrl))) { const images = asArray(video.thumbnailUrl).map((image) => resolveWithBase(ctx.meta.host, image)); video.thumbnailUrl = images.length > 1 ? images : images[0]; } if (video.thumbnail) video.thumbnail = resolveRelation(video.thumbnailUrl, ctx, imageResolver); return video; }, resolveRootNode(video, { find }) { if (video.image && !video.thumbnail) { const firstImage = asArray(video.image)[0]; setIfEmpty(video, "thumbnail", find(firstImage["@id"])?.url); } } }); const movieResolver = defineSchemaOrgResolver({ defaults: { "@type": "Movie" }, resolve(node, ctx) { node.aggregateRating = resolveRelation(node.aggregateRating, ctx, aggregateRatingResolver); node.review = resolveRelation(node.review, ctx, reviewResolver); node.director = resolveRelation(node.director, ctx, personResolver); node.actor = resolveRelation(node.actor, ctx, personResolver); node.trailer = resolveRelation(node.trailer, ctx, videoResolver); node.productionCompany = resolveRelation(node.productionCompany, ctx, organizationResolver); if (node.dateCreated) node.dateCreated = resolvableDateToDate(node.dateCreated); return node; } }); const musicAlbumResolver = defineSchemaOrgResolver({ defaults: { "@type": "MusicAlbum" }, idPrefix: "host", resolve(node, ctx) { if (node.datePublished) node.datePublished = resolvableDateToIso(node.datePublished); if (node.url) node.url = resolveWithBase(ctx.meta.host, node.url); node.byArtist = resolveRelation(node.byArtist, ctx, personResolver); node.aggregateRating = resolveRelation(node.aggregateRating, ctx, aggregateRatingResolver); node.review = resolveRelation(node.review, ctx, reviewResolver); return node; } }); const musicGroupResolver = defineSchemaOrgResolver({ defaults: { "@type": "MusicGroup" }, idPrefix: "host", inheritMeta: [ { meta: "host", key: "url" } ], resolve(node, ctx) { if (node.foundingDate) node.foundingDate = resolvableDateToDate(node.foundingDate); if (node.dissolutionDate) node.dissolutionDate = resolvableDateToDate(node.dissolutionDate); if (node.url) node.url = resolveWithBase(ctx.meta.host, node.url); node.member = resolveRelation(node.member, ctx, personResolver); return node; } }); const musicPlaylistResolver = defineSchemaOrgResolver({ defaults: { "@type": "MusicPlaylist" }, idPrefix: "host", resolve(node, ctx) { if (node.datePublished) node.datePublished = resolvableDateToIso(node.datePublished); if (node.dateModified) node.dateModified = resolvableDateToIso(node.dateModified); if (node.url) node.url = resolveWithBase(ctx.meta.host, node.url); node.creator = resolveRelation(node.creator, ctx, personResolver); node.aggregateRating = resolveRelation(node.aggregateRating, ctx, aggregateRatingResolver); return node; } }); const musicRecordingResolver = defineSchemaOrgResolver({ defaults: { "@type": "MusicRecording" }, idPrefix: "host", resolve(node, ctx) { if (node.datePublished) node.datePublished = resolvableDateToIso(node.datePublished); if (node.url) node.url = resolveWithBase(ctx.meta.host, node.url); if (node.audio) node.audio = resolveWithBase(ctx.meta.host, node.audio); node.byArtist = resolveRelation(node.byArtist, ctx, personResolver); node.aggregateRating = resolveRelation(node.aggregateRating, ctx, aggregateRatingResolver); return node; } }); const podcastEpisodeResolver = defineSchemaOrgResolver({ defaults: { "@type": "PodcastEpisode" }, inheritMeta: [ "inLanguage" ], resolve(node, ctx) { node.author = resolveRelation(node.author, ctx, personResolver); node.aggregateRating = resolveRelation(node.aggregateRating, ctx, aggregateRatingResolver); node.review = resolveRelation(node.review, ctx, reviewResolver); if (node.datePublished) node.datePublished = resolvableDateToIso(node.datePublished); if (node.uploadDate) node.uploadDate = resolvableDateToIso(node.uploadDate); return node; } }); const podcastSeasonResolver = defineSchemaOrgResolver({ defaults: { "@type": "PodcastSeason" }, resolve(node, ctx) { node.actor = resolveRelation(node.actor, ctx, personResolver); node.director = resolveRelation(node.director, ctx, personResolver); node.productionCompany = resolveRelation(node.productionCompany, ctx, organizationResolver); node.aggregateRating = resolveRelation(node.aggregateRating, ctx, aggregateRatingResolver); if (node.datePublished) node.datePublished = resolvableDateToIso(node.datePublished); if (node.startDate) node.startDate = resolvableDateToIso(node.startDate); if (node.endDate) node.endDate = resolvableDateToIso(node.endDate); return node; } }); const podcastSeriesResolver = defineSchemaOrgResolver({ defaults: { "@type": "PodcastSeries" }, resolve(node, ctx) { node.author = resolveRelation(node.author, ctx, personResolver); node.aggregateRating = resolveRelation(node.aggregateRating, ctx, aggregateRatingResolver); if (node.datePublished) node.datePublished = resolvableDateToIso(node.datePublished); if (node.startDate) node.startDate = resolvableDateToIso(node.startDate); if (node.endDate) node.endDate = resolvableDateToIso(node.endDate); return node; } }); const ProductId = "#product"; const productResolver = defineSchemaOrgResolver({ defaults: { "@type": "Product" }, inheritMeta: [ "description", "image", { meta: "title", key: "name" } ], idPrefix: ["url", ProductId], resolve(node, ctx) { node.aggregateOffer = resolveRelation(node.aggregateOffer, ctx, aggregateOfferResolver); node.aggregateRating = resolveRelation(node.aggregateRating, ctx, aggregateRatingResolver); node.offers = resolveRelation(node.offers, ctx, offerResolver); node.review = resolveRelation(node.review, ctx, reviewResolver); return node; }, resolveRootNode(product, { find }) { const webPage = find(PrimaryWebPageId); const identity = find(IdentityId); if (identity) setIfEmpty(product, "brand", idReference(identity)); if (webPage) setIfEmpty(product, "mainEntityOfPage", idReference(webPage)); return product; } }); const answerResolver = defineSchemaOrgResolver({ cast(node) { if (typeof node === "string") { return { text: node }; } return node; }, defaults: { "@type": "Answer" } }); const questionResolver = defineSchemaOrgResolver({ defaults: { "@type": "Question" }, inheritMeta: [ "inLanguage" ], idPrefix: "url", resolve(question, ctx) { if (question.question) { question.name = question.question; delete question.question; } if (question.answer) { question.acceptedAnswer = question.answer; delete question.answer; } question.acceptedAnswer = resolveRelation(question.acceptedAnswer, ctx, answerResolver); question.dateCreated = resolvableDateToIso(question.dateCreated); return question; }, resolveRootNode(question, { find }) { const webPage = find(PrimaryWebPageId); if (webPage && asArray(webPage["@type"]).includes("FAQPage")) dedupeMerge(webPage, "mainEntity", idReference(question)); } }); const RecipeId = "#recipe"; const recipeResolver = defineSchemaOrgResolver({ defaults: { "@type": "Recipe" }, inheritMeta: [ { meta: "title", key: "name" }, "description", "image", "datePublished" ], idPrefix: ["url", RecipeId], resolve(node, ctx) { node.recipeInstructions = resolveRelation(node.recipeInstructions, ctx, howToStepResolver); return node; }, resolveRootNode(node, { find }) { const article = find(PrimaryArticleId); const webPage = find(PrimaryWebPageId); if (article) setIfEmpty(node, "mainEntityOfPage", idReference(article)); else if (webPage) setIfEmpty(node, "mainEntityOfPage", idReference(webPage)); if (article?.author) setIfEmpty(node, "author", article.author); return node; } }); const ServiceId = "#service"; const serviceResolver = defineSchemaOrgResolver({ defaults: { "@type": "Service" }, inheritMeta: [ "description", "image", { meta: "title", key: "name" } ], idPrefix: ["url", ServiceId], resolve(node, ctx) { resolveDefaultType(node, "Service"); node.offers = resolveRelation(node.offers, ctx, offerResolver); node.aggregateRating = resolveRelation(node.aggregateRating, ctx, aggregateRatingResolver); node.review = resolveRelation(node.review, ctx, reviewResolver); return node; }, resolveRootNode(service, { find }) { const webPage = find(PrimaryWebPageId); const identity = find(IdentityId); if (identity) setIfEmpty(service, "provider", idReference(identity)); if (identity) setIfEmpty(service, "brand", idReference(identity)); if (webPage) setIfEmpty(service, "mainEntityOfPage", idReference(webPage)); return service; } }); const softwareAppResolver = defineSchemaOrgResolver({ defaults: { "@type": "SoftwareApplication" }, resolve(node, ctx) { resolveDefaultType(node, "SoftwareApplication"); node.offers = resolveRelation(node.offers, ctx, offerResolver); node.aggregateRating = resolveRelation(node.aggregateRating, ctx, aggregateRatingResolver); node.review = resolveRelation(node.review, ctx, reviewResolver); return node; } }); const tvEpisodeResolver = defineSchemaOrgResolver({ defaults: { "@type": "TVEpisode" }, resolve(node, ctx) { node.actor = resolveRelation(node.actor, ctx, personResolver); node.director = resolveRelation(node.director, ctx, personResolver); node.musicBy = resolveRelation(node.musicBy, ctx, personResolver); node.video = resolveRelation(node.video, ctx, videoResolver); node.aggregateRating = resolveRelation(node.aggregateRating, ctx, aggregateRatingResolver); node.review = resolveRelation(node.review, ctx, reviewResolver); if (node.datePublished) node.datePublished = resolvableDateToIso(node.datePublished); if (node.uploadDate) node.uploadDate = resolvableDateToIso(node.uploadDate); return node; } }); const tvSeasonResolver = defineSchemaOrgResolver({ defaults: { "@type": "TVSeason" }, resolve(node, ctx) { node.actor = resolveRelation(node.actor, ctx, personResolver); node.director = resolveRelation(node.director, ctx, personResolver); node.productionCompany = resolveRelation(node.productionCompany, ctx, organizationResolver); node.aggregateRating = resolveRelation(node.aggregateRating, ctx, aggregateRatingResolver); node.trailer = resolveRelation(node.trailer, ctx, videoResolver); if (node.datePublished) node.datePublished = resolvableDateToIso(node.datePublished); if (node.startDate) node.startDate = resolvableDateToIso(node.startDate); if (node.endDate) node.endDate = resolvableDateToIso(node.endDate); return node; } }); const tvSeriesResolver = defineSchemaOrgResolver({ defaults: { "@type": "TVSeries" }, resolve(node, ctx) { node.actor = resolveRelation(node.actor, ctx, personResolver); node.director = resolveRelation(node.director, ctx, personResolver); node.creator = resolveRelation(node.creator, ctx, personResolver); node.productionCompany = resolveRelation(node.productionCompany, ctx, organizationResolver); node.aggregateRating = resolveRelation(node.aggregateRating, ctx, aggregateRatingResolver); node.trailer = resolveRelation(node.trailer, ctx, videoResolver); if (node.datePublished) node.datePublished = resolvableDateToIso(node.datePublished); if (node.startDate) node.startDate = resolvableDateToIso(node.startDate); if (node.endDate) node.endDate = resolvableDateToIso(node.endDate); return node; } }); const ALIAS_RE = /([a-z])([A-Z])/g; function nextNodeId(ctx, alias) { ctx.nodeIdCounters[alias] = (ctx.nodeIdCounters[alias] || 0) + 1; return ctx.nodeIdCounters[alias].toString(); } function resolveMeta(meta) { if (!meta.path) meta.path = "/"; if (!meta.host && typeof document !== "undefined") meta.host = document.location.host; if (meta.path !== "/") { if (meta.trailingSlash && !hasTrailingSlash(meta.path)) meta.path = withTrailingSlash(meta.path); else if (!meta.trailingSlash && hasTrailingSlash(meta.path)) meta.path = withoutTrailingSlash(meta.path); } meta.url = joinURL(meta.host || "", meta.path); return { ...meta, host: meta.host, url: meta.url, currency: meta.currency, image: meta.image, inLanguage: meta.inLanguage, title: meta.title, description: meta.description, datePublished: meta.datePublished, dateModified: meta.dateModified }; } function resolveNode(node, ctx, resolver) { if (resolver?.cast) node = resolver.cast(node, ctx); if (resolver?.defaults) { let defaults = resolver.defaults; if (typeof defaults === "function") defaults = defaults(ctx); node = { ...defaults, ...node }; } const inheritMeta = resolver?.inheritMeta; if (inheritMeta) { for (let i = 0; i < inheritMeta.length; i++) { const entry = inheritMeta[i]; if (typeof entry === "string") setIfEmpty(node, entry, ctx.meta[entry]); else setIfEmpty(node, entry.key, ctx.meta[entry.meta]); } } if (resolver?.resolve) node = resolver.resolve(node, ctx); for (const k in node) { const v = node[k]; if (Array.isArray(v)) { for (let i = 0; i < v.length; i++) { const item = v[i]; if (typeof item === "object" && item?._resolver) node[k][i] = resolveRelation(item, ctx, item._resolver); } } else if (typeof v === "object" && v?._resolver) { node[k] = resolveRelation(v, ctx, v._resolver); } } stripEmptyProperties(node); return node; } function resolveNodeId(node, ctx, resolver, resolveAsRoot = false) { if (node["@id"] && node["@id"].startsWith("http")) return node; const prefix = resolver ? (Array.isArray(resolver.idPrefix) ? resolver.idPrefix[0] : resolver.idPrefix) || "url" : "url"; const rootId = node["@id"] || (resolver ? Array.isArray(resolver.idPrefix) ? resolver.idPrefix?.[1] : void 0 : ""); if (!node["@id"] && resolveAsRoot && rootId) { node["@id"] = prefixId(ctx.meta[prefix], rootId); return node; } if (node["@id"]?.startsWith("#/schema/") || node["@id"]?.startsWith("/")) { node["@id"] = prefixId(ctx.meta[prefix], node["@id"]); return node; } let alias = resolver?.alias; if (!alias) { const type = asArray(node["@type"])?.[0] || ""; alias = type.replace(ALIAS_RE, "$1-$2").toLowerCase(); } node["@id"] = prefixId(ctx.meta[prefix], `#/schema/${alias}/${node["@id"] || nextNodeId(ctx, alias)}`); return node; } function resolveRelation(input, ctx, fallbackResolver, options = {}) { if (!input) return input; const items = asArray(input); const ids = []; for (let i = 0; i < items.length; i++) { const a = items[i]; let keyCount = 0; for (const _ in a) keyCount++; if (keyCount === 1 && a["@id"] || keyCount === 2 && a["@id"] && a["@type"]) { ids.push(resolveNodeId({ "@id": ctx.find(a["@id"])?.["@id"] || a["@id"] }, ctx)); continue; } let resolver = fallbackResolver; if (a._resolver && typeof a._resolver !== "string") { resolver = a._resolver; delete a._resolver; } if (!resolver) { ids.push(a); continue; } let node = resolveNode(a, ctx, resolver); if (options.afterResolve) options.afterResolve(node); if (options.generateId || options.root) node = resolveNodeId(node, ctx, resolver, false); if (options.root) { if (resolver.resolveRootNode) resolver.resolveRootNode(node, ctx); ctx.push(node); ids.push(idReference(node["@id"])); continue; } ids.push(node); } return !options.array && ids.length === 1 ? ids[0] : ids; } const UNSAFE_KEYS$1 = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]); function merge(target, source) { if (!source) return target; for (const key in source) { if (!Object.hasOwn(source, key) || UNSAFE_KEYS$1.has(key)) continue; const value = source[key]; if (value === void 0) continue; if (Array.isArray(target[key])) { if (Array.isArray(value)) { const merged = [...target[key], ...value]; if (key === "@type") { target[key] = [...new Set(merged)]; } else if (key === "itemListElement") { merged.sort((a, b) => (a.position || 0) - (b.position || 0)); for (let i = 0; i < merged.length; i++) merged[i].position = i + 1; target[key] = merged; } else if (key === "potentialAction") { const byType = /* @__PURE__ */ Object.create(null); for (const action of merged) { const type = action["@type"]; if (byType[type]) { if (action.target && byType[type].target) { const a = Array.isArray(byType[type].target) ? byType[type].target : [byType[type].target]; const b = Array.isArray(action.target) ? action.target : [action.target]; byType[type].target = [.../* @__PURE__ */ new Set([...a, ...b])]; } } else { byType[type] = { ...action }; } } target[key] = Object.values(byType); } else { const hasTypedObjects = merged.length > 0 && merged.every( (item) => item && typeof item === "object" && item["@type"] ); if (hasTypedObjects) { const byType = /* @__PURE__ */ Object.create(null); for (const item of merged) byType[item["@type"]] = item; target[key] = Object.values(byType); } else { target[key] = merged; } } } else { target[key] = merge(target[key], [value]); } } else if (target[key] && typeof target[key] === "object" && typeof value === "object" && !Array.isArray(value)) { target[key] = merge({ ...target[key] }, value); } else { target[key] = value; } } return target; } const DOMAIN_RE = /(?:https?:)?\/\//; function indexNode(index, node) { if (!node["@id"]) return; const nodeId = node["@id"]; const fragmentKey = resolveAsGraphKey(nodeId); index.set(fragmentKey, node); index.set(nodeId, node); const domainKey = nodeId.replace(DOMAIN_RE, "").split("/")[0]; index.set(domainKey, node); } function createSchemaOrgGraph() { const ctx = { find(id) { let resolver = (s) => s; if (id[0] === "#") { resolver = resolveAsGraphKey; } else if (id[0] === "/") { resolver = (s) => s.replace(DOMAIN_RE, "").split("/")[0]; } const key = resolver(id); if (ctx.nodeIndex.size > 0) { return ctx.nodeIndex.get(key) || null; } return ctx.nodes.filter((n) => !!n["@id"]).find((n) => resolver(n["@id"]) === key); }, push(input) { asArray(input).forEach((node) => { const registeredNode = node; ctx.nodes.push(registeredNode); if (ctx.nodeIndex.size > 0) indexNode(ctx.nodeIndex, registeredNode); }); }, resolveGraph(meta) { for (const k in ctx.nodeIdCounters) delete ctx.nodeIdCounters[k]; ctx.meta = resolveMeta({ ...meta }); const len = ctx.nodes.length; for (let i = 0; i < len; i++) { let node = ctx.nodes[i]; const resolver = node._resolver; node = resolveNode(node, ctx, resolver); node = resolveNodeId(node, ctx, resolver, true); ctx.nodes[i] = node; } const dedupedNodes = /* @__PURE__ */ Object.create(null); ctx.nodeIndex = /* @__PURE__ */ new Map(); for (let i = 0; i < ctx.nodes.length; i++) { const n = ctx.nodes[i]; const nodeKey = resolveAsGraphKey(n["@id"]); if (dedupedNodes[nodeKey]) { if (n._dedupeStrategy !== "replace") dedupedNodes[nodeKey] = merge(dedupedNodes[nodeKey], n); else dedupedNodes[nodeKey] = n; } else { dedupedNodes[nodeKey] = n; } } ctx.nodes = Object.values(dedupedNodes); for (let i = 0; i < ctx.nodes.length; i++) indexNode(ctx.nodeIndex, ctx.nodes[i]); const countBeforeRelations = ctx.nodes.length; for (let i = 0; i < ctx.nodes.length; i++) { const node = ctx.nodes[i]; if (node.image && typeof node.image === "string") { node.image = resolveRelation(node.image, ctx, imageResolver, { root: true }); } node.translationOfWork = resolveRelation(node.translationOfWork, ctx); node.workTran