Site Name Schema Framework
Use conditional-logic schema generation to keep WebSite site name markup aligned with real page state and avoid stale or invalid static templates.
When To Use It
Google Search CentralSite Name documentationAdd WebSite markup to the home page of the domain or subdomain only; it isn't needed on other pages. Google uses it, together with other home page signals, to choose the site name shown in Search results. Site names are available in all languages where Google Search is available, on mobile and desktop.
Key Implementation Documentation Highlights
- Required properties: name (the name of the website) and url (the canonical home page, for example https://example.com/ or https://news.example.com/). Recommended: alternateName.
- Home page only: site names are supported at the domain and subdomain level. Subdirectory home pages such as https://example.com/news are not supported.
- Choose a unique, accurate, concise and commonly recognized name: "Google", not "Google, Inc". Avoid generic names unless they are a well-known brand.
- alternateName: an acronym or shorter name, listed in order of preference, most important first; alternate names follow the same guidelines.
- Consistency: use the same name in WebSite markup, og:site_name, the <title>, headings and other home page text. Organization markup should use the same name and alternateName; the registered company name belongs in Organization legalName.
- One WebSite node: avoid duplicate WebSite blocks. If the home page already has WebSite markup, add name and alternateName to it instead of adding a second block.
- Crawlability: the home page and its duplicates (HTTP and HTTPS, www and non-www) must be accessible to Googlebot, not blocked by robots.txt and not noindex.
- Retired sitelinks search box: Google stopped showing it on Nov 21, 2024. Leftover WebSite plus potentialAction SearchAction markup causes no errors, but new builds don't need it; the WebSite markup itself still drives site names.
- After deployment, use URL Inspection to request a recrawl of the home page so Google picks up the new name.
Template Approach
const siteData = {
siteName: "Trailhead Outfitters",
shortName: "Trailhead",
homeUrl: "https://www.example.com/"
};
const staticTemplate = {
"@context": "https://schema.org",
"@type": "WebSite",
"name": siteData.siteName,
"alternateName": siteData.shortName,
"url": siteData.homeUrl
};
{
"@context": "https://schema.org",
"@type": "WebSite",
"name": "Trailhead Outfitters",
"alternateName": "Trailhead",
"url": "https://www.example.com/"
}
Conditional-logic Framework
const ISO_DATE_TIME = /^(\d{4})-(\d{2})-(\d{2})(?:T(\d{2}):(\d{2})(?::(\d{2}))?(Z|[+-]\d{2}:\d{2})?)?$/;
function isAbsoluteUrl(value) {
return typeof value === "string" && /^https?:\/\/[^\s/?#]+\.[^\s/?#]+(?:[/?#]\S*)?$/i.test(value);
}
// Returns the ISO 8601 string unchanged (timezone offset kept), or null if unparseable.
function toIsoDateTime(value) {
if (typeof value !== "string") return null;
const text = value.trim();
const m = text.match(ISO_DATE_TIME);
if (!m) return null;
const [, y, mo, d, h = "00", mi = "00", s = "00"] = m;
const date = new Date(Date.UTC(+y, +mo - 1, +d));
if (date.getUTCMonth() !== +mo - 1 || date.getUTCDate() !== +d) return null;
if (+h > 23 || +mi > 59 || +s > 59) return null;
return text;
}
function toIsoDate(value) {
const iso = toIsoDateTime(value);
return iso ? iso.slice(0, 10) : null;
}
// Drops null, undefined, "", [] and {} recursively.
function compact(value) {
if (Array.isArray(value)) {
const items = value.map(compact).filter((v) => v !== undefined);
return items.length ? items : undefined;
}
if (value && typeof value === "object") {
const out = {};
for (const [key, v] of Object.entries(value)) {
const c = compact(v);
if (c !== undefined) out[key] = c;
}
return Object.keys(out).length ? out : undefined;
}
return value === null || value === undefined || value === "" ? undefined : value;
}
function passesPageGates(source) {
if (source.indexable === false) return false;
if (source.canonicalUrl && source.url && source.canonicalUrl !== source.url) return false;
return source.contentVisible !== false;
}
// Legal suffixes belong in Organization legalName, not in the site name ("Google", not "Google, Inc").
const LEGAL_SUFFIX = /(?:,\s*|\s+)(?:inc|llc|ltd|limited|corp|corporation|gmbh|plc)\.?$/i;
function cleanName(value) {
const text = typeof value === "string" ? value.trim().replace(/\s+/g, " ") : "";
return text && !LEGAL_SUFFIX.test(text) ? text : null;
}
// The canonical home page of a domain or subdomain: root path, no query or fragment.
function toHomeUrl(value) {
if (!isAbsoluteUrl(value)) return null;
const url = new URL(value);
return url.pathname === "/" && !url.search && !url.hash ? `${url.origin}/` : null;
}
function buildWebSiteSchema(source) {
// (a) Page gates: indexable, self-canonical, and the home page at the domain or subdomain root.
// Subdirectory home pages such as https://example.com/news are not supported.
if (!passesPageGates(source)) return null;
const homeUrl = toHomeUrl(source.url);
if (!homeUrl) return null;
// (b) Required: name and url. The name must be concise and match og:site_name,
// the title and the Organization name; a mismatch is fixed in the source, not shipped.
const name = cleanName(source.siteName);
if (!name) return null;
const otherNames = [source.ogSiteName, source.organizationName].filter(Boolean);
if (otherNames.some((other) => other.trim() !== name)) return null;
if (source.title && !source.title.includes(name)) return null;
// alternateName: in order of preference, deduplicated, never the name itself.
const alternateName = [...new Set((source.alternateNames || []).map(cleanName))]
.filter((alt) => alt && alt.toLowerCase() !== name.toLowerCase());
// (c) Required plus recommended properties; (d) one WebSite node per site: if the page already
// has WebSite markup, merge into it. No SearchAction: the sitelinks search box was retired in Nov 2024.
const schema = {
"@context": "https://schema.org",
"@type": "WebSite",
"@id": `${homeUrl}#website`,
name,
alternateName,
url: homeUrl,
publisher: isAbsoluteUrl(source.organizationId) ? { "@id": source.organizationId } : null
};
// (e) Strip empty values.
return compact(schema);
}
// Usage:
// const source = {
// "url": "https://www.example.com/",
// "canonicalUrl": "https://www.example.com/",
// "indexable": true,
// "contentVisible": true,
// "siteName": "Trailhead Outfitters",
// "alternateNames": [
// "Trailhead",
// "THO"
// ],
// "ogSiteName": "Trailhead Outfitters",
// "organizationName": "Trailhead Outfitters",
// "title": "Trailhead Outfitters: Hiking Packs, Tents and Trail Apparel"
// };
// const schema = buildWebSiteSchema(source); // null when a gate or required property fails
import re
from datetime import date, datetime, timezone
from urllib.parse import urlsplit
ISO_DATE_TIME = re.compile(r"^(\d{4})-(\d{2})-(\d{2})(?:T(\d{2}):(\d{2})(?::(\d{2}))?(Z|[+-]\d{2}:\d{2})?)?$")
ABSOLUTE_URL = re.compile(r"^https?://[^\s/?#]+\.[^\s/?#]+(?:[/?#]\S*)?$", re.I)
def is_absolute_url(value):
return isinstance(value, str) and bool(ABSOLUTE_URL.match(value))
# Returns the ISO 8601 string unchanged (timezone offset kept), or None if unparseable.
def to_iso_date_time(value):
if not isinstance(value, str):
return None
text = value.strip()
m = ISO_DATE_TIME.match(text)
if not m:
return None
y, mo, d = int(m[1]), int(m[2]), int(m[3])
try:
date(y, mo, d)
except ValueError:
return None
if int(m[4] or 0) > 23 or int(m[5] or 0) > 59 or int(m[6] or 0) > 59:
return None
return text
def to_iso_date(value):
iso = to_iso_date_time(value)
return iso[:10] if iso else None
# Drops None, "", [] and {} recursively.
def compact(value):
if isinstance(value, list):
items = [c for c in (compact(v) for v in value) if c is not None]
return items or None
if isinstance(value, dict):
out = {k: c for k, c in ((k, compact(v)) for k, v in value.items()) if c is not None}
return out or None
return None if value is None or value == "" else value
def passes_page_gates(source):
if source.get("indexable") is False:
return False
if source.get("canonicalUrl") and source.get("url") and source["canonicalUrl"] != source["url"]:
return False
return source.get("contentVisible") is not False
# Legal suffixes belong in Organization legalName, not in the site name ("Google", not "Google, Inc").
LEGAL_SUFFIX = re.compile(r"(?:,\s*|\s+)(?:inc|llc|ltd|limited|corp|corporation|gmbh|plc)\.?$", re.I)
def clean_name(value):
text = re.sub(r"\s+", " ", value.strip()) if isinstance(value, str) else ""
return text if text and not LEGAL_SUFFIX.search(text) else None
# The canonical home page of a domain or subdomain: root path, no query or fragment.
def to_home_url(value):
if not is_absolute_url(value):
return None
url = urlsplit(value)
if url.path != "/" or url.query or url.fragment:
return None
return f"{url.scheme.lower()}://{url.netloc.lower()}/"
def build_website_schema(source):
# (a) Page gates: indexable, self-canonical, and the home page at the domain or subdomain root.
# Subdirectory home pages such as https://example.com/news are not supported.
if not passes_page_gates(source):
return None
home_url = to_home_url(source.get("url"))
if not home_url:
return None
# (b) Required: name and url. The name must be concise and match og:site_name,
# the title and the Organization name; a mismatch is fixed in the source, not shipped.
name = clean_name(source.get("siteName"))
if not name:
return None
other_names = [n for n in (source.get("ogSiteName"), source.get("organizationName")) if n]
if any(other.strip() != name for other in other_names):
return None
if source.get("title") and name not in source["title"]:
return None
# alternateName: in order of preference, deduplicated, never the name itself.
alternate_name = []
for alt in (clean_name(a) for a in source.get("alternateNames") or []):
if alt and alt.lower() != name.lower() and alt not in alternate_name:
alternate_name.append(alt)
# (c) Required plus recommended properties; (d) one WebSite node per site: if the page already
# has WebSite markup, merge into it. No SearchAction: the sitelinks search box was retired in Nov 2024.
schema = {
"@context": "https://schema.org",
"@type": "WebSite",
"@id": f"{home_url}#website",
"name": name,
"alternateName": alternate_name,
"url": home_url,
"publisher": {"@id": source["organizationId"]} if is_absolute_url(source.get("organizationId")) else None,
}
# (e) Strip empty values.
return compact(schema)
{
"@context": "https://schema.org",
"@type": "WebSite",
"@id": "https://www.example.com/#website",
"name": "Trailhead Outfitters",
"alternateName": [
"Trailhead",
"THO"
],
"url": "https://www.example.com/"
}
Why Conditional Logic Is Better Than Static Templates
- Emits WebSite markup only on the root home page, where a sitewide static template would repeat it on every page and on unsupported subdirectory sites.
- Refuses to ship a name that disagrees with og:site_name, the title or the Organization name, so the signals Google compares stay aligned.
- Strips legal suffixes and duplicate alternate names while keeping the order of preference.
- Keeps the retired SearchAction out of new builds and links the WebSite node to the Organization by @id when one exists.
Property Reference
What Google's Site name documentation requires and recommends, property by property. These are the same rules the SchemaCDN validator checks. Each name links to its schema.org definition.
| Property | Expects | Notes | |
|---|---|---|---|
| name | Required | Text | The name of the website. |
| url | Required | URL | The URL of the home page of the site. |
| alternateName | Recommended | Text, can repeat | An alternate name of the website, for example a recognized acronym or shorter name. |