@unhead/schema-org
Version:
Unhead Schema.org for Simple and Automated Google Rich Results
1,646 lines (1,587 loc) • 65.3 kB
JavaScript
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