Fact Check Schema Framework
Use conditional-logic schema generation to keep Fact Check markup aligned with real page state and avoid stale or invalid static templates.
- Google is phasing out ClaimReview rich results in Search: announced Jun 12, 2025 and removed from Search Console rich result reporting, the Rich Results Test and Search appearance filters on Sep 9, 2025. Expect no fact check rich result.
- The markup is still supported by Google's Fact Check Explorer and keeps its schema.org meaning for AI assistants and other ClaimReview consumers, so it still earns its place on real fact check articles.
- Emit it only for a visible fact check with a clear claim and verdict; do not add it to other pages hoping for a Search feature.
When To Use It
Google Search CentralFact Check documentationUse only on fact check articles that name a specific claim, attribute it to a source outside your organization, and state a clear verdict in the visible article.
Key Implementation Documentation Highlights
- Google still documents ClaimReview while the phase-out runs. Required: claimReviewed (a short summary of the claim; keep it under 75 characters to limit wrapping on mobile), reviewRating and url (the page hosting the full fact check article).
- Required on reviewRating (Rating): alternateName, the textual verdict such as False, which is the only rating value shown. name is recommended and used when alternateName is missing.
- Recommended numeric scale on reviewRating: ratingValue, bestRating and worstRating. worstRating is at least 1 and below bestRating, and ratingValue sits between them inclusive.
- Recommended: author, the publisher of the fact check (not of the claim), with name and url; and itemReviewed, a Claim with author (who made the claim: name required, sameAs recommended), datePublished, appearance and firstAppearance.
- Site requirements: several pages marked with ClaimReview, a corrections policy or error reporting, Google News transparency standards, and no political campaigns, parties or elected officials as publishers.
Rules the framework enforces before it emits markup:
- One ClaimReview per page; a second fact check on the same URL suppresses the markup.
- No textual verdict (alternateName or name): emit nothing.
- A numeric scale must satisfy 1 <= worstRating < bestRating and worstRating <= ratingValue <= bestRating, otherwise the markup is suppressed.
- url is the absolute, self-canonical URL of the article itself, and the claim and conclusion must be readable in the visible article.
Template Approach
const pageData = {
url: "https://www.example.com/fact-checks/2026/moon-cheese",
claim: "The moon is made of cheese",
verdict: "False",
publisher: "Example Fact Desk"
};
const staticTemplate = {
"@context": "https://schema.org",
"@type": "ClaimReview",
"url": pageData.url,
"claimReviewed": pageData.claim,
"reviewRating": { "@type": "Rating", "alternateName": pageData.verdict },
"author": { "@type": "Organization", "name": pageData.publisher }
};
{
"@context": "https://schema.org",
"@type": "ClaimReview",
"url": "https://www.example.com/fact-checks/2026/moon-cheese",
"claimReviewed": "The moon is made of cheese",
"reviewRating": {
"@type": "Rating",
"alternateName": "False"
},
"author": {
"@type": "Organization",
"name": "Example Fact Desk"
}
}
Conditional-logic Framework
function isAbsoluteUrl(value) {
if (typeof value !== "string") return false;
try {
const url = new URL(value);
return url.protocol === "https:" || url.protocol === "http:";
} catch {
return false;
}
}
const ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;
const ISO_DATETIME = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2}(\.\d+)?)?(Z|[+-]\d{2}:\d{2})?$/;
function toIsoDate(value) {
if (typeof value !== "string" || !ISO_DATE.test(value.trim())) return null;
return isNaN(Date.parse(value.trim())) ? null : value.trim();
}
function toIsoDateTime(value) {
if (typeof value !== "string" || !ISO_DATETIME.test(value.trim())) return null;
// keep the original string so the timezone offset survives
return isNaN(Date.parse(value.trim())) ? null : value.trim();
}
function compact(value) {
if (Array.isArray(value)) {
const items = value.map(compact).filter((item) => item !== undefined);
return items.length ? items : undefined;
}
if (value && typeof value === "object") {
const out = {};
for (const [key, item] of Object.entries(value)) {
const kept = compact(item);
if (kept !== undefined) out[key] = kept;
}
return Object.keys(out).length ? out : undefined;
}
if (value === null || value === undefined || value === "") return undefined;
return value;
}
function buildRating(verdict) {
// the textual verdict is required: alternateName, or name when alternateName is absent
const label = (verdict.label || "").trim();
if (!label) return null;
const rating = { "@type": "Rating", "alternateName": label, "name": label };
const { value, worst, best } = verdict;
const numeric = [value, worst, best].every((n) => typeof n === "number" && isFinite(n));
if (numeric) {
// worstRating at least 1 and below bestRating; ratingValue inside the scale
if (worst < 1 || worst >= best || value < worst || value > best) return null;
Object.assign(rating, { "ratingValue": value, "worstRating": worst, "bestRating": best });
}
return rating;
}
function buildClaim(claim) {
if (!claim) return null;
return {
"@type": "Claim",
"author": claim.authorName
? {
"@type": claim.authorType === "Person" ? "Person" : "Organization",
"name": claim.authorName,
"sameAs": isAbsoluteUrl(claim.authorSameAs) ? claim.authorSameAs : null
}
: null,
"datePublished": toIsoDateTime(claim.datePublished) || toIsoDate(claim.datePublished),
"appearance": (claim.appearances || []).filter(isAbsoluteUrl),
"firstAppearance": isAbsoluteUrl(claim.firstAppearance) ? claim.firstAppearance : null
};
}
function buildClaimReviewSchema(source) {
// Google is phasing out ClaimReview rich results; emit only for a visible fact check
// (schema.org semantics, Fact Check Explorer and other consumers)
if (source.indexable === false) return null;
if (!isAbsoluteUrl(source.url)) return null;
if (source.canonicalUrl && source.canonicalUrl !== source.url) return null;
if (source.verdictVisible !== true) return null; // claim and conclusion must be readable on the page
if ((source.claimReviewsOnPage ?? 1) !== 1) return null; // one ClaimReview per page
const claimReviewed = (source.claimSummary || "").trim();
if (!claimReviewed) return null;
const reviewRating = buildRating(source.verdict || {});
if (!reviewRating) return null;
const schema = {
"@context": "https://schema.org",
"@type": "ClaimReview",
"url": source.url, // the page hosting the full fact check article
"claimReviewed": claimReviewed, // keep under 75 characters to limit wrapping on mobile
"reviewRating": reviewRating,
"author": source.publisher
? {
"@type": "Organization",
"name": source.publisher.name,
"url": isAbsoluteUrl(source.publisher.url) ? source.publisher.url : null
}
: null,
"itemReviewed": buildClaim(source.claim)
};
return compact(schema);
}
// Usage:
// buildClaimReviewSchema({
// url: "https://www.example.com/fact-checks/2026/moon-cheese",
// canonicalUrl: "https://www.example.com/fact-checks/2026/moon-cheese",
// indexable: true,
// verdictVisible: true,
// claimReviewsOnPage: 1,
// claimSummary: "The moon is made of cheese",
// verdict: { label: "False", value: 1, worst: 1, best: 5 },
// publisher: { name: "Example Fact Desk", url: "https://www.example.com/about/corrections-policy" },
// claim: {
// authorName: "Square Moon Society",
// authorSameAs: "https://www.example.org/square-moon-society",
// datePublished: "2026-04-02",
// appearances: ["https://www.example.org/posts/moon-facts"],
// firstAppearance: "https://www.example.org/posts/moon-facts"
// }
// });
import math
import re
from datetime import date, datetime
from urllib.parse import urlparse
def is_absolute_url(value):
if not isinstance(value, str):
return False
parsed = urlparse(value)
return parsed.scheme in ("http", "https") and bool(parsed.netloc)
ISO_DATE = re.compile(r"^\d{4}-\d{2}-\d{2}$")
ISO_DATETIME = re.compile(r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2}(\.\d+)?)?(Z|[+-]\d{2}:\d{2})?$")
def to_iso_date(value):
if not isinstance(value, str) or not ISO_DATE.match(value.strip()):
return None
try:
date.fromisoformat(value.strip())
except ValueError:
return None
return value.strip()
def to_iso_datetime(value):
if not isinstance(value, str) or not ISO_DATETIME.match(value.strip()):
return None
# keep the original string so the timezone offset survives
try:
datetime.fromisoformat(value.strip().replace("Z", "+00:00"))
except ValueError:
return None
return value.strip()
def compact(value):
if isinstance(value, list):
items = [kept for kept in (compact(item) for item in value) if kept is not None]
return items or None
if isinstance(value, dict):
out = {}
for key, item in value.items():
kept = compact(item)
if kept is not None:
out[key] = kept
return out or None
if value is None or value == "":
return None
return value
def is_number(value):
return isinstance(value, (int, float)) and not isinstance(value, bool) and math.isfinite(value)
def build_rating(verdict):
# the textual verdict is required: alternateName, or name when alternateName is absent
label = (verdict.get("label") or "").strip()
if not label:
return None
rating = {"@type": "Rating", "alternateName": label, "name": label}
value, worst, best = verdict.get("value"), verdict.get("worst"), verdict.get("best")
if all(is_number(n) for n in (value, worst, best)):
# worstRating at least 1 and below bestRating; ratingValue inside the scale
if worst < 1 or worst >= best or value < worst or value > best:
return None
rating.update({"ratingValue": value, "worstRating": worst, "bestRating": best})
return rating
def build_claim(claim):
if not claim:
return None
return {
"@type": "Claim",
"author": {
"@type": "Person" if claim.get("authorType") == "Person" else "Organization",
"name": claim["authorName"],
"sameAs": claim.get("authorSameAs") if is_absolute_url(claim.get("authorSameAs")) else None,
} if claim.get("authorName") else None,
"datePublished": to_iso_datetime(claim.get("datePublished")) or to_iso_date(claim.get("datePublished")),
"appearance": [u for u in claim.get("appearances") or [] if is_absolute_url(u)],
"firstAppearance": claim.get("firstAppearance") if is_absolute_url(claim.get("firstAppearance")) else None,
}
def build_claim_review_schema(source):
# Google is phasing out ClaimReview rich results; emit only for a visible fact check
# (schema.org semantics, Fact Check Explorer and other consumers)
if source.get("indexable") is False:
return None
if not is_absolute_url(source.get("url")):
return None
if source.get("canonicalUrl") and source["canonicalUrl"] != source["url"]:
return None
if source.get("verdictVisible") is not True:
return None # claim and conclusion must be readable on the page
reviews_on_page = source.get("claimReviewsOnPage")
if (1 if reviews_on_page is None else reviews_on_page) != 1:
return None # one ClaimReview per page
claim_reviewed = (source.get("claimSummary") or "").strip()
if not claim_reviewed:
return None
review_rating = build_rating(source.get("verdict") or {})
if not review_rating:
return None
publisher = source.get("publisher")
schema = {
"@context": "https://schema.org",
"@type": "ClaimReview",
"url": source["url"], # the page hosting the full fact check article
"claimReviewed": claim_reviewed, # keep under 75 characters to limit wrapping on mobile
"reviewRating": review_rating,
"author": {
"@type": "Organization",
"name": publisher.get("name"),
"url": publisher.get("url") if is_absolute_url(publisher.get("url")) else None,
} if publisher else None,
"itemReviewed": build_claim(source.get("claim")),
}
return compact(schema)
# Usage:
# build_claim_review_schema({
# "url": "https://www.example.com/fact-checks/2026/moon-cheese",
# "canonicalUrl": "https://www.example.com/fact-checks/2026/moon-cheese",
# "indexable": True,
# "verdictVisible": True,
# "claimReviewsOnPage": 1,
# "claimSummary": "The moon is made of cheese",
# "verdict": {"label": "False", "value": 1, "worst": 1, "best": 5},
# "publisher": {"name": "Example Fact Desk", "url": "https://www.example.com/about/corrections-policy"},
# "claim": {
# "authorName": "Square Moon Society",
# "authorSameAs": "https://www.example.org/square-moon-society",
# "datePublished": "2026-04-02",
# "appearances": ["https://www.example.org/posts/moon-facts"],
# "firstAppearance": "https://www.example.org/posts/moon-facts",
# },
# })
{
"@context": "https://schema.org",
"@type": "ClaimReview",
"url": "https://www.example.com/fact-checks/2026/moon-cheese",
"claimReviewed": "The moon is made of cheese",
"reviewRating": {
"@type": "Rating",
"alternateName": "False",
"name": "False",
"ratingValue": 1,
"worstRating": 1,
"bestRating": 5
},
"author": {
"@type": "Organization",
"name": "Example Fact Desk",
"url": "https://www.example.com/about/corrections-policy"
},
"itemReviewed": {
"@type": "Claim",
"author": {
"@type": "Organization",
"name": "Square Moon Society",
"sameAs": "https://www.example.org/square-moon-society"
},
"datePublished": "2026-04-02",
"appearance": [
"https://www.example.org/posts/moon-facts"
],
"firstAppearance": "https://www.example.org/posts/moon-facts"
}
}
The Output As An Entity Graph
The conditional-logic output above, parsed in your browser and drawn as the graph a crawler builds from it. Every nested object is a node and the property that holds it is the edge. Tap a node to inspect its properties, or drag to rearrange.
Why Conditional Logic Is Better Than Static Templates
- Pulls the claim summary, verdict, and claim source from the same editorial fields that render the article, so markup and visible verdict cannot disagree.
- Validates the rating scale in code, so a CMS default of 0 for worstRating never ships.
- Omits the numeric scale entirely when the newsroom uses text-only verdicts, instead of inventing numbers.
- Gives one switch point if you later stop emitting ClaimReview as the phase-out completes.
Property Reference
What Google's Fact check documentation requires and recommends, property by property. These are the same rules the SchemaCDN validator checks. Each name links to its schema.org definition.
Google Search no longer shows fact check rich results, but Fact Check Explorer still reads ClaimReview markup, so these properties still apply there.
| Property | Expects | Notes | |
|---|---|---|---|
| claimReviewed | Required | Text | A short summary of the claim being evaluated. Up to 75 characters. |
| reviewRating | Required | Rating | The assessment of the claim. This object supports both a numeric and a textual assessment. |
| reviewRating.alternateName | Required | Text | The truthfulness rating assigned to ClaimReview.reviewRating, as a human-readable short word or phrase. The textual value is currently the only value shown in search results. |
| reviewRating.bestRating | Recommended | Number | For numeric ratings, the best value possible in the scale from worst to best. |
| reviewRating.name | Recommended | Text | Same as alternateName, and used when alternateName is not provided. |
| reviewRating.ratingValue | Recommended | Number | A numeric rating of this claim, in the range worstRating to bestRating inclusive. |
| reviewRating.worstRating | Recommended | Number | For numeric ratings, the worst value possible in a scale from worst to best. |
| url | Required | URL | Link to the page hosting the full article of the fact check. |
| author | Recommended | Organization or Person | The publisher of the fact check article, not the publisher of the claim. |
| author.name | Recommended | Text | Name of the organization that is publishing the fact check. |
| author.url | Recommended | URL | The URL of the publisher of the fact check. This can be a home page, contact page, or other appropriate page. |
| itemReviewed | Recommended | Claim | An object describing the claim being made. |
| itemReviewed.appearance | Recommended | URL or CreativeWork, can repeat | A link to, or inline description of, a CreativeWork in which this claim appears. |
| itemReviewed.author | Recommended | Organization or Person | The author of the claim, not the author of the fact check. |
| itemReviewed.datePublished | Recommended | DateTime or Date | The date when the claim was made or entered public discourse. |
| itemReviewed.firstAppearance | Recommended | URL or CreativeWork | A link to, or inline description of, a CreativeWork in which this specific claim first appears. |