Kemp.rest / Examples / Code / Schema markup validator
KEMP.REST / EXAMPLES / CODE
Schema markup validator
Written by Kevin Kemp in HTML, CSS, and JavaScript for PressCare. A public AI SEO check. Pasted markup is checked in the browser.
View the codeThis is the schema validator in the Tools menu on presscare.com, the AI SEO check beside the generator. Paste a page or a raw JSON-LD block and it reports which required and recommended properties each entity is missing. The page builder writes this kind of graph onto each service page.
Type of code
A self-contained code block, themed to match the llms.txt generator. Add the class smv-light when it sits on a light section. No libraries, no external requests, no API keys.
Technology
HTML, CSS, JavaScript, and JSON. It finds every application/ld+json script, parses it, and unpacks arrays and @graph so nested entities are checked one by one. Fifteen types get a field-by-field check against Google’s documented properties: Article (including NewsArticle and BlogPosting), BreadcrumbList, Course, Event, FAQ, JobPosting, LocalBusiness, Organization, Person, Product, Recipe, Review, SoftwareApplication, Video, and WebSite. A missing required field is an error. A missing recommended field is a warning. Any other type is still parsed and marked as syntax-only.
What it does
- Accepts a full page source or a JSON-LD fragment, including drafts that are not published yet.
- Names the exact property path that is missing, including one-of groups where any one of several fields will satisfy the check.
- Renders a badge per entity: valid, warnings, or error, and copies a report.
- Includes a broken sample so the checks can be seen before pasting real markup.
Problem it solved
Broken schema is invisible on the page and obvious to a crawler. This checks it in the browser, before the page is published and before Google’s Rich Results Test is run on the live URL.
Code
Back to summary · schema-markup-validator.html
<!--
PressCare+ Schema Markup Validator — drop-in Code Block for Avada
Prefix: smv-
Paste-only build: HTML or raw JSON-LD is validated in the browser.
No libraries, no external requests, no API keys, nothing leaves the browser.
Theming matches the llms.txt generator (ltxg-). Add class smv-light for the khaki version.
-->
<style>
.smv-root{
--smv-ink:#061634;
--smv-panel:#0B2350;
--smv-panel-hi:#123063;
--smv-field:#081D42;
--smv-line:rgba(237,234,227,.15);
--smv-line-soft:rgba(237,234,227,.08);
--smv-fg:#EDEAE3;
--smv-fg-dim:#9AACC9;
--smv-gold:#F6B210;
--smv-gold-ink:#09255C;
--smv-paper:#F9F6F1;
--smv-paper-ink:#09255C;
--smv-flag-warn:#F6B210;
--smv-flag-bad:#E8765A;
--smv-flag-ok:#8FA6C4;
--smv-ok:#7FC8A9;
--smv-r:12px;
--smv-r-sm:8px;
--smv-mono:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,"Liberation Mono",monospace;
box-sizing:border-box;
display:block;
background:
radial-gradient(120% 90% at 8% 0%, #0D2A5E 0%, rgba(13,42,94,0) 58%),
linear-gradient(178deg,#071B45 0%,#061634 100%);
color:var(--smv-fg);
border-radius:var(--smv-r);
padding:clamp(20px,3.4vw,44px);
font-family:inherit;
line-height:1.55;
-webkit-font-smoothing:antialiased;
}
.smv-root.smv-light{
--smv-ink:#F9F6F1;
--smv-panel:#FFFFFF;
--smv-panel-hi:#F5F1EA;
--smv-field:#FFFFFF;
--smv-line:rgba(9,37,92,.16);
--smv-line-soft:rgba(9,37,92,.09);
--smv-fg:#09255C;
--smv-fg-dim:#5B6472;
--smv-paper:#FFFFFF;
--smv-flag-ok:#5B6472;
background:linear-gradient(178deg,#F9F6F1 0%,#F5F1EA 100%);
}
.smv-root *,.smv-root *::before,.smv-root *::after{box-sizing:border-box;}
/* ---------- masthead ---------- */
.smv-top{max-width:62ch;margin:0 0 clamp(22px,3vw,34px);}
.smv-root h2.smv-h{
font-size:clamp(1.5rem,1.05rem + 1.5vw,2.2rem);
line-height:1.15;letter-spacing:-.015em;font-weight:700;
color:var(--smv-fg);margin:0 0 .5rem;
}
.smv-sub{color:var(--smv-fg-dim);margin:0;font-size:1rem;}
.smv-sub a{color:var(--smv-gold);text-decoration:underline;text-underline-offset:3px;}
.smv-sub a:hover{color:var(--smv-gold);opacity:.82;}
/* ---------- frame ---------- */
.smv-grid{display:grid;grid-template-columns:minmax(0,1fr);gap:clamp(20px,2.6vw,30px);}
@media(min-width:1000px){
.smv-grid{grid-template-columns:minmax(0,.92fr) minmax(0,1.08fr);align-items:start;}
.smv-in{position:sticky;top:120px;}
}
.smv-block{
background:var(--smv-panel);
border:1px solid var(--smv-line);
border-radius:var(--smv-r);
padding:clamp(16px,2vw,24px);
}
.smv-block + .smv-block{margin-top:16px;}
.smv-root h3.smv-bh{font-size:1.02rem;font-weight:700;letter-spacing:-.005em;color:var(--smv-fg);margin:0 0 .2rem;}
.smv-bnote{color:var(--smv-fg-dim);font-size:.87rem;margin:0 0 1rem;max-width:58ch;}
.smv-bnote code,.smv-qa code,.smv-step code{
font-family:var(--smv-mono);font-size:.84em;
background:var(--smv-panel-hi);color:var(--smv-gold);
padding:1px 6px;border-radius:4px;word-break:break-word;
}
.smv-root textarea{
width:100%;
background:var(--smv-field);
color:var(--smv-fg);
border:1px solid var(--smv-line);
border-radius:var(--smv-r-sm);
padding:12px 13px;
font-family:var(--smv-mono);font-size:.84rem;line-height:1.6;
margin:0;min-height:260px;resize:vertical;box-shadow:none;
transition:border-color .14s ease, background .14s ease;
}
.smv-root textarea::placeholder{color:var(--smv-fg-dim);opacity:.62;}
.smv-root textarea:hover{border-color:rgba(246,178,16,.4);}
.smv-root textarea:focus{outline:none;border-color:var(--smv-gold);background:var(--smv-panel-hi);}
.smv-root :focus-visible{outline:2px solid var(--smv-gold);outline-offset:2px;}
/* ---------- buttons ---------- */
.smv-root button{font-family:inherit;cursor:pointer;border-radius:var(--smv-r-sm);
transition:background .14s ease,border-color .14s ease,color .14s ease,opacity .14s ease;}
.smv-btn{background:var(--smv-gold);color:var(--smv-gold-ink);border:1px solid var(--smv-gold);
font-weight:700;font-size:.92rem;padding:11px 20px;}
.smv-btn:hover{background:#FFC63A;border-color:#FFC63A;}
.smv-btn[disabled]{opacity:.45;cursor:not-allowed;}
.smv-btn-2{background:transparent;color:var(--smv-fg);border:1px solid var(--smv-line);
font-weight:600;font-size:.92rem;padding:11px 20px;}
.smv-btn-2:hover{border-color:var(--smv-gold);color:var(--smv-gold);}
.smv-btn-3{background:transparent;color:var(--smv-fg-dim);border:1px dashed var(--smv-line);
font-weight:600;font-size:.83rem;padding:8px 14px;}
.smv-btn-3:hover{border-color:var(--smv-gold);border-style:solid;color:var(--smv-gold);}
.smv-rowbtns{display:flex;gap:9px;flex-wrap:wrap;margin-top:12px;}
/* ---------- results ---------- */
.smv-sum{
background:var(--smv-panel);border:1px solid var(--smv-line);
border-left:3px solid var(--smv-gold);
border-radius:var(--smv-r-sm);padding:14px 16px;margin-bottom:14px;
}
.smv-sumline{display:flex;gap:8px;flex-wrap:wrap;align-items:center;
font-family:var(--smv-mono);font-size:.8rem;color:var(--smv-fg-dim);margin:0;}
.smv-sumline b{color:var(--smv-fg);font-weight:700;}
.smv-sumline .smv-e{color:var(--smv-flag-bad);}
.smv-sumline .smv-w{color:var(--smv-flag-warn);}
.smv-empty{
border:1px dashed var(--smv-line);border-radius:var(--smv-r);
padding:clamp(26px,4vw,44px) 22px;text-align:center;color:var(--smv-fg-dim);font-size:.92rem;
}
.smv-empty strong{display:block;color:var(--smv-fg);font-size:1rem;margin-bottom:.35rem;}
.smv-cards{display:grid;gap:12px;}
.smv-card{background:var(--smv-panel);border:1px solid var(--smv-line);
border-radius:var(--smv-r-sm);overflow:hidden;}
.smv-card.is-err{border-color:rgba(232,118,90,.5);}
.smv-card.is-warn{border-color:rgba(246,178,16,.42);}
.smv-chead{display:flex;gap:12px;align-items:center;justify-content:space-between;
padding:13px 16px;border-bottom:1px solid var(--smv-line-soft);flex-wrap:wrap;}
.smv-ctype{font-family:var(--smv-mono);font-size:.92rem;font-weight:700;color:var(--smv-fg);word-break:break-word;}
.smv-ctype small{display:block;font-family:inherit;font-size:.75rem;font-weight:400;color:var(--smv-fg-dim);margin-top:2px;}
.smv-badge{font-size:.7rem;font-weight:700;letter-spacing:.08em;text-transform:uppercase;
padding:4px 10px;border-radius:999px;white-space:nowrap;}
.smv-badge.ok{background:rgba(127,200,169,.16);color:var(--smv-ok);}
.smv-badge.warn{background:rgba(246,178,16,.16);color:var(--smv-gold);}
.smv-badge.err{background:rgba(232,118,90,.16);color:var(--smv-flag-bad);}
.smv-badge.info{background:rgba(154,172,201,.14);color:var(--smv-fg-dim);}
.smv-cbody{padding:4px 16px 14px;}
.smv-glab{font-size:.73rem;font-weight:700;letter-spacing:.07em;text-transform:uppercase;
color:var(--smv-fg-dim);margin:12px 0 5px;}
.smv-list{list-style:none;margin:0;padding:0;}
.smv-list li{display:flex;gap:10px;align-items:flex-start;font-size:.86rem;
color:var(--smv-fg-dim);padding:6px 0;border-top:1px solid var(--smv-line-soft);}
.smv-list li:first-child{border-top:none;}
.smv-flag{width:8px;height:8px;border-radius:50%;flex:0 0 8px;margin-top:7px;background:var(--smv-flag-ok);}
.smv-flag.warn{background:var(--smv-flag-warn);}
.smv-flag.bad{background:var(--smv-flag-bad);}
.smv-path{font-family:var(--smv-mono);color:var(--smv-fg);font-size:.82rem;}
.smv-note{color:var(--smv-fg-dim);font-size:.82rem;margin:10px 0 0;
border-left:2px solid var(--smv-gold);padding-left:10px;max-width:60ch;}
/* ---------- shared rhythm ---------- */
.smv-band{margin-top:clamp(30px,4vw,52px);padding-top:clamp(26px,3vw,38px);border-top:1px solid var(--smv-line);}
.smv-root h3.smv-sh{font-size:clamp(1.18rem,1rem + .6vw,1.5rem);font-weight:700;
letter-spacing:-.01em;color:var(--smv-fg);margin:0 0 .45rem;}
.smv-slead{color:var(--smv-fg-dim);font-size:.94rem;margin:0 0 clamp(18px,2.4vw,28px);max-width:60ch;}
.smv-steps{display:grid;gap:16px;grid-template-columns:minmax(0,1fr);}
@media(min-width:700px){.smv-steps{grid-template-columns:repeat(2,minmax(0,1fr));gap:18px 26px;}}
@media(min-width:1100px){.smv-steps{grid-template-columns:repeat(4,minmax(0,1fr));}}
.smv-step{display:grid;grid-template-columns:auto minmax(0,1fr);gap:14px;align-items:start;}
.smv-num{font-family:var(--smv-mono);font-size:1.05rem;font-weight:700;color:var(--smv-gold);
line-height:1;border:1px solid rgba(246,178,16,.4);border-radius:50%;
width:34px;height:34px;flex:0 0 34px;display:flex;align-items:center;justify-content:center;}
.smv-step h4{font-size:.97rem;font-weight:700;color:var(--smv-fg);margin:.35rem 0 .3rem;}
.smv-step p{color:var(--smv-fg-dim);font-size:.875rem;margin:0;max-width:42ch;}
/* ---------- type table ---------- */
.smv-types{display:flex;flex-wrap:wrap;gap:8px;}
.smv-chip{font-family:var(--smv-mono);font-size:.79rem;color:var(--smv-fg-dim);
background:var(--smv-panel);border:1px solid var(--smv-line);border-radius:999px;padding:5px 12px;}
/* ---------- faq ---------- */
.smv-faq{display:grid;gap:10px;max-width:74ch;}
.smv-q{background:var(--smv-panel);border:1px solid var(--smv-line);border-radius:var(--smv-r-sm);overflow:hidden;}
.smv-q[open]{border-color:rgba(246,178,16,.42);}
.smv-root .smv-q summary{list-style:none;cursor:pointer;display:flex;gap:14px;
align-items:flex-start;justify-content:space-between;padding:15px 18px;
font-size:.96rem;font-weight:600;color:var(--smv-fg);}
.smv-root .smv-q summary::-webkit-details-marker{display:none;}
.smv-root .smv-q summary::marker{content:"";}
.smv-root .smv-q summary:hover{color:var(--smv-gold);}
.smv-sign{flex:0 0 14px;width:14px;height:14px;position:relative;margin-top:5px;}
.smv-sign::before,.smv-sign::after{content:"";position:absolute;background:var(--smv-gold);
transition:transform .16s ease,opacity .16s ease;}
.smv-sign::before{left:0;top:6px;width:14px;height:2px;}
.smv-sign::after{left:6px;top:0;width:2px;height:14px;}
.smv-q[open] .smv-sign::after{transform:scaleY(0);opacity:0;}
.smv-qa{padding:0 18px 17px;color:var(--smv-fg-dim);font-size:.9rem;max-width:66ch;}
.smv-qa p{margin:0 0 .7rem;}
.smv-qa p:last-child{margin-bottom:0;}
.smv-qa strong{color:var(--smv-fg);font-weight:600;}
/* ---------- cta ---------- */
.smv-cta{background:var(--smv-panel);border:1px solid var(--smv-line);border-left:3px solid var(--smv-gold);
border-radius:var(--smv-r-sm);padding:clamp(20px,2.4vw,28px);display:grid;gap:18px;align-items:center;}
@media(min-width:800px){.smv-cta{grid-template-columns:minmax(0,1fr) auto;gap:30px;}}
.smv-cta h3{font-size:1.08rem;font-weight:700;margin:0 0 .5rem;color:var(--smv-fg);}
.smv-cta p{color:var(--smv-fg-dim);font-size:.91rem;margin:0;max-width:54ch;}
.smv-cta a.smv-ctabtn{display:inline-block;white-space:nowrap;background:var(--smv-gold);
color:var(--smv-gold-ink);font-weight:700;font-size:.93rem;padding:13px 24px;
border-radius:var(--smv-r-sm);text-decoration:none;}
.smv-cta a.smv-ctabtn:hover{background:#FFC63A;color:var(--smv-gold-ink);}
.smv-toast{position:fixed;left:50%;bottom:28px;transform:translateX(-50%) translateY(12px);
background:var(--smv-gold);color:var(--smv-gold-ink);font-weight:700;font-size:.88rem;
padding:11px 20px;border-radius:var(--smv-r-sm);opacity:0;pointer-events:none;
transition:opacity .18s ease,transform .18s ease;z-index:99999;}
.smv-toast.on{opacity:1;transform:translateX(-50%) translateY(0);}
@media(prefers-reduced-motion:reduce){.smv-root *,.smv-toast{transition:none !important;}}
</style>
<div class="smv-root" id="smv-app">
<div class="smv-top">
<h2 class="smv-h">Schema markup validator</h2>
<p class="smv-sub">Paste a page's HTML or raw JSON-LD and see exactly which required and recommended properties each entity is missing. Checked in your browser against <a href="https://developers.google.com/search/docs/appearance/structured-data/search-gallery" target="_blank" rel="noopener">Google's documented requirements</a>. Nothing leaves this page.</p>
</div>
<div class="smv-grid">
<div class="smv-in">
<div class="smv-block">
<h3 class="smv-bh">Paste your markup</h3>
<p class="smv-bnote">Either the full page source, or just the JSON-LD. Every <code>application/ld+json</code> block is pulled out, including entities inside <code>@graph</code> and arrays.</p>
<textarea id="smv-input" spellcheck="false" placeholder='<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Product",
"name": "Blue widget"
}
</script>'></textarea>
<div class="smv-rowbtns">
<button type="button" class="smv-btn" data-act="validate">Validate markup</button>
<button type="button" class="smv-btn-2" data-act="copy">Copy report</button>
</div>
<div class="smv-rowbtns">
<button type="button" class="smv-btn-3" data-act="sample">Load a broken sample</button>
<button type="button" class="smv-btn-3" data-act="clear">Clear</button>
</div>
</div>
</div>
<div class="smv-out">
<div id="smv-summary"></div>
<div class="smv-cards" id="smv-cards" aria-live="polite"></div>
</div>
</div>
<!-- ========== HOW IT WORKS ========== -->
<div class="smv-band">
<h3 class="smv-sh">How it works</h3>
<p class="smv-slead">Four steps, all of them in your browser. No account, no crawl, no waiting.</p>
<div class="smv-steps">
<div class="smv-step">
<span class="smv-num">1</span>
<div>
<h4>Get the markup</h4>
<p>Open your page, view source, and copy the lot. Or copy just the JSON-LD if you already know where it lives. Drafts work too — you can check markup before you publish it.</p>
</div>
</div>
<div class="smv-step">
<span class="smv-num">2</span>
<div>
<h4>Every block, every entity</h4>
<p>The validator finds each <code>ld+json</code> script, parses it, and unpacks arrays and <code>@graph</code> containers so nested entities get checked individually rather than counted as one.</p>
</div>
</div>
<div class="smv-step">
<span class="smv-num">3</span>
<div>
<h4>Field by field</h4>
<p>Each recognised type is checked against Google's documented property list. A missing required field is an error. A missing recommended field is a warning. Both name the exact path to add.</p>
</div>
</div>
<div class="smv-step">
<span class="smv-num">4</span>
<div>
<h4>Fix and re-check</h4>
<p>Add what is missing, paste it back, and watch the errors clear. When it comes up clean, run Google's Rich Results Test on the live URL for final sign-off.</p>
</div>
</div>
</div>
</div>
<!-- ========== TYPES ========== -->
<div class="smv-band">
<h3 class="smv-sh">Types checked in depth</h3>
<p class="smv-slead">These fifteen get a full field-by-field check. Anything else is still parsed and checked for valid syntax, and the card says so rather than pretending.</p>
<div class="smv-types">
<span class="smv-chip">Article</span>
<span class="smv-chip">NewsArticle</span>
<span class="smv-chip">BlogPosting</span>
<span class="smv-chip">BreadcrumbList</span>
<span class="smv-chip">Course</span>
<span class="smv-chip">Event</span>
<span class="smv-chip">FAQPage</span>
<span class="smv-chip">JobPosting</span>
<span class="smv-chip">LocalBusiness</span>
<span class="smv-chip">Organization</span>
<span class="smv-chip">Person</span>
<span class="smv-chip">Product</span>
<span class="smv-chip">Recipe</span>
<span class="smv-chip">Review</span>
<span class="smv-chip">SoftwareApplication</span>
<span class="smv-chip">VideoObject</span>
<span class="smv-chip">WebSite</span>
</div>
</div>
<!-- ========== FAQ ========== -->
<div class="smv-band">
<h3 class="smv-sh">Questions people actually ask</h3>
<p class="smv-slead">Including the ones about what this tool deliberately does not do.</p>
<div class="smv-faq">
<details class="smv-q">
<summary>What counts as an error versus a warning?<span class="smv-sign"></span></summary>
<div class="smv-qa">
<p><strong>Errors</strong> are missing required fields. Without them the entity is not eligible for its rich result at all. <strong>Warnings</strong> are missing recommended fields — the markup is still valid, but you are leaving eligibility and display detail on the table.</p>
<p>Every finding names the exact property path, so you know where in the object to add it rather than guessing.</p>
</div>
</details>
<details class="smv-q">
<summary>Why does my Article show warnings but no errors?<span class="smv-sign"></span></summary>
<div class="smv-qa">
<p>Because Google documents no required properties for Article. The widely repeated "you must have headline, image, datePublished and author" is Google's <em>recommended</em> set, not a requirement — plenty of SEO blog posts get this wrong.</p>
<p>So a bare Article is technically valid. It is also useless, which is why this tool still flags every missing recommended field rather than handing you a green badge and calling it done.</p>
</div>
</details>
<details class="smv-q">
<summary>Does it check microdata or RDFa?<span class="smv-sign"></span></summary>
<div class="smv-qa">
<p>No — JSON-LD only. It is the format Google recommends and what nearly every modern site outputs. If your theme still emits microdata, moving to JSON-LD is the fix, not a different validator.</p>
</div>
</details>
<details class="smv-q">
<summary>Is this the same as Google's Rich Results Test?<span class="smv-sign"></span></summary>
<div class="smv-qa">
<p>No, and they are worth using together. Google's test tells you whether Google will grant a rich result today, on a live URL it can crawl. This one gives you a fast field-by-field checklist against the documented requirements, on markup you have not published yet.</p>
<p>Use this one while you work. Use Google's for final sign-off.</p>
</div>
</details>
<details class="smv-q">
<summary>Why not just paste my URL?<span class="smv-sign"></span></summary>
<div class="smv-qa">
<p>Fetching a page has to happen on a server, which means quotas, rate limits and your URLs sitting in someone's log. This version does none of that — the markup is parsed in your browser and never sent anywhere.</p>
<p>View source on your page, copy, paste. It is one extra step and it costs you nothing in privacy.</p>
</div>
</details>
<details class="smv-q">
<summary>It found no structured data. What now?<span class="smv-sign"></span></summary>
<div class="smv-qa">
<p>Search the source you copied for <code>ld+json</code>. If it is not there, either the page has no structured data, or JavaScript injects it after load — in which case copy from your browser's Elements panel instead of View Source.</p>
<p>On WordPress, most schema comes from your SEO plugin. If Yoast or Rank Math is active and you are seeing nothing, the schema output setting is probably switched off.</p>
</div>
</details>
<details class="smv-q">
<summary>Can I have two blocks of schema on one page?<span class="smv-sign"></span></summary>
<div class="smv-qa">
<p>Yes, and most WordPress sites do. Google reads every block on the page. The tidier pattern is one block with an <code>@graph</code> array holding all your entities, which is what the major SEO plugins output.</p>
<p>What you should not do is describe the same thing twice in two blocks with conflicting values.</p>
</div>
</details>
<details class="smv-q">
<summary>Will fixing these errors improve my rankings?<span class="smv-sign"></span></summary>
<div class="smv-qa">
<p>Not directly. Structured data is not a ranking factor. What it changes is how your result <em>looks</em> — stars, prices, dates, breadcrumbs — and a better-looking result earns more clicks at the same position.</p>
<p>It also gives AI assistants a clean, unambiguous description of what the page is, which is becoming the more interesting half of the argument.</p>
</div>
</details>
</div>
</div>
<!-- ========== CTA ========== -->
<div class="smv-band">
<div class="smv-cta">
<div>
<h3>Want this handled for you?</h3>
<p>PressCare+ will audit the structured data across your whole WordPress site, fix what is broken, and keep it valid as your pages change — alongside the backups, updates and security work in every care plan.</p>
</div>
<a class="smv-ctabtn" href="https://presscare.com/">See care plans</a>
</div>
</div>
<div class="smv-toast" id="smv-toast" role="status"></div>
</div>
<script>
(function(){
var root = document.getElementById('smv-app');
if(!root || root.dataset.smvReady === '1') return;
root.dataset.smvReady = '1';
var inputEl = root.querySelector('#smv-input');
var sumEl = root.querySelector('#smv-summary');
var cardsEl = root.querySelector('#smv-cards');
var toastEl = root.querySelector('#smv-toast');
var lastReport = '';
/* ================= field catalog =================
Sourced from Google's structured data documentation.
r = required (missing -> error), w = recommended (missing -> warning).
================================================== */
var SPECS = {
article: {
label:'Article',
types:['article','newsarticle','blogposting'],
note:'Google documents no required properties for Article. Everything below is its recommended set, so a bare Article is technically valid and practically useless.',
r:[],
w:[
['headline','Headline'],
['image','Image'],
['datePublished','Date published (ISO 8601)'],
['dateModified','Date modified (ISO 8601)'],
['author','Author'],
['author.name','Author name'],
['publisher.name','Publisher name'],
['publisher.logo','Publisher logo']
]
},
breadcrumb: {
label:'BreadcrumbList',
types:['breadcrumblist'],
r:[['itemListElement','Breadcrumb items']],
w:[],
custom:function(e){
var out = {r:[], w:[]};
var items = asArray(e.itemListElement).filter(isObj);
if(!items.length) return out;
if(items.length < 2){
out.w.push(['itemListElement','Only one item — a breadcrumb trail needs at least two']);
}
items.forEach(function(it, i){
var n = i + 1;
if(!has(it,'position')) out.r.push(['itemListElement['+n+'].position','Position']);
if(!has(it,'name') && !has(it,'item.name')) out.r.push(['itemListElement['+n+'].name','Name']);
if(i < items.length - 1 && !has(it,'item')){
out.r.push(['itemListElement['+n+'].item','Item URL (required on all but the last crumb)']);
}
});
return out;
}
},
course: {
label:'Course',
types:['course'],
r:[
['name','Course name'],
['description','Description'],
['provider','Provider'],
['provider.name','Provider name']
],
w:[
['url','Course URL'],
['image','Image'],
['offers','Offers'],
['hasCourseInstance','Course instance (delivery mode and schedule)'],
['aggregateRating','Aggregate rating']
]
},
event: {
label:'Event',
types:['event','businessevent','childrensevent','comedyevent','coursesinstance','dancesevent','deliveryevent','educationevent','exhibitionevent','festival','foodevent','literaryevent','musicevent','publicationevent','saleevent','screeningevent','socialevent','sportsevent','theaterevent','visualartsevent'],
r:[
['name','Event name'],
['startDate','Start date (ISO 8601)'],
['location','Location']
],
w:[
['endDate','End date'],
['description','Description'],
['image','Image'],
['eventStatus','Event status'],
['eventAttendanceMode','Attendance mode (online, offline or mixed)'],
['offers','Offers'],
['offers.url','Ticket URL'],
['performer','Performer'],
['organizer','Organizer']
],
custom:function(e){
var out = {r:[], w:[]};
var loc = nested(e,'location');
if(loc){
var type = String(firstType(loc) || '').toLowerCase();
if(type === 'virtuallocation'){
if(!has(loc,'url')) out.r.push(['location.url','Virtual location URL']);
} else {
if(!has(loc,'name')) out.w.push(['location.name','Venue name']);
if(!has(loc,'address')) out.r.push(['location.address','Venue address']);
}
}
return out;
}
},
faq: {
label:'FAQPage',
types:['faqpage'],
note:'FAQ rich results are heavily restricted in Google Search and no longer show for most sites. The markup is still worth keeping as a clean description of the page for AI assistants.',
r:[['mainEntity','Questions']],
w:[],
custom:function(e){
var out = {r:[], w:[]};
var qs = asArray(e.mainEntity).filter(isObj);
qs.forEach(function(q, i){
var n = i + 1;
if(!has(q,'name')) out.r.push(['mainEntity['+n+'].name','Question text']);
if(!has(q,'acceptedAnswer')) out.r.push(['mainEntity['+n+'].acceptedAnswer','Accepted answer']);
else if(!has(q,'acceptedAnswer.text')) out.r.push(['mainEntity['+n+'].acceptedAnswer.text','Answer text']);
});
return out;
}
},
jobposting: {
label:'JobPosting',
types:['jobposting'],
r:[
['title','Job title'],
['description','Description (full HTML description of the role)'],
['datePosted','Date posted (ISO 8601)'],
['hiringOrganization','Hiring organization'],
['jobLocation','Job location']
],
w:[
['validThrough','Valid through (expiry date)'],
['employmentType','Employment type'],
['baseSalary','Base salary'],
['identifier','Job identifier'],
['hiringOrganization.name','Hiring organization name'],
['jobLocation.address','Job location address']
],
custom:function(e){
var out = {r:[], w:[]};
var remote = String(e.jobLocationType || '').toUpperCase() === 'TELECOMMUTE';
if(remote && !has(e,'applicantLocationRequirements')){
out.r.push(['applicantLocationRequirements','Applicant location requirements (required for remote roles)']);
}
return out;
}
},
localbusiness: {
label:'LocalBusiness',
types:['localbusiness','store','restaurant','foodestablishment','cafeorcoffeeshop','bakery','bar','barorpub','dentist','physician','hospital','medicalbusiness','medicalclinic','healthandbeautybusiness','hairsalon','daysspa','beautysalon','professionalservice','legalservice','attorney','notary','accountingservice','financialservice','insuranceagency','realestateagent','homeandconstructionbusiness','generalcontractor','plumber','electrician','hvacbusiness','roofingcontractor','movingcompany','locksmith','housepainter','automotivebusiness','autorepair','autodealer','gasstation','lodgingbusiness','hotel','motel','bedandbreakfast','resort','entertainmentbusiness','sportsactivitylocation','gym','healthclub','travelagency','childcare','selfstorage','emergencyservice','governmentoffice','shoppingcenter','touristinformationcenter','veterinarycare','library','radiostation','recyclingcenter','internetcafe','dryCleaningorlaundry'],
r:[
['name','Business name'],
['address','Postal address']
],
w:[
['telephone','Telephone'],
['url','Website URL'],
['image','Image'],
['openingHoursSpecification','Opening hours'],
['geo','Geo coordinates'],
['priceRange','Price range'],
['aggregateRating','Aggregate rating']
],
custom:function(e){
var out = {r:[], w:[]};
var addr = nested(e,'address');
if(addr){
[['streetAddress','Street address'],['addressLocality','City'],['addressCountry','Country']].forEach(function(f){
if(!has(addr,f[0])) out.r.push(['address.'+f[0],f[1]]);
});
[['addressRegion','Region or state'],['postalCode','Postal code']].forEach(function(f){
if(!has(addr,f[0])) out.w.push(['address.'+f[0],f[1]]);
});
}
var geo = nested(e,'geo');
if(geo){
if(!has(geo,'latitude')) out.w.push(['geo.latitude','Latitude']);
if(!has(geo,'longitude')) out.w.push(['geo.longitude','Longitude']);
}
return out;
}
},
organization: {
label:'Organization',
types:['organization','corporation','ngo','educationalorganization','collegeoruniversity','school','onlinestore','onlinebusiness','airline','sportsteam','performinggroup','newsmediaorganization','governmentorganization'],
r:[['name','Organization name']],
w:[
['url','Website URL'],
['logo','Logo'],
['sameAs','Social or reference profiles (sameAs)'],
['description','Description'],
['address','Postal address'],
['contactPoint','Contact point']
]
},
person: {
label:'Person',
types:['person'],
note:'Person markup does not produce a rich result on its own. It matters as an author or entity reference, so the name and identity links are what count.',
r:[['name','Name']],
w:[
['url','URL'],
['image','Image'],
['jobTitle','Job title'],
['sameAs','Identity links (sameAs)'],
['worksFor','Works for']
]
},
product: {
label:'Product',
types:['product','productgroup','individualproduct','productmodel','vehicle','car'],
r:[
['name','Product name'],
['image','Product image']
],
oneOf:[{
paths:['offers','review','aggregateRating'],
label:'One of offers, review or aggregateRating'
}],
w:[
['description','Description'],
['brand','Brand'],
['sku','SKU'],
['gtin','GTIN or other product identifier'],
['aggregateRating','Aggregate rating'],
['review','Review']
],
custom:function(e){
var out = {r:[], w:[]};
var offer = nested(e,'offers');
if(offer){
var isAgg = String(firstType(offer) || '').toLowerCase() === 'aggregateoffer';
if(isAgg){
if(!has(offer,'lowPrice')) out.r.push(['offers.lowPrice','Low price']);
if(!has(offer,'priceCurrency')) out.r.push(['offers.priceCurrency','Price currency']);
if(!has(offer,'highPrice')) out.w.push(['offers.highPrice','High price']);
} else {
if(!has(offer,'price') && !has(offer,'priceSpecification')) out.r.push(['offers.price','Price']);
if(!has(offer,'priceCurrency') && !has(offer,'priceSpecification')) out.r.push(['offers.priceCurrency','Price currency']);
if(!has(offer,'availability')) out.w.push(['offers.availability','Availability']);
if(!has(offer,'url')) out.w.push(['offers.url','Offer URL']);
if(!has(offer,'shippingDetails')) out.w.push(['offers.shippingDetails','Shipping details']);
if(!has(offer,'hasMerchantReturnPolicy')) out.w.push(['offers.hasMerchantReturnPolicy','Return policy']);
}
}
var ar = nested(e,'aggregateRating');
if(ar){
if(!has(ar,'ratingValue')) out.r.push(['aggregateRating.ratingValue','Rating value']);
if(!has(ar,'ratingCount') && !has(ar,'reviewCount')){
out.r.push(['aggregateRating.reviewCount','Review count or rating count']);
}
}
var rv = nested(e,'review');
if(rv){
if(!has(rv,'author')) out.r.push(['review.author','Review author']);
if(!has(rv,'reviewRating.ratingValue')) out.r.push(['review.reviewRating.ratingValue','Review rating value']);
}
return out;
}
},
recipe: {
label:'Recipe',
types:['recipe'],
r:[
['name','Recipe name'],
['image','Image of the finished dish'],
['recipeIngredient','Ingredients'],
['recipeInstructions','Instructions']
],
w:[
['description','Description'],
['author','Author'],
['datePublished','Date published'],
['prepTime','Prep time (ISO 8601 duration)'],
['cookTime','Cook time (ISO 8601 duration)'],
['totalTime','Total time (ISO 8601 duration)'],
['recipeYield','Yield'],
['recipeCategory','Category'],
['recipeCuisine','Cuisine'],
['nutrition.calories','Calories'],
['aggregateRating','Aggregate rating'],
['video','Video']
]
},
review: {
label:'Review',
types:['review','claimreview','userreview','criticreview'],
r:[
['itemReviewed','Item reviewed'],
['itemReviewed.name','Name of the item reviewed'],
['author','Author'],
['reviewRating','Review rating'],
['reviewRating.ratingValue','Rating value']
],
w:[
['datePublished','Date published'],
['reviewBody','Review body'],
['reviewRating.bestRating','Best rating'],
['reviewRating.worstRating','Worst rating'],
['publisher','Publisher']
]
},
software: {
label:'SoftwareApplication',
types:['softwareapplication','mobileapplication','webapplication','videogame'],
r:[
['name','Application name'],
['applicationCategory','Application category'],
['operatingSystem','Operating system']
],
oneOf:[{
paths:['offers','aggregateRating','review'],
label:'One of offers, aggregateRating or review'
}],
w:[
['description','Description'],
['screenshot','Screenshot'],
['aggregateRating.ratingValue','Rating value'],
['aggregateRating.ratingCount','Rating count'],
['offers.price','Price'],
['offers.priceCurrency','Price currency']
]
},
video: {
label:'VideoObject',
types:['videoobject','videogameclip','clip'],
r:[
['name','Video title'],
['thumbnailUrl','Thumbnail URL'],
['uploadDate','Upload date (ISO 8601)']
],
oneOf:[{
paths:['contentUrl','embedUrl'],
label:'One of contentUrl or embedUrl'
}],
w:[
['description','Description'],
['duration','Duration (ISO 8601 duration)'],
['interactionStatistic','View count (interactionStatistic)'],
['expires','Expiry date, if the video should stop showing'],
['publisher','Publisher']
]
},
website: {
label:'WebSite',
types:['website'],
r:[
['name','Site name'],
['url','Site URL']
],
w:[
['potentialAction','Sitelinks search box (potentialAction)'],
['publisher','Publisher'],
['alternateName','Alternate name']
],
custom:function(e){
var out = {r:[], w:[]};
var pa = nested(e,'potentialAction');
if(pa){
if(!has(pa,'target')) out.r.push(['potentialAction.target','Search target URL template']);
if(!has(pa,'query-input')) out.r.push(['potentialAction.query-input','Query input']);
}
return out;
}
}
};
/* build a lookup: lowercased @type -> spec */
var INDEX = {};
Object.keys(SPECS).forEach(function(k){
SPECS[k].types.forEach(function(t){ INDEX[t] = SPECS[k]; });
});
/* ================= helpers ================= */
function esc(s){
return String(s).replace(/[&<>"']/g, function(c){
return {'&':'&','<':'<','>':'>','"':'"',"'":'''}[c];
});
}
function isObj(v){ return v !== null && typeof v === 'object' && !Array.isArray(v); }
function asArray(v){
if(v === null || v === undefined) return [];
return Array.isArray(v) ? v : [v];
}
function firstType(o){
var t = o && o['@type'];
if(Array.isArray(t)) return t[0];
return t;
}
/* First nested object under key, or null if it is absent or wholly empty.
Stops an empty {} being reported once as a missing parent and again
for every sub-property inside it. */
function nested(entity, key){
var o = asArray(entity[key]).filter(isObj)[0];
return (o && Object.keys(o).length) ? o : null;
}
function typeLabel(o){
var t = o && o['@type'];
if(Array.isArray(t)) return t.join(', ');
return t || '(no @type)';
}
function pick(objs, key){
var out = [];
objs.forEach(function(o){
asArray(o).forEach(function(x){
if(isObj(x) && Object.prototype.hasOwnProperty.call(x, key)) out.push(x[key]);
});
});
return out;
}
function get(obj, path){
var cur = [obj];
path.split('.').forEach(function(k){ cur = pick(cur, k); });
return cur;
}
function isFilled(v){
if(v === null || v === undefined) return false;
if(typeof v === 'string') return v.trim() !== '';
if(Array.isArray(v)) return v.some(isFilled);
if(typeof v === 'object') return Object.keys(v).length > 0;
return true;
}
function has(obj, path){
return get(obj, path).some(isFilled);
}
function toast(msg){
toastEl.textContent = msg;
toastEl.classList.add('on');
clearTimeout(toastEl._t);
toastEl._t = setTimeout(function(){ toastEl.classList.remove('on'); }, 1900);
}
/* ================= extraction ================= */
function extractBlocks(raw){
var text = String(raw || '').trim();
var blocks = [];
if(!text) return blocks;
var looksJson = text.charAt(0) === '{' || text.charAt(0) === '[';
if(looksJson){
blocks.push(text);
return blocks;
}
try{
var doc = new DOMParser().parseFromString(text, 'text/html');
var nodes = doc.querySelectorAll('script[type="application/ld+json"], script[type="application/ld+JSON"]');
for(var i = 0; i < nodes.length; i++){
blocks.push(nodes[i].textContent);
}
}catch(err){ /* fall through */ }
if(!blocks.length){
/* last resort: markup pasted without a parseable wrapper */
var re = /<script[^>]*application\/ld\+json[^>]*>([\s\S]*?)<\/script>/gi;
var m;
while((m = re.exec(text)) !== null){ blocks.push(m[1]); }
}
return blocks;
}
function collectEntities(parsed, out){
asArray(parsed).forEach(function(node){
if(!isObj(node)) return;
if(Object.prototype.hasOwnProperty.call(node, '@graph')){
collectEntities(node['@graph'], out);
var rest = {};
var keep = false;
Object.keys(node).forEach(function(k){
if(k !== '@graph' && k !== '@context'){ rest[k] = node[k]; keep = true; }
});
if(keep && rest['@type']) out.push(rest);
return;
}
out.push(node);
});
return out;
}
/* ================= checking ================= */
function checkEntity(entity){
var typeName = typeLabel(entity);
var types = asArray(entity['@type']).map(function(t){
return String(t || '').replace(/^.*[\/#]/, '').toLowerCase();
});
var spec = null;
for(var i = 0; i < types.length; i++){
if(INDEX[types[i]]){ spec = INDEX[types[i]]; break; }
}
var res = { type: typeName, spec: spec, errors: [], warnings: [], status: 'detected', note: '' };
if(!entity['@type']){
res.status = 'error';
res.errors.push(['@type','Every entity needs an @type']);
return res;
}
if(!spec) return res;
res.note = spec.note || '';
(spec.r || []).forEach(function(f){
if(!has(entity, f[0])) res.errors.push(f);
});
(spec.oneOf || []).forEach(function(group){
var ok = group.paths.some(function(p){ return has(entity, p); });
if(!ok) res.errors.push([group.paths.join(' | '), group.label]);
});
(spec.w || []).forEach(function(f){
if(!has(entity, f[0])) res.warnings.push(f);
});
if(spec.custom){
var extra = spec.custom(entity) || {};
(extra.r || []).forEach(function(f){ res.errors.push(f); });
(extra.w || []).forEach(function(f){ res.warnings.push(f); });
}
res.status = res.errors.length ? 'error' : (res.warnings.length ? 'warning' : 'valid');
return res;
}
/* ================= render ================= */
var BADGE = {
valid: ['ok','Valid'],
warning: ['warn','Warnings'],
error: ['err','Errors'],
detected:['info','Detected'],
broken: ['err','Invalid JSON']
};
function renderFindings(list, level){
if(!list.length) return '';
var cls = level === 'error' ? 'bad' : 'warn';
var head = level === 'error' ? 'Missing required' : 'Missing recommended';
return '<p class="smv-glab">' + head + '</p><ul class="smv-list">' +
list.map(function(f){
return '<li><span class="smv-flag ' + cls + '"></span><span>' +
'<span class="smv-path">' + esc(f[0]) + '</span> — ' + esc(f[1]) +
'</span></li>';
}).join('') + '</ul>';
}
function validate(){
var raw = inputEl.value;
var blocks = extractBlocks(raw);
if(!blocks.length){
sumEl.innerHTML = '';
cardsEl.innerHTML = '<div class="smv-empty"><strong>No JSON-LD found</strong>' +
'Nothing in what you pasted contains an <code>application/ld+json</code> block or a JSON object. ' +
'Search your page source for <code>ld+json</code> to confirm it is there.</div>';
lastReport = 'No JSON-LD found in the pasted markup.';
return;
}
var entities = [];
var badBlocks = [];
blocks.forEach(function(b, i){
var parsed;
try{
parsed = JSON.parse(b);
}catch(err){
badBlocks.push({ index: i + 1, message: String(err.message || err) });
return;
}
collectEntities(parsed, entities);
});
var results = entities.map(checkEntity);
var errors = results.reduce(function(n, r){ return n + r.errors.length; }, 0);
var warnings = results.reduce(function(n, r){ return n + r.warnings.length; }, 0);
/* summary */
var bits = [];
bits.push('<b>' + blocks.length + '</b> block' + (blocks.length === 1 ? '' : 's'));
bits.push('<b>' + entities.length + '</b> entit' + (entities.length === 1 ? 'y' : 'ies'));
bits.push('<span class="smv-e"><b>' + errors + '</b> error' + (errors === 1 ? '' : 's') + '</span>');
bits.push('<span class="smv-w"><b>' + warnings + '</b> warning' + (warnings === 1 ? '' : 's') + '</span>');
if(badBlocks.length){
bits.push('<span class="smv-e"><b>' + badBlocks.length + '</b> unparseable</span>');
}
sumEl.innerHTML = '<div class="smv-sum"><p class="smv-sumline">' +
bits.join('<span>·</span>') + '</p></div>';
/* cards */
var html = '';
badBlocks.forEach(function(b){
html += '<div class="smv-card is-err"><div class="smv-chead">' +
'<span class="smv-ctype">Block ' + b.index + '<small>Not valid JSON — nothing in it could be checked</small></span>' +
'<span class="smv-badge err">Invalid JSON</span></div>' +
'<div class="smv-cbody"><ul class="smv-list"><li><span class="smv-flag bad"></span><span>' +
esc(b.message) + '</span></li></ul></div></div>';
});
results.forEach(function(r){
var badge = BADGE[r.status] || BADGE.detected;
var cls = r.status === 'error' ? ' is-err' : (r.status === 'warning' ? ' is-warn' : '');
var sub = r.spec
? (r.spec.label === r.type ? 'Checked against Google’s field list' : 'Checked as ' + esc(r.spec.label))
: 'Outside the 15-type catalog — syntax only, not checked field by field';
html += '<div class="smv-card' + cls + '">' +
'<div class="smv-chead"><span class="smv-ctype">' + esc(r.type) + '<small>' + sub + '</small></span>' +
'<span class="smv-badge ' + badge[0] + '">' + badge[1] + '</span></div>';
var body = renderFindings(r.errors, 'error') + renderFindings(r.warnings, 'warning');
if(!body && r.spec){
body = '<p class="smv-glab">Nothing missing</p><ul class="smv-list"><li>' +
'<span class="smv-flag"></span><span>Every required and recommended property is present.</span></li></ul>';
}
if(r.note) body += '<p class="smv-note">' + esc(r.note) + '</p>';
if(body) html += '<div class="smv-cbody">' + body + '</div>';
html += '</div>';
});
if(!results.length && !badBlocks.length){
html = '<div class="smv-empty"><strong>No entities found</strong>' +
'The JSON parsed, but there were no objects with an <code>@type</code> inside it.</div>';
}
cardsEl.innerHTML = html;
lastReport = buildReport(blocks.length, entities.length, errors, warnings, badBlocks, results);
}
function buildReport(blockCount, entityCount, errors, warnings, badBlocks, results){
var L = [];
L.push('Schema markup validation report');
L.push('Source: pasted markup');
L.push(blockCount + ' JSON-LD block(s), ' + entityCount + ' entity(ies), ' +
errors + ' error(s), ' + warnings + ' warning(s)' +
(badBlocks.length ? ', ' + badBlocks.length + ' unparseable block(s)' : ''));
badBlocks.forEach(function(b){
L.push('');
L.push('Block ' + b.index + ' — INVALID JSON');
L.push(' ' + b.message);
});
results.forEach(function(r){
L.push('');
L.push(r.type + ' — ' + (BADGE[r.status] || BADGE.detected)[1].toUpperCase());
if(!r.spec){
L.push(' Outside the 15-type catalog. Syntax checked only.');
return;
}
if(r.errors.length){
L.push(' Missing required:');
r.errors.forEach(function(f){ L.push(' - ' + f[0] + ' (' + f[1] + ')'); });
}
if(r.warnings.length){
L.push(' Missing recommended:');
r.warnings.forEach(function(f){ L.push(' - ' + f[0] + ' (' + f[1] + ')'); });
}
if(!r.errors.length && !r.warnings.length){
L.push(' Nothing missing.');
}
});
L.push('');
L.push('Checked with the free schema markup validator at https://presscare.com/');
return L.join('\n');
}
var SAMPLE = '<script type="application/ld+json">\n' +
JSON.stringify({
"@context":"https://schema.org",
"@graph":[
{
"@type":"Product",
"name":"Stoneware pour-over kettle",
"offers":{ "@type":"Offer", "price":"48.00" }
},
{
"@type":"LocalBusiness",
"name":"Kettle & Co",
"address":{ "@type":"PostalAddress", "addressLocality":"Portland" },
"telephone":"+1-503-555-0142"
},
{
"@type":"BlogPosting",
"headline":"How to brew better coffee at home",
"author":{ "@type":"Person" }
},
{
"@type":"OpeningHoursSpecification",
"dayOfWeek":"Monday"
}
]
}, null, 2) +
'\n<\/script>';
/* ================= events ================= */
root.addEventListener('click', function(e){
var btn = e.target.closest('button[data-act]');
if(!btn) return;
var act = btn.dataset.act;
if(act === 'validate'){ validate(); return; }
if(act === 'sample'){
inputEl.value = SAMPLE;
validate();
return;
}
if(act === 'clear'){
inputEl.value = '';
sumEl.innerHTML = '';
cardsEl.innerHTML = '';
lastReport = '';
inputEl.focus();
return;
}
if(act === 'copy'){
if(!lastReport){ toast('Validate something first'); return; }
var done = function(){ toast('Report copied'); };
if(navigator.clipboard && navigator.clipboard.writeText){
navigator.clipboard.writeText(lastReport).then(done, function(){ fallbackCopy(lastReport, done); });
} else {
fallbackCopy(lastReport, done);
}
return;
}
});
function fallbackCopy(text, done){
var ta = document.createElement('textarea');
ta.value = text;
ta.setAttribute('readonly','');
ta.style.position = 'fixed';
ta.style.opacity = '0';
document.body.appendChild(ta);
ta.select();
try{ document.execCommand('copy'); done(); }
catch(err){ toast('Copy failed — select the report manually'); }
document.body.removeChild(ta);
}
inputEl.addEventListener('keydown', function(e){
if((e.metaKey || e.ctrlKey) && e.key === 'Enter'){ e.preventDefault(); validate(); }
});
cardsEl.innerHTML = '<div class="smv-empty"><strong>Paste your markup to start</strong>' +
'Full page source or raw JSON-LD, either works. Nothing is sent anywhere.</div>';
})();
</script>