Organization
On this page. Who runs the site: name, logo, contact details and social profiles.
Define who runs the site once, on the home page: the Organization with its return policy, shipping and loyalty program, plus the WebSite that sets the site name.
The home page is where Google looks for the site name and where an Organization description belongs. It is also the one place to define store-wide policies, so a return policy or shipping rate is written once instead of on every product.
Everything here gets a stable @id. Product, article and location pages then refer to the Organization and its policies by that @id instead of repeating them.
Each card opens the framework for that feature, with its own rules and property reference. Everything here is defined on the homepage.
On this page. Who runs the site: name, logo, contact details and social profiles.
On this page. WebSite on the home page only, with the same name as the Organization.
On this page. Store-wide MerchantReturnPolicy, nested under the Organization.
On this page. Store-wide ShippingService, with a free shipping threshold.
On this page. MemberProgram and tiers that product offers price against.
Every node has a stable @id. A node is written out once, where it belongs, and everywhere else it is referenced by that @id alone. This is the map for this page, generated from the output below.
| @id | Defined on | Type | Referenced by |
|---|---|---|---|
/#organization | This page | OnlineStore | WebSite.publisher, WebPage.about |
/#return-policy | This page | MerchantReturnPolicy | Nested in OnlineStore.hasMerchantReturnPolicy |
/#shipping | This page | ShippingService | Nested in OnlineStore.hasShippingService |
/#member-program | This page | MemberProgram | Nested in OnlineStore.hasMemberProgram |
/#tier-member | This page | MemberProgramTier | Nested in MemberProgram.hasTiers |
/#tier-summit | This page | MemberProgramTier | Nested in MemberProgram.hasTiers |
/#website | This page | WebSite | WebPage.isPartOf |
/#webpage | This page | WebPage | Top-level node |
One function turns the page's data into the whole graph. It drops anything Google would reject instead of shipping it half built. The input below includes rows it is meant to drop, so you can see the logic work.
// Homepage schema: the site's identity, defined once.
// Organization (with its return policy, shipping service and loyalty
// program), WebSite for the site name, and the home WebPage. Every other
// page type points at these nodes by @id instead of repeating them.
const ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;
const COUNTRY = /^[A-Z]{2}$/;
const CURRENCY = /^[A-Z]{3}$/;
const clean = (v) => (typeof v === 'string' ? v.trim() : v);
const isUrl = (v) => typeof v === 'string' && /^https:\/\/[^\s]+$/.test(v);
const compact = (obj) => {
if (Array.isArray(obj)) {
const out = obj.map(compact).filter((v) => v !== undefined);
return out.length ? out : undefined;
}
if (obj && typeof obj === 'object') {
const out = {};
for (const [k, v] of Object.entries(obj)) {
const c = compact(v);
if (c !== undefined) out[k] = c;
}
return Object.keys(out).length ? out : undefined;
}
return obj === null || obj === '' ? undefined : obj;
};
function ids(siteUrl) {
const root = siteUrl.replace(/\/+$/, '');
return {
root,
organization: `${root}/#organization`,
website: `${root}/#website`,
returnPolicy: `${root}/#return-policy`,
shipping: `${root}/#shipping`,
memberProgram: `${root}/#member-program`,
tier: (key) => `${root}/#tier-${key}`,
};
}
// Return policy: only emitted when the category is known, and only with the
// fields that category allows (a store that does not accept returns has no
// return window or method).
function returnPolicy(p, id) {
if (!p || !COUNTRY.test(p.country || '') || !isUrl(p.link)) return undefined;
const category = {
finite: 'https://schema.org/MerchantReturnFiniteReturnWindow',
unlimited: 'https://schema.org/MerchantReturnUnlimitedWindow',
none: 'https://schema.org/MerchantReturnNotPermitted',
}[p.window];
if (!category) return undefined;
const allowsReturns = p.window !== 'none';
return compact({
'@type': 'MerchantReturnPolicy',
'@id': id,
applicableCountry: p.country,
returnPolicyCountry: p.country,
returnPolicyCategory: category,
merchantReturnLink: p.link,
merchantReturnDays: p.window === 'finite' && Number.isInteger(p.days) && p.days > 0 ? p.days : undefined,
returnMethod: allowsReturns && p.byMail ? 'https://schema.org/ReturnByMail' : undefined,
returnFees: allowsReturns ? (p.freeReturns ? 'https://schema.org/FreeReturn' : 'https://schema.org/ReturnFeesCustomerResponsibility') : undefined,
refundType: allowsReturns ? 'https://schema.org/FullRefund' : undefined,
});
}
// Shipping service: one ShippingConditions entry per destination with a
// valid country and either a flat rate or free shipping above a threshold.
function shippingService(s, id) {
if (!s || !Array.isArray(s.destinations) || !CURRENCY.test(s.currency || '')) return undefined;
const transit = (d) =>
Number.isInteger(d.minDays) && Number.isInteger(d.maxDays) && d.minDays <= d.maxDays
? { '@type': 'ServicePeriod', duration: { '@type': 'QuantitativeValue', minValue: d.minDays, maxValue: d.maxDays, unitCode: 'DAY' } }
: undefined;
const conditions = s.destinations
.filter((d) => COUNTRY.test(d.country || '') && Number.isFinite(d.rate) && d.rate >= 0)
.flatMap((d) => {
const region = { '@type': 'DefinedRegion', addressCountry: d.country };
const paid = {
'@type': 'ShippingConditions',
shippingDestination: region,
shippingRate: { '@type': 'MonetaryAmount', value: d.rate, currency: s.currency },
transitTime: transit(d),
};
// Free shipping above a threshold is a second condition with its own
// order value band, not a note on the first.
if (!(Number.isFinite(d.freeOver) && d.freeOver > 0 && d.rate > 0)) return [compact(paid)];
return [
compact({ ...paid, orderValue: { '@type': 'MonetaryAmount', minValue: 0, maxValue: d.freeOver, currency: s.currency } }),
compact({
'@type': 'ShippingConditions',
shippingDestination: region,
orderValue: { '@type': 'MonetaryAmount', minValue: d.freeOver, currency: s.currency },
shippingRate: { '@type': 'MonetaryAmount', value: 0, currency: s.currency },
transitTime: transit(d),
}),
];
});
if (!conditions.length) return undefined;
return compact({ '@type': 'ShippingService', '@id': id, name: clean(s.name), shippingConditions: conditions });
}
// Loyalty program: each tier needs a name and at least one benefit, and gets
// its own @id so product offers can price against it. Google recognises two
// benefits, points and member pricing; a points tier must say how many points
// a purchase earns, or the tier is dropped.
function memberProgram(m, idOf, id) {
if (!m || !m.name || !m.description) return undefined;
const tiers = (m.tiers || [])
.filter((t) => t.key && t.name)
.map((t) => {
const points = t.benefits?.includes('points') && Number.isFinite(t.pointsPerDollar) && t.pointsPerDollar > 0;
const benefits = [
points ? 'https://schema.org/TierBenefitLoyaltyPoints' : undefined,
t.benefits?.includes('price') ? 'https://schema.org/TierBenefitLoyaltyPrice' : undefined,
].filter(Boolean);
if (!benefits.length) return undefined;
return compact({
'@type': 'MemberProgramTier',
'@id': idOf(t.key),
name: clean(t.name),
url: isUrl(t.url) ? t.url : undefined,
hasTierBenefit: benefits,
membershipPointsEarned: points ? { '@type': 'QuantitativeValue', value: t.pointsPerDollar, unitText: 'points per USD' } : undefined,
hasTierRequirement: Number.isFinite(t.minSpend) && t.minSpend > 0
? { '@type': 'MonetaryAmount', value: t.minSpend, currency: 'USD' }
: undefined,
});
})
.filter(Boolean);
if (!tiers.length) return undefined;
return compact({ '@type': 'MemberProgram', '@id': id, name: clean(m.name), description: clean(m.description), url: isUrl(m.url) ? m.url : undefined, hasTiers: tiers });
}
function buildHomepageSchema(source) {
const site = source.site || {};
if (!isUrl(site.url) || !site.name) return null;
const id = ids(site.url);
const organization = compact({
'@type': site.type || 'Organization',
'@id': id.organization,
name: clean(site.name),
alternateName: clean(site.alternateName),
url: `${id.root}/`,
logo: isUrl(site.logo) ? { '@type': 'ImageObject', url: site.logo } : undefined,
description: clean(site.description),
email: clean(site.email),
telephone: clean(site.telephone),
sameAs: (site.sameAs || []).filter(isUrl),
foundingDate: ISO_DATE.test(site.foundingDate || '') ? site.foundingDate : undefined,
address: site.address && site.address.street && COUNTRY.test(site.address.country || '')
? {
'@type': 'PostalAddress',
streetAddress: clean(site.address.street),
addressLocality: clean(site.address.city),
addressRegion: clean(site.address.region),
postalCode: clean(site.address.postalCode),
addressCountry: site.address.country,
}
: undefined,
contactPoint: site.email || site.telephone
? { '@type': 'ContactPoint', contactType: 'customer service', email: clean(site.email), telephone: clean(site.telephone) }
: undefined,
hasMerchantReturnPolicy: returnPolicy(site.returnPolicy, id.returnPolicy),
hasShippingService: shippingService(site.shipping, id.shipping),
hasMemberProgram: memberProgram(site.loyalty, id.tier, id.memberProgram),
});
// Site name: only on the home page, with the same name as the Organization.
const website = compact({
'@type': 'WebSite',
'@id': id.website,
name: clean(site.name),
alternateName: clean(site.alternateName),
url: `${id.root}/`,
publisher: { '@id': id.organization },
});
const webpage = compact({
'@type': 'WebPage',
'@id': `${id.root}/#webpage`,
url: `${id.root}/`,
name: clean(source.page?.title) || clean(site.name),
isPartOf: { '@id': id.website },
about: { '@id': id.organization },
});
return { '@context': 'https://schema.org', '@graph': [organization, website, webpage] };
}
{
"site": {
"type": "OnlineStore",
"name": "Harbor & Pine",
"alternateName": "Harbor and Pine",
"url": "https://www.example.com",
"logo": "https://www.example.com/assets/logo.png",
"description": "Outdoor clothing and camp gear, sold online and in three stores in Maine.",
"email": "help@example.com",
"telephone": "+1-207-555-0140",
"foundingDate": "2014-05-01",
"sameAs": [
"https://www.instagram.com/example",
"https://www.linkedin.com/company/example"
],
"returnPolicy": {
"country": "US",
"window": "finite",
"days": 30,
"byMail": true,
"freeReturns": true,
"link": "https://www.example.com/returns/"
},
"shipping": {
"name": "Standard shipping",
"currency": "USD",
"destinations": [
{
"country": "US",
"rate": 6.95,
"freeOver": 75,
"minDays": 2,
"maxDays": 5
}
]
},
"loyalty": {
"name": "Trailhead Rewards",
"description": "Free to join. Members earn points on every order and get member pricing on selected gear.",
"url": "https://www.example.com/rewards/",
"tiers": [
{
"key": "member",
"name": "Member",
"benefits": [
"points",
"price"
],
"pointsPerDollar": 1,
"url": "https://www.example.com/rewards/#member"
},
{
"key": "summit",
"name": "Summit",
"benefits": [
"points",
"price"
],
"pointsPerDollar": 2,
"minSpend": 500,
"url": "https://www.example.com/rewards/#summit"
}
]
},
"address": {
"street": "214 Commercial Street",
"city": "Portland",
"region": "ME",
"postalCode": "04101",
"country": "US"
}
},
"page": {
"title": "Harbor & Pine | Outdoor clothing and camp gear"
}
}
{
"@context": "https://schema.org",
"@graph": [
{
"@type": "OnlineStore",
"@id": "https://www.example.com/#organization",
"name": "Harbor & Pine",
"alternateName": "Harbor and Pine",
"url": "https://www.example.com/",
"logo": {
"@type": "ImageObject",
"url": "https://www.example.com/assets/logo.png"
},
"description": "Outdoor clothing and camp gear, sold online and in three stores in Maine.",
"email": "help@example.com",
"telephone": "+1-207-555-0140",
"sameAs": [
"https://www.instagram.com/example",
"https://www.linkedin.com/company/example"
],
"foundingDate": "2014-05-01",
"address": {
"@type": "PostalAddress",
"streetAddress": "214 Commercial Street",
"addressLocality": "Portland",
"addressRegion": "ME",
"postalCode": "04101",
"addressCountry": "US"
},
"contactPoint": {
"@type": "ContactPoint",
"contactType": "customer service",
"email": "help@example.com",
"telephone": "+1-207-555-0140"
},
"hasMerchantReturnPolicy": {
"@type": "MerchantReturnPolicy",
"@id": "https://www.example.com/#return-policy",
"applicableCountry": "US",
"returnPolicyCountry": "US",
"returnPolicyCategory": "https://schema.org/MerchantReturnFiniteReturnWindow",
"merchantReturnLink": "https://www.example.com/returns/",
"merchantReturnDays": 30,
"returnMethod": "https://schema.org/ReturnByMail",
"returnFees": "https://schema.org/FreeReturn",
"refundType": "https://schema.org/FullRefund"
},
"hasShippingService": {
"@type": "ShippingService",
"@id": "https://www.example.com/#shipping",
"name": "Standard shipping",
"shippingConditions": [
{
"@type": "ShippingConditions",
"shippingDestination": {
"@type": "DefinedRegion",
"addressCountry": "US"
},
"shippingRate": {
"@type": "MonetaryAmount",
"value": 6.95,
"currency": "USD"
},
"transitTime": {
"@type": "ServicePeriod",
"duration": {
"@type": "QuantitativeValue",
"minValue": 2,
"maxValue": 5,
"unitCode": "DAY"
}
},
"orderValue": {
"@type": "MonetaryAmount",
"minValue": 0,
"maxValue": 75,
"currency": "USD"
}
},
{
"@type": "ShippingConditions",
"shippingDestination": {
"@type": "DefinedRegion",
"addressCountry": "US"
},
"orderValue": {
"@type": "MonetaryAmount",
"minValue": 75,
"currency": "USD"
},
"shippingRate": {
"@type": "MonetaryAmount",
"value": 0,
"currency": "USD"
},
"transitTime": {
"@type": "ServicePeriod",
"duration": {
"@type": "QuantitativeValue",
"minValue": 2,
"maxValue": 5,
"unitCode": "DAY"
}
}
}
]
},
"hasMemberProgram": {
"@type": "MemberProgram",
"@id": "https://www.example.com/#member-program",
"name": "Trailhead Rewards",
"description": "Free to join. Members earn points on every order and get member pricing on selected gear.",
"url": "https://www.example.com/rewards/",
"hasTiers": [
{
"@type": "MemberProgramTier",
"@id": "https://www.example.com/#tier-member",
"name": "Member",
"url": "https://www.example.com/rewards/#member",
"hasTierBenefit": [
"https://schema.org/TierBenefitLoyaltyPoints",
"https://schema.org/TierBenefitLoyaltyPrice"
],
"membershipPointsEarned": {
"@type": "QuantitativeValue",
"value": 1,
"unitText": "points per USD"
}
},
{
"@type": "MemberProgramTier",
"@id": "https://www.example.com/#tier-summit",
"name": "Summit",
"url": "https://www.example.com/rewards/#summit",
"hasTierBenefit": [
"https://schema.org/TierBenefitLoyaltyPoints",
"https://schema.org/TierBenefitLoyaltyPrice"
],
"membershipPointsEarned": {
"@type": "QuantitativeValue",
"value": 2,
"unitText": "points per USD"
},
"hasTierRequirement": {
"@type": "MonetaryAmount",
"value": 500,
"currency": "USD"
}
}
]
}
},
{
"@type": "WebSite",
"@id": "https://www.example.com/#website",
"name": "Harbor & Pine",
"alternateName": "Harbor and Pine",
"url": "https://www.example.com/",
"publisher": {
"@id": "https://www.example.com/#organization"
}
},
{
"@type": "WebPage",
"@id": "https://www.example.com/#webpage",
"url": "https://www.example.com/",
"name": "Harbor & Pine | Outdoor clothing and camp gear",
"isPartOf": {
"@id": "https://www.example.com/#website"
},
"about": {
"@id": "https://www.example.com/#organization"
}
}
]
}
The output above was run through the SchemaCDN validator, which checks every rule in Google's documentation for each feature. Result: no errors, and eligible for:
ProductGroup with a priced Offer per variant, member pricing and links to store-wide policies.
Article with a real Person author, the Organization as publisher, video and a paywalled section.
One store location with address, geo and hours, linked to its parent brand.
Recipe with timed steps, nutrition, ratings and a video whose key moments match the steps.
One open role with pay, closing date and work location, linked to the employer and the store.
An in-store workshop with times, a rescheduled date, the store as venue and a free ticket.
A community thread that becomes Q&A or a discussion posting, depending on the thread.