Kemp.rest / Examples / Code / Schema markup generator

KEMP.REST / EXAMPLES / CODE

Schema markup generator

Written by Kevin Kemp in HTML, CSS, and JavaScript for PressCare. A public AI SEO tool. JSON-LD is assembled in the browser.

View the code

This is the schema generator in the Tools menu on presscare.com, a public AI SEO tool. It builds JSON-LD for 15 schema types, marks the fields Google asks for, and can read markup that is already on a page. Nothing is uploaded or stored. The page builder writes Service and FAQPage JSON-LD onto each service page from the business record. The validator checks a block before it is published.

Type of code

A self-contained code block. One script defines the field spec for each type, draws the form, and writes the JSON-LD.

Technology

HTML, CSS, JavaScript, and JSON-LD. The 15 types are Article, Course, FAQ page, Recipe, Video, Product, Review, Software app, Job posting, Local business (with subtypes such as Restaurant, Store, Dentist, and LegalService), Organization, Person, Event, Breadcrumbs, and Website. Fields are required, recommended, or optional. Import accepts a JSON-LD block or a full page source and fills the form from the ld+json it finds. The only sample host in the source is example.com.

What it does

Problem it solved

Structured data had to be valid JSON-LD a person could paste into a page, including a page that already had schema, without a server, a crawl allowance, or an account.

Code

Back to summary · presscare.com/free-online-schema-markup-generator/

<style>
.smgx-root{
  --smgx-ink:#061634;
  --smgx-panel:#0B2350;
  --smgx-panel-hi:#123063;
  --smgx-field:#081D42;
  --smgx-line:rgba(237,234,227,.15);
  --smgx-line-soft:rgba(237,234,227,.08);
  --smgx-fg:#EDEAE3;
  --smgx-fg-dim:#9AACC9;
  --smgx-gold:#F6B210;
  --smgx-gold-ink:#09255C;
  --smgx-paper:#F9F6F1;
  --smgx-paper-ink:#09255C;
  --smgx-paper-dim:#5B6472;
  --smgx-req:#E8765A;
  --smgx-rec:#F6B210;
  --smgx-ok:#7FC8A9;
  --smgx-r:12px;
  --smgx-r-sm:8px;
  --smgx-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(--smgx-fg);
  border-radius:var(--smgx-r);
  padding:clamp(20px,3.4vw,44px);
  font-family:inherit;line-height:1.55;-webkit-font-smoothing:antialiased;
}
.smgx-root *,.smgx-root *::before,.smgx-root *::after{box-sizing:border-box;}

/* masthead */
.smgx-top{max-width:64ch;margin:0 0 clamp(20px,2.6vw,30px);}
.smgx-root h2.smgx-h{font-size:clamp(1.5rem,1.05rem + 1.5vw,2.2rem);line-height:1.15;letter-spacing:-.015em;font-weight:700;color:var(--smgx-fg);margin:0 0 .5rem;}
.smgx-sub{color:var(--smgx-fg-dim);margin:0;font-size:1rem;}
.smgx-sub a{color:var(--smgx-gold);text-decoration:underline;text-underline-offset:3px;}

/* type picker */
.smgx-picker{background:var(--smgx-panel);border:1px solid var(--smgx-line);border-radius:var(--smgx-r);padding:clamp(14px,1.8vw,20px);margin-bottom:16px;}
.smgx-pickhead{display:flex;justify-content:space-between;align-items:baseline;gap:12px;flex-wrap:wrap;margin-bottom:12px;}
.smgx-root h3.smgx-bh{font-size:1.02rem;font-weight:700;color:var(--smgx-fg);margin:0;}
.smgx-count{font-family:var(--smgx-mono);font-size:.76rem;color:var(--smgx-fg-dim);}
.smgx-grp{margin-bottom:12px;}
.smgx-grp:last-child{margin-bottom:0;}
.smgx-grplab{font-size:.72rem;letter-spacing:.12em;text-transform:uppercase;color:var(--smgx-fg-dim);margin:0 0 7px;}
.smgx-chips{display:flex;flex-wrap:wrap;gap:7px;}
.smgx-root button.smgx-chip{
  background:var(--smgx-field);border:1px solid var(--smgx-line);color:var(--smgx-fg);
  border-radius:999px;padding:7px 14px;font-size:.85rem;font-weight:600;font-family:inherit;cursor:pointer;
  transition:border-color .14s ease,background .14s ease,color .14s ease;
}
.smgx-root button.smgx-chip:hover{border-color:rgba(246,178,16,.5);color:var(--smgx-gold);}
.smgx-root button.smgx-chip[aria-pressed="true"]{background:var(--smgx-gold);border-color:var(--smgx-gold);color:var(--smgx-gold-ink);}

/* layout */
.smgx-grid{display:grid;grid-template-columns:minmax(0,1fr);gap:clamp(18px,2.4vw,28px);}
@media(min-width:1040px){
  .smgx-grid{grid-template-columns:minmax(0,1.04fr) minmax(0,.96fr);align-items:start;}
  .smgx-out{position:sticky;top:120px;}
}

/* readiness strip */
.smgx-strip{background:var(--smgx-panel);border:1px solid var(--smgx-line);border-radius:var(--smgx-r-sm);padding:12px 14px;margin-bottom:14px;}
.smgx-stripbar{display:flex;gap:16px;flex-wrap:wrap;align-items:center;font-family:var(--smgx-mono);font-size:.78rem;}
.smgx-tally{display:flex;align-items:center;gap:7px;color:var(--smgx-fg-dim);}
.smgx-dot{width:8px;height:8px;border-radius:50%;flex:0 0 8px;}
.smgx-dot.req{background:var(--smgx-req);}
.smgx-dot.rec{background:var(--smgx-rec);}
.smgx-dot.ok{background:var(--smgx-ok);}
.smgx-issues{list-style:none;margin:10px 0 0;padding:0;display:grid;gap:5px;}
.smgx-issues li{margin:0;}
.smgx-root button.smgx-issue{
  background:none;border:none;padding:0;font-family:inherit;font-size:.83rem;text-align:left;
  color:var(--smgx-fg-dim);cursor:pointer;text-decoration:underline;text-decoration-style:dotted;text-underline-offset:3px;
}
.smgx-root button.smgx-issue:hover{color:var(--smgx-gold);}
.smgx-clear{color:var(--smgx-ok);font-size:.83rem;margin:10px 0 0;}

/* editor */
.smgx-block{background:var(--smgx-panel);border:1px solid var(--smgx-line);border-radius:var(--smgx-r);padding:clamp(16px,2vw,24px);}
.smgx-block + .smgx-block{margin-top:14px;}
.smgx-bnote{color:var(--smgx-fg-dim);font-size:.87rem;margin:.2rem 0 1rem;max-width:58ch;}
.smgx-tierhead{display:flex;align-items:center;gap:9px;margin:0 0 .2rem;}
.smgx-pill{font-size:.68rem;letter-spacing:.09em;text-transform:uppercase;font-weight:700;padding:3px 9px;border-radius:999px;}
.smgx-pill.req{background:rgba(232,118,90,.16);color:var(--smgx-req);}
.smgx-pill.rec{background:rgba(246,178,16,.16);color:var(--smgx-rec);}
.smgx-pill.opt{background:rgba(154,172,201,.14);color:var(--smgx-fg-dim);}

.smgx-field{margin-bottom:14px;}
.smgx-field:last-child{margin-bottom:0;}
.smgx-root label.smgx-lab{display:block;font-size:.83rem;font-weight:600;color:var(--smgx-fg);margin:0 0 5px;}
.smgx-hint{display:block;font-weight:400;color:var(--smgx-fg-dim);font-size:.79rem;margin-top:2px;}
.smgx-root input[type=text],.smgx-root input[type=url],.smgx-root input[type=number],
.smgx-root input[type=date],.smgx-root input[type=time],.smgx-root textarea,.smgx-root select{
  width:100%;background:var(--smgx-field);color:var(--smgx-fg);
  border:1px solid var(--smgx-line);border-radius:var(--smgx-r-sm);
  padding:10px 12px;font-family:inherit;font-size:.93rem;line-height:1.45;
  margin:0;min-height:0;box-shadow:none;
  transition:border-color .14s ease,background .14s ease;
}
.smgx-root textarea{resize:vertical;min-height:82px;}
.smgx-root select{appearance:none;-webkit-appearance:none;background-image:linear-gradient(45deg,transparent 50%,var(--smgx-fg-dim) 50%),linear-gradient(135deg,var(--smgx-fg-dim) 50%,transparent 50%);background-position:calc(100% - 18px) 50%,calc(100% - 13px) 50%;background-size:5px 5px,5px 5px;background-repeat:no-repeat;padding-right:36px;}
.smgx-root input::placeholder,.smgx-root textarea::placeholder{color:var(--smgx-fg-dim);opacity:.65;}
.smgx-root input:focus,.smgx-root textarea:focus,.smgx-root select:focus{outline:none;border-color:var(--smgx-gold);background:var(--smgx-panel-hi);}
.smgx-root :focus-visible{outline:2px solid var(--smgx-gold);outline-offset:2px;}
.smgx-field[data-tier=req] input,.smgx-field[data-tier=req] textarea,.smgx-field[data-tier=req] select{border-left:3px solid var(--smgx-req);}
.smgx-field[data-tier=rec] input,.smgx-field[data-tier=rec] textarea,.smgx-field[data-tier=rec] select{border-left:3px solid var(--smgx-rec);}
.smgx-field.smgx-lit input,.smgx-field.smgx-lit textarea,.smgx-field.smgx-lit select{border-color:var(--smgx-gold);background:var(--smgx-panel-hi);}

/* repeaters */
.smgx-rep{border:1px solid var(--smgx-line);border-radius:var(--smgx-r-sm);background:var(--smgx-panel-hi);padding:12px;}
.smgx-row{display:grid;gap:8px;padding:10px 0;border-top:1px solid var(--smgx-line-soft);align-items:end;}
.smgx-row:first-child{border-top:none;padding-top:0;}
@media(min-width:620px){.smgx-row{grid-template-columns:var(--smgx-cols) auto;}}
.smgx-row .smgx-lab{font-size:.74rem;color:var(--smgx-fg-dim);font-weight:500;}
.smgx-root button.smgx-x{background:transparent;border:1px solid var(--smgx-line);color:var(--smgx-fg-dim);width:34px;height:38px;flex:0 0 34px;font-size:1.05rem;line-height:1;padding:0;border-radius:var(--smgx-r-sm);cursor:pointer;font-family:inherit;}
.smgx-root button.smgx-x:hover{border-color:var(--smgx-req);color:var(--smgx-req);}
.smgx-root button.smgx-add{background:transparent;color:var(--smgx-fg-dim);border:1px dashed var(--smgx-line);border-radius:var(--smgx-r-sm);font-weight:600;font-size:.82rem;padding:8px 14px;margin-top:10px;cursor:pointer;font-family:inherit;}
.smgx-root button.smgx-add:hover{border-color:var(--smgx-gold);border-style:solid;color:var(--smgx-gold);}

/* notices */
.smgx-note{border-left:2px solid var(--smgx-gold);padding:2px 0 2px 11px;color:var(--smgx-fg-dim);font-size:.83rem;margin:0 0 14px;max-width:60ch;}
.smgx-note strong{color:var(--smgx-fg);font-weight:600;}

/* output */
.smgx-tab{display:inline-flex;align-items:center;gap:8px;background:var(--smgx-paper);color:var(--smgx-paper-ink);font-family:var(--smgx-mono);font-size:.8rem;font-weight:600;padding:6px 14px;border-radius:var(--smgx-r-sm) var(--smgx-r-sm) 0 0;}
.smgx-tab i{width:7px;height:7px;border-radius:50%;background:var(--smgx-gold);display:block;}
.smgx-paper{background:var(--smgx-paper);border-radius:0 var(--smgx-r) var(--smgx-r) var(--smgx-r);padding:18px 20px;max-height:min(52vh,520px);overflow:auto;}
.smgx-pre{margin:0;font-family:var(--smgx-mono);font-size:.83rem;line-height:1.7;color:var(--smgx-paper-ink);white-space:pre-wrap;word-break:break-word;background:none;border:none;padding:0;}
.smgx-acts{display:flex;gap:10px;flex-wrap:wrap;align-items:center;margin-top:14px;}
.smgx-root button.smgx-btn{background:var(--smgx-gold);color:var(--smgx-gold-ink);border:1px solid var(--smgx-gold);font-weight:700;font-size:.92rem;padding:11px 20px;border-radius:var(--smgx-r-sm);cursor:pointer;font-family:inherit;}
.smgx-root button.smgx-btn:hover{background:#FFC63A;border-color:#FFC63A;}
.smgx-root button.smgx-btn2{background:transparent;color:var(--smgx-fg);border:1px solid var(--smgx-line);font-weight:600;font-size:.92rem;padding:11px 20px;border-radius:var(--smgx-r-sm);cursor:pointer;font-family:inherit;}
.smgx-root button.smgx-btn2:hover{border-color:var(--smgx-gold);color:var(--smgx-gold);}
.smgx-check{display:flex;align-items:center;gap:8px;font-size:.85rem;color:var(--smgx-fg);cursor:pointer;}
.smgx-root input[type=checkbox].smgx-cb{appearance:none;-webkit-appearance:none;width:17px;height:17px;flex:0 0 17px;margin:0;border:1px solid var(--smgx-line);border-radius:4px;background:var(--smgx-field);cursor:pointer;position:relative;}
.smgx-root input[type=checkbox].smgx-cb:checked{background:var(--smgx-gold);border-color:var(--smgx-gold);}
.smgx-root input[type=checkbox].smgx-cb:checked::after{content:"";position:absolute;left:5px;top:1px;width:5px;height:10px;border:solid var(--smgx-gold-ink);border-width:0 2px 2px 0;transform:rotate(42deg);}
.smgx-rrt{color:var(--smgx-gold);font-size:.85rem;text-decoration:underline;text-underline-offset:3px;}

/* preview */
.smgx-pv{margin-top:18px;background:var(--smgx-panel);border:1px solid var(--smgx-line);border-radius:var(--smgx-r);padding:16px;}
.smgx-pvlab{font-size:.72rem;letter-spacing:.12em;text-transform:uppercase;color:var(--smgx-fg-dim);margin:0 0 10px;}
.smgx-card{background:#fff;color:#202124;border-radius:10px;padding:14px 16px;font-size:.86rem;line-height:1.45;}
.smgx-card .u{color:#4d5156;font-size:.76rem;display:block;margin-bottom:3px;word-break:break-all;}
.smgx-card .t{color:#1a0dab;font-size:1.02rem;font-weight:500;line-height:1.3;display:block;margin-bottom:4px;}
.smgx-card .d{color:#4d5156;font-size:.83rem;}
.smgx-card .st{color:#e8a33d;letter-spacing:1px;}
.smgx-card .mt{color:#70757a;font-size:.78rem;margin-top:6px;display:block;}
.smgx-card .thumb{width:100%;aspect-ratio:16/9;border-radius:8px;background:#e8eaed center/cover no-repeat;margin-bottom:10px;display:block;}
.smgx-card .crumbs{color:#202124;font-size:.78rem;}
.smgx-card .pill{display:inline-block;background:#f1f3f4;color:#3c4043;border-radius:999px;padding:3px 10px;font-size:.74rem;margin:0 5px 5px 0;}
.smgx-card .qa{border-top:1px solid #e0e0e0;padding:9px 0;color:#202124;font-size:.85rem;display:flex;justify-content:space-between;gap:10px;}
.smgx-card .qa:first-of-type{border-top:none;}
.smgx-card .datebox{display:inline-block;border:1px solid #dadce0;border-radius:8px;padding:6px 10px;text-align:center;margin-right:12px;vertical-align:top;}
.smgx-card .datebox b{display:block;font-size:1.1rem;color:#202124;line-height:1.1;}
.smgx-card .datebox span{font-size:.68rem;text-transform:uppercase;letter-spacing:.08em;color:#70757a;}
.smgx-pvnote{color:var(--smgx-fg-dim);font-size:.78rem;margin:10px 0 0;}

/* import */
.smgx-imp{margin-top:14px;background:var(--smgx-panel);border:1px solid var(--smgx-line);border-radius:var(--smgx-r);padding:clamp(14px,1.8vw,20px);}
.smgx-imp summary{list-style:none;cursor:pointer;font-size:.95rem;font-weight:700;color:var(--smgx-fg);display:flex;justify-content:space-between;gap:12px;}
.smgx-imp summary::-webkit-details-marker{display:none;}
.smgx-imp summary::marker{content:"";}
.smgx-imp summary:hover{color:var(--smgx-gold);}
.smgx-imp[open] summary{margin-bottom:12px;}
.smgx-sign{flex:0 0 14px;width:14px;height:14px;position:relative;margin-top:5px;}
.smgx-sign::before,.smgx-sign::after{content:"";position:absolute;background:var(--smgx-gold);transition:transform .16s ease,opacity .16s ease;}
.smgx-sign::before{left:0;top:6px;width:14px;height:2px;}
.smgx-sign::after{left:6px;top:0;width:2px;height:14px;}
.smgx-imp[open] .smgx-sign::after{transform:scaleY(0);opacity:0;}
.smgx-impmsg{font-size:.84rem;margin:10px 0 0;color:var(--smgx-fg-dim);}
.smgx-impmsg.bad{color:var(--smgx-req);}
.smgx-impmsg.good{color:var(--smgx-ok);}

.smgx-toast{position:fixed;left:50%;bottom:28px;transform:translateX(-50%) translateY(12px);background:var(--smgx-gold);color:var(--smgx-gold-ink);font-weight:700;font-size:.88rem;padding:11px 20px;border-radius:var(--smgx-r-sm);opacity:0;pointer-events:none;transition:opacity .18s ease,transform .18s ease;z-index:99999;}
.smgx-toast.on{opacity:1;transform:translateX(-50%) translateY(0);}
@media(prefers-reduced-motion:reduce){.smgx-root *,.smgx-toast{transition:none !important;}}
</style>

<div class="smgx-root" id="smgx-app">

  <div class="smgx-top">
    <h2 class="smgx-h">Schema markup generator</h2>
    <p class="smgx-sub">Build valid JSON-LD structured data for 15 schema types, check it against what Google actually asks for, then paste it into your site. Runs entirely in your browser — nothing is uploaded, stored or counted.</p>
  </div>

  <div class="smgx-picker">
    <div class="smgx-pickhead">
      <h3 class="smgx-bh">Pick a schema type</h3>
      <span class="smgx-count">15 types</span>
    </div>
    <div id="smgx-groups"></div>
  </div>

  <div class="smgx-grid">
    <div>
      <div class="smgx-strip" id="smgx-strip"></div>
      <div id="smgx-editor"></div>
    </div>

    <div class="smgx-out">
      <span class="smgx-tab"><i></i><span id="smgx-filename">schema.jsonld</span></span>
      <div class="smgx-paper"><pre class="smgx-pre" id="smgx-code">{}</pre></div>
      <div class="smgx-acts">
        <button type="button" class="smgx-btn" id="smgx-copy">Copy markup</button>
        <button type="button" class="smgx-btn2" id="smgx-dl">Download</button>
        <label class="smgx-check"><input type="checkbox" class="smgx-cb" id="smgx-wrap" checked> Wrap in script tags   </label>
      </div>
      <p style="margin:12px 0 0"><a class="smgx-rrt" href="https://search.google.com/test/rich-results" target="_blank" rel="noopener">Open Google’s Rich Results Test →</a></p>

      <div class="smgx-pv">
        <p class="smgx-pvlab">Result preview</p>
        <div class="smgx-card" id="smgx-preview"></div>
        <p class="smgx-pvnote">An approximation, not a promise. Google decides page by page whether to draw a rich result at all.</p>
      </div>

      <details class="smgx-imp">
        <summary>Import markup you already have <span class="smgx-sign"></span></summary>
        <p class="smgx-bnote" style="margin-top:0">Paste an existing JSON-LD block — or the whole page source, and we’ll dig the <code>ld+json</code> out of it. The form switches to the type it finds and fills in what it can.</p>
        <textarea id="smgx-impbox" rows="5" placeholder='{"@context":"https://schema.org","@type":"Article", ... }'></textarea>
        <div class="smgx-acts"><button type="button" class="smgx-btn2" id="smgx-impgo">Read it in</button></div>
        <p class="smgx-impmsg" id="smgx-impmsg"></p>
      </details>
    </div>
  </div>
</div>

<script>
(function(){
"use strict";
var root=document.getElementById('smgx-app'); if(!root) return;

function f(p,l,tier,t,o){var x={p:p,l:l,tier:tier,t:t||'text'};if(o)for(var k in o)x[k]=o[k];return x;}
var LB=['LocalBusiness','Restaurant','Store','Hotel','Dentist','MedicalClinic','AutoRepair','ProfessionalService','HomeAndConstructionBusiness','LegalService','FinancialService','RealEstateAgent','HealthAndBeautyBusiness'];
var DAYS=['Monday','Tuesday','Wednesday','Thursday','Friday','Saturday','Sunday'];
var ADDR=function(pre,tier){return [
  f(pre+'streetAddress','Street address',tier),
  f(pre+'addressLocality','City / town',tier),
  f(pre+'addressRegion','State / region',tier),
  f(pre+'postalCode','Postal code',tier),
  f(pre+'addressCountry','Country code',tier,'text',{ph:'US'})
];};

var T={};

T.Article={g:'Content',l:'Article',pv:'article',sub:['Article','NewsArticle','BlogPosting','TechArticle'],fs:[
  f('headline','Headline','req','text',{ph:'Why your WordPress site stopped showing up in AI answers',hint:'Under about 110 characters.'}),
  f('image','Image URLs','req','rep',{cols:[{k:'v',l:'Image URL',t:'url',ph:'https://example.com/hero-16x9.jpg'}],hint:'Supply 16:9, 4:3 and 1:1 crops of the same image where you can.'}),
  f('datePublished','Date published','req','date'),
  f('author.name','Author name','rec'),
  f('author.url','Author page URL','rec','url'),
  f('dateModified','Date last modified','rec','date'),
  f('description','Description','rec','area'),
  f('url','Canonical URL of this page','rec','url'),
  f('publisher.name','Publisher name','rec'),
  f('publisher.logo.url','Publisher logo URL','rec','url'),
  f('articleSection','Section or category','opt'),
  f('keywords','Keywords','opt','text',{hint:'Comma separated.'}),
  f('inLanguage','Language tag','opt','text',{ph:'en-US'}),
  f('wordCount','Word count','opt','num')
]};

T.Course={g:'Content',l:'Course',pv:'article',fs:[
  f('name','Course name','req'),
  f('description','Description','req','area'),
  f('provider.name','Provider name','req'),
  f('provider.url','Provider URL','rec','url'),
  f('url','Course page URL','rec','url'),
  f('hasCourseInstance.courseMode','Delivery mode','rec','sel',{options:['','Online','Onsite','Blended']}),
  f('offers.price','Price','rec','num',{ph:'0'}),
  f('offers.priceCurrency','Currency','rec','text',{ph:'USD'}),
  f('educationalLevel','Level','opt','text',{ph:'Beginner'}),
  f('timeRequired','Time required (minutes)','opt','dur'),
  f('inLanguage','Language tag','opt','text',{ph:'en-US'})
]};

T.FAQPage={g:'Content',l:'FAQ page',pv:'faq',notice:'Google stopped drawing FAQ accordions in ordinary search results back in 2023 — only a narrow band of government and health sites still get them. The markup is still worth shipping: <strong>AI Overviews, assistants and voice results read it</strong>, and it makes the page’s structure unambiguous.',fs:[
  f('mainEntity','Questions and answers','req','rep',{cols:[{k:'q',l:'Question',t:'text',ph:'How long does a WordPress migration take?'},{k:'a',l:'Answer',t:'area',ph:'Most sites move in under an hour of downtime...'}],hint:'Every question here must also be visible to a reader on the page itself.'}),
  f('url','Page URL','rec','url'),
  f('inLanguage','Language tag','opt','text',{ph:'en-US'})
]};

T.Recipe={g:'Content',l:'Recipe',pv:'recipe',fs:[
  f('name','Recipe name','req'),
  f('image','Image URLs','req','rep',{cols:[{k:'v',l:'Image URL',t:'url'}]}),
  f('recipeIngredient','Ingredients','rec','rep',{cols:[{k:'v',l:'Ingredient',t:'text',ph:'2 cups plain flour'}]}),
  f('recipeInstructions','Steps','rec','rep',{cols:[{k:'v',l:'Step',t:'area',ph:'Heat the oven to 180C.'}]}),
  f('description','Description','rec','area'),
  f('author.name','Author name','rec'),
  f('datePublished','Date published','rec','date'),
  f('prepTime','Prep time (minutes)','rec','dur'),
  f('cookTime','Cook time (minutes)','rec','dur'),
  f('recipeYield','Yield','rec','text',{ph:'4 servings'}),
  f('nutrition.calories','Calories','rec','text',{ph:'380 calories'}),
  f('recipeCategory','Category','opt','text',{ph:'Dessert'}),
  f('recipeCuisine','Cuisine','opt','text',{ph:'Italian'}),
  f('aggregateRating.ratingValue','Average rating','opt','num'),
  f('aggregateRating.ratingCount','Number of ratings','opt','num'),
  f('keywords','Keywords','opt')
]};

T.VideoObject={g:'Content',l:'Video',pv:'video',fs:[
  f('name','Video title','req'),
  f('description','Description','req','area'),
  f('thumbnailUrl','Thumbnail URLs','req','rep',{cols:[{k:'v',l:'Thumbnail URL',t:'url'}]}),
  f('uploadDate','Upload date','req','date'),
  f('duration','Duration (minutes)','rec','dur'),
  f('contentUrl','Direct file URL','rec','url'),
  f('embedUrl','Embed player URL','rec','url'),
  f('publisher.name','Publisher name','rec'),
  f('publisher.logo.url','Publisher logo URL','rec','url'),
  f('expires','Expiry date','opt','date')
]};

T.Product={g:'Commerce',l:'Product',pv:'product',fs:[
  f('name','Product name','req'),
  f('image','Image URLs','req','rep',{cols:[{k:'v',l:'Image URL',t:'url'}]}),
  f('offers.price','Price','req','num',{ph:'49.00'}),
  f('offers.priceCurrency','Currency','req','text',{ph:'USD'}),
  f('offers.availability','Availability','req','sel',{options:['','InStock','OutOfStock','PreOrder','BackOrder','Discontinued']}),
  f('description','Description','rec','area'),
  f('sku','SKU','rec'),
  f('brand.name','Brand','rec'),
  f('offers.url','Buy URL','rec','url'),
  f('offers.priceValidUntil','Price valid until','rec','date'),
  f('aggregateRating.ratingValue','Average rating','rec','num',{ph:'4.7'}),
  f('aggregateRating.reviewCount','Review count','rec','num',{ph:'128'}),
  f('gtin13','GTIN-13 / EAN','opt'),
  f('mpn','Manufacturer part number','opt'),
  f('offers.itemCondition','Condition','opt','sel',{options:['','NewCondition','UsedCondition','RefurbishedCondition','DamagedCondition']})
]};

T.Review={g:'Commerce',l:'Review',pv:'product',fs:[
  f('itemReviewed.@type','Type of the thing reviewed','req','sel',{options:['Product','Book','Movie','Course','LocalBusiness','SoftwareApplication','Organization','Event']}),
  f('itemReviewed.name','Name of the thing reviewed','req'),
  f('reviewRating.ratingValue','Rating given','req','num',{ph:'4.5'}),
  f('author.name','Reviewer name','req'),
  f('reviewRating.bestRating','Top of the scale','rec','num',{ph:'5'}),
  f('reviewRating.worstRating','Bottom of the scale','rec','num',{ph:'1'}),
  f('reviewBody','Review text','rec','area'),
  f('datePublished','Date published','rec','date'),
  f('publisher.name','Publication','rec'),
  f('url','Review URL','opt','url'),
  f('itemReviewed.image','Image of the thing reviewed','opt','url')
]};

T.SoftwareApplication={g:'Commerce',l:'Software app',pv:'product',fs:[
  f('name','Application name','req'),
  f('offers.price','Price','req','num',{ph:'0',hint:'Use 0 for free software. Google needs either a price or a rating.'}),
  f('offers.priceCurrency','Currency','req','text',{ph:'USD'}),
  f('aggregateRating.ratingValue','Average rating','rec','num'),
  f('aggregateRating.ratingCount','Number of ratings','rec','num'),
  f('operatingSystem','Operating system','rec','text',{ph:'Windows, macOS'}),
  f('applicationCategory','Category','rec','sel',{options:['','BusinessApplication','DeveloperApplication','DesignApplication','GameApplication','SecurityApplication','WebApplication','MobileApplication','UtilitiesApplication']}),
  f('description','Description','rec','area'),
  f('url','Product page URL','rec','url'),
  f('screenshot','Screenshot URL','opt','url'),
  f('softwareVersion','Version','opt'),
  f('fileSize','File size','opt','text',{ph:'24MB'})
]};

T.JobPosting={g:'Business',l:'Job posting',pv:'job',fs:[
  f('title','Job title','req',"text",{ph:'WordPress Systems Engineer',hint:'The role only. No location or company name in this field.'}),
  f('description','Full description','req','area',{hint:'Google wants the complete posting here. Basic HTML is allowed.'}),
  f('datePosted','Date posted','req','date'),
  f('hiringOrganization.name','Company name','req')
].concat(ADDR('jobLocation.address.','req')).concat([
  f('validThrough','Applications close','rec','date'),
  f('employmentType','Employment type','rec','sel',{options:['','FULL_TIME','PART_TIME','CONTRACTOR','TEMPORARY','INTERN','VOLUNTEER','PER_DIEM','OTHER']}),
  f('baseSalary.currency','Salary currency','rec','text',{ph:'USD'}),
  f('baseSalary.value.minValue','Salary from','rec','num'),
  f('baseSalary.value.maxValue','Salary to','rec','num'),
  f('baseSalary.value.unitText','Salary period','rec','sel',{options:['','HOUR','DAY','WEEK','MONTH','YEAR']}),
  f('hiringOrganization.sameAs','Company website','rec','url'),
  f('hiringOrganization.logo','Company logo URL','rec','url'),
  f('jobLocationType','Fully remote?','opt','sel',{options:['','TELECOMMUTE']}),
  f('identifier.value','Internal job ID','opt')
])};

T.LocalBusiness={g:'Business',l:'Local business',pv:'local',sub:LB,fs:[
  f('name','Business name','req'),
  f('image','Image URLs','req','rep',{cols:[{k:'v',l:'Image URL',t:'url'}]}),
  f('telephone','Phone','req','text',{ph:'+1 555 010 2030'})
].concat(ADDR('address.','req')).concat([
  f('url','Website URL','rec','url'),
  f('priceRange','Price range','rec','text',{ph:'$$'}),
  f('openingHoursSpecification','Opening hours','rec','rep',{cols:[{k:'d',l:'Day',t:'sel',options:[''].concat(DAYS)},{k:'o',l:'Opens',t:'time'},{k:'c',l:'Closes',t:'time'}]}),
  f('geo.latitude','Latitude','rec','num'),
  f('geo.longitude','Longitude','rec','num'),
  f('aggregateRating.ratingValue','Average rating','opt','num'),
  f('aggregateRating.reviewCount','Review count','opt','num'),
  f('sameAs','Profile URLs','opt','rep',{cols:[{k:'v',l:'Profile URL',t:'url',ph:'https://www.facebook.com/yourpage'}]}),
  f('menu','Menu URL','opt','url')
])};

T.Organization={g:'Business',l:'Organization',pv:'article',fs:[
  f('name','Organization name','req'),
  f('url','Website URL','req','url'),
  f('logo','Logo URL','rec','url',{hint:'At least 112x112px, and it must be crawlable.'}),
  f('description','Description','rec','area'),
  f('sameAs','Profile URLs','rec','rep',{cols:[{k:'v',l:'Profile URL',t:'url'}],hint:'Social and directory profiles that clearly belong to you.'}),
  f('contactPoint.telephone','Contact phone','rec','text'),
  f('contactPoint.contactType','Contact type','rec','sel',{options:['','customer support','technical support','sales','billing support','emergency']}),
  f('contactPoint.areaServed','Area served','rec','text',{ph:'US'})
].concat(ADDR('address.','rec')).concat([
  f('legalName','Registered legal name','opt'),
  f('foundingDate','Founded','opt','date'),
  f('email','Email','opt')
])};

T.Person={g:'Business',l:'Person',pv:'article',fs:[
  f('name','Full name','req'),
  f('url','Profile or site URL','rec','url'),
  f('image','Photo URL','rec','url'),
  f('jobTitle','Job title','rec'),
  f('worksFor.name','Employer','rec'),
  f('description','Bio','rec','area'),
  f('sameAs','Profile URLs','rec','rep',{cols:[{k:'v',l:'Profile URL',t:'url'}]}),
  f('email','Email','opt'),
  f('telephone','Phone','opt'),
  f('knowsAbout','Areas of expertise','opt','text',{hint:'Comma separated.'}),
  f('alumniOf.name','Alma mater','opt')
]};

T.Event={g:'Other',l:'Event',pv:'event',fs:[
  f('name','Event name','req'),
  f('startDate','Start','req','date'),
  f('location.name','Venue name','req'),
  f('location.address.streetAddress','Street address','req'),
  f('location.address.addressLocality','City / town','req'),
  f('location.address.addressRegion','State / region','rec'),
  f('location.address.postalCode','Postal code','rec'),
  f('location.address.addressCountry','Country code','rec','text',{ph:'US'}),
  f('endDate','End','rec','date'),
  f('image','Image URLs','rec','rep',{cols:[{k:'v',l:'Image URL',t:'url'}]}),
  f('description','Description','rec','area'),
  f('eventAttendanceMode','Attendance mode','rec','sel',{options:['','OfflineEventAttendanceMode','OnlineEventAttendanceMode','MixedEventAttendanceMode']}),
  f('eventStatus','Status','rec','sel',{options:['','EventScheduled','EventPostponed','EventRescheduled','EventCancelled','EventMovedOnline']}),
  f('offers.price','Ticket price','rec','num'),
  f('offers.priceCurrency','Currency','rec','text',{ph:'USD'}),
  f('offers.url','Ticket URL','rec','url'),
  f('offers.availability','Ticket availability','rec','sel',{options:['','InStock','SoldOut','PreOrder']}),
  f('performer.name','Performer','opt'),
  f('organizer.name','Organizer','opt'),
  f('url','Event page URL','opt','url')
]};

T.BreadcrumbList={g:'Other',l:'Breadcrumbs',pv:'crumb',fs:[
  f('itemListElement','Trail','req','rep',{cols:[{k:'n',l:'Label',t:'text',ph:'WordPress Care Plans'},{k:'u',l:'URL',t:'url',ph:'https://presscare.com/care-plans/'}],hint:'List them left to right, the way a reader sees them. Leave the last URL blank if it points at the current page.'})
]};

T.WebSite={g:'Other',l:'Website',pv:'article',fs:[
  f('name','Site name','req'),
  f('url','Home page URL','req','url'),
  f('alternateName','Alternate name','rec'),
  f('potentialAction.target','Site search URL pattern','rec','url',{ph:'https://presscare.com/?s={search_term_string}',hint:'Include the literal {search_term_string} placeholder where the query goes.'}),
  f('description','Description','opt','area'),
  f('publisher.name','Publisher','opt'),
  f('inLanguage','Language tag','opt','text',{ph:'en-US'})
]};

var ORDER=['Article','Course','FAQPage','Recipe','VideoObject','Product','Review','SoftwareApplication','JobPosting','LocalBusiness','Organization','Person','Event','BreadcrumbList','WebSite'];
var GROUPS=['Content','Commerce','Business','Other'];
var NEST={'author':'Person','publisher':'Organization','publisher.logo':'ImageObject','address':'PostalAddress','offers':'Offer','aggregateRating':'AggregateRating','geo':'GeoCoordinates','brand':'Brand','reviewRating':'Rating','provider':'Organization','contactPoint':'ContactPoint','hiringOrganization':'Organization','jobLocation':'Place','jobLocation.address':'PostalAddress','baseSalary':'MonetaryAmount','baseSalary.value':'QuantitativeValue','location':'Place','location.address':'PostalAddress','performer':'Person','organizer':'Organization','worksFor':'Organization','alumniOf':'EducationalOrganization','nutrition':'NutritionInformation','identifier':'PropertyValue','hasCourseInstance':'CourseInstance'};
var ENUM={'offers.availability':'https://schema.org/','offers.itemCondition':'https://schema.org/','eventAttendanceMode':'https://schema.org/','eventStatus':'https://schema.org/'};

var state={type:'Article',sub:'Article',v:{},rep:{}};

/* ---------- helpers ---------- */
function esc(s){var A=String.fromCharCode(38);return String(s).replace(/[&<>"']/g,function(c){return A+'#'+c.charCodeAt(0)+';';});}        
function id(p){return 'smgx-i-'+p.replace(/[^a-z0-9]/gi,'_');}
function setPath(o,path,val){var ks=path.split('.'),c=o;for(var i=0;i<ks.length-1;i++){if(typeof c[ks[i]]!=='object'||c[ks[i]]===null)c[ks[i]]={};c=c[ks[i]];}c[ks[ks.length-1]]=val;}
function getPath(o,path){var ks=path.split('.'),c=o;for(var i=0;i<ks.length;i++){if(c===null||typeof c!=='object')return undefined;c=c[ks[i]];}return c;}
function dur(mins){var m=parseInt(mins,10);if(!m||m<=0)return '';var h=Math.floor(m/60),r=m%60;return 'PT'+(h?h+'H':'')+(r?r+'M':(h?'':'0M'));}
function undur(s){var m=/^PT(?:(\d+)H)?(?:(\d+)M)?/.exec(s||'');if(!m)return '';return String((parseInt(m[1]||0,10)*60)+parseInt(m[2]||0,10));}
function toast(msg){var t=document.getElementById('smgx-toast');t.textContent=msg;t.classList.add('on');setTimeout(function(){t.classList.remove('on');},1700);}

function def(){return T[state.type];}
function filled(fd){
  if(fd.t==='rep'){var rows=state.rep[fd.p]||[];for(var i=0;i<rows.length;i++){for(var k in rows[i]){if(String(rows[i][k]||'').trim())return true;}}return false;}
  return String(state.v[fd.p]||'').trim()!=='';
}

/* ---------- type picker ---------- */
function drawGroups(){
  var host=document.getElementById('smgx-groups'),html='';
  GROUPS.forEach(function(g){
    html+='<div class="smgx-grp"><p class="smgx-grplab">'+g+'</p><div class="smgx-chips">';
    ORDER.forEach(function(k){ if(T[k].g!==g) return;
      html+='<button type="button" class="smgx-chip" data-t="'+k+'" aria-pressed="'+(k===state.type)+'">'+esc(T[k].l)+'</button>';
    });
    html+='</div></div>';
  });
  host.innerHTML=html;
}

(function bindGroupsOnce(){
  document.getElementById('smgx-groups').addEventListener('click',function(e){
    var b=e.target.closest?e.target.closest('.smgx-chip'):null; if(!b) return;
    state.type=b.getAttribute('data-t');
    state.sub=(T[state.type].sub||[state.type])[0];
    state.v={};state.rep={};
    drawGroups();drawEditor();render();
  });
})();

/* ---------- editor ---------- */
function fieldHTML(fd){
  var v=state.v[fd.p]||'', lit=filled(fd)?' smgx-lit':'';
  var lab='<label class="smgx-lab" for="'+id(fd.p)+'">'+esc(fd.l)+(fd.hint?'<span class="smgx-hint">'+fd.hint+'</span>':'')+'</label>';
  var inp;
  if(fd.t==='area') inp='<textarea id="'+id(fd.p)+'" data-p="'+fd.p+'" placeholder="'+esc(fd.ph||'')+'">'+esc(v)+'</textarea>';
  else if(fd.t==='sel'){
    inp='<select id="'+id(fd.p)+'" data-p="'+fd.p+'">';
    fd.options.forEach(function(o){inp+='<option value="'+esc(o)+'"'+(o===v?' selected':'')+'>'+(o===''?'—':esc(o))+'</option>';});
    inp+='</select>';
  }
  else if(fd.t==='rep') return repHTML(fd);
  else{
    var ty=fd.t==='num'||fd.t==='dur'?'number':(fd.t==='url'?'url':(fd.t==='date'?'date':(fd.t==='time'?'time':'text')));
    inp='<input type="'+ty+'" id="'+id(fd.p)+'" data-p="'+fd.p+'" value="'+esc(v)+'" placeholder="'+esc(fd.ph||'')+'">';
  }
  return '<div class="smgx-field'+lit+'" data-tier="'+fd.tier+'" data-for="'+fd.p+'">'+lab+inp+'</div>';
}

function repHTML(fd){
  var rows=state.rep[fd.p]||[{}], cols=fd.cols;
  var w=cols.map(function(){return 'minmax(0,1fr)';}).join(' ');
  var h='<div class="smgx-field'+(filled(fd)?' smgx-lit':'')+'" data-tier="'+fd.tier+'" data-for="'+fd.p+'"><label class="smgx-lab" id="'+id(fd.p)+'" tabindex="-1">'+esc(fd.l)+(fd.hint?'<span class="smgx-hint">'+fd.hint+'</span>':'')+'</label><div class="smgx-rep" data-rep="'+fd.p+'" style="--smgx-cols:'+w+'">';
  rows.forEach(function(r,i){
    h+='<div class="smgx-row">';
    cols.forEach(function(c){
      var val=r[c.k]||'';
      h+='<div><label class="smgx-lab">'+esc(c.l)+'</label>';
      if(c.t==='area') h+='<textarea rows="2" data-rp="'+fd.p+'" data-ri="'+i+'" data-rk="'+c.k+'" placeholder="'+esc(c.ph||'')+'">'+esc(val)+'</textarea>';
      else if(c.t==='sel'){h+='<select data-rp="'+fd.p+'" data-ri="'+i+'" data-rk="'+c.k+'">';c.options.forEach(function(o){h+='<option value="'+esc(o)+'"'+(o===val?' selected':'')+'>'+(o===''?'—':esc(o))+'</option>';});h+='</select>';}
      else h+='<input type="'+(c.t==='url'?'url':(c.t==='time'?'time':'text'))+'" data-rp="'+fd.p+'" data-ri="'+i+'" data-rk="'+c.k+'" value="'+esc(val)+'" placeholder="'+esc(c.ph||'')+'">';
      h+='</div>';
    });
    h+='<button type="button" class="smgx-x" data-del="'+fd.p+'" data-ri="'+i+'" aria-label="Remove row">×</button></div>';
  });
  h+='</div><button type="button" class="smgx-add" data-add="'+fd.p+'">+ Add another</button></div>';
  return h;
}

function drawEditor(){
  var d=def(),host=document.getElementById('smgx-editor'),h='';
  d.fs.forEach(function(fd){
    if(fd.t==='sel'&&fd.options&&fd.options[0]!==''&&state.v[fd.p]===undefined) state.v[fd.p]=fd.options[0];
  });
  h+='<div class="smgx-block">';
  h+='<div class="smgx-tierhead"><h3 class="smgx-bh">'+esc(d.l)+'</h3></div>';
  if(d.sub){
    h+='<p class="smgx-bnote">Narrow the type and the markup gets more specific. Everything else on this form stays the same.</p>';
    h+='<div class="smgx-field"><label class="smgx-lab" for="smgx-sub">Sub-type</label><select id="smgx-sub">';
    d.sub.forEach(function(s){h+='<option value="'+s+'"'+(s===state.sub?' selected':'')+'>'+s+'</option>';});
    h+='</select></div>';
  }
  if(d.notice) h+='<p class="smgx-note">'+d.notice+'</p>';
  h+='</div>';

  ['req','rec','opt'].forEach(function(tier){
    var list=d.fs.filter(function(x){return x.tier===tier;});
    if(!list.length) return;
    var titles={req:['Required','Leave any of these out and the rich result is not eligible at all.'],
                rec:['Recommended','Not mandatory, but they are what turns a plain blue link into something with a picture, a price or a rating.'],
                opt:['Optional','Only worth adding when you genuinely have the data and it makes the listing more useful.']};
    h+='<div class="smgx-block"><div class="smgx-tierhead"><span class="smgx-pill '+tier+'">'+titles[tier][0]+'</span></div><p class="smgx-bnote">'+titles[tier][1]+'</p>';
    list.forEach(function(fd){h+=fieldHTML(fd);});
    h+='</div>';
  });
  host.innerHTML=h;

  var sub=document.getElementById('smgx-sub');
  if(sub) sub.addEventListener('change',function(){state.sub=this.value;render();});
}

(function bindEditorOnce(){
  var host=document.getElementById('smgx-editor');
  host.addEventListener('input',onEdit);
  host.addEventListener('change',onEdit);
  host.addEventListener('click',function(e){
    var a=e.target.closest?e.target.closest('[data-add]'):null,x=e.target.closest?e.target.closest('[data-del]'):null;
    if(a){var p=a.getAttribute('data-add');state.rep[p]=(state.rep[p]||[{}]).concat([{}]);redrawRep(p);render();}
    if(x){var p2=x.getAttribute('data-del'),i=+x.getAttribute('data-ri');var r=state.rep[p2]||[{}];r.splice(i,1);if(!r.length)r=[{}];state.rep[p2]=r;redrawRep(p2);render();}
  });
})();

function redrawRep(p){
  var fd=def().fs.filter(function(x){return x.p===p;})[0];
  var wrap=document.querySelector('#smgx-editor [data-for="'+p+'"]');
  if(!fd||!wrap) return;
  var tmp=document.createElement('div');tmp.innerHTML=repHTML(fd);
  wrap.parentNode.replaceChild(tmp.firstChild,wrap);
}

function onEdit(e){
  var t=e.target;
  if(t.hasAttribute('data-p')){
    state.v[t.getAttribute('data-p')]=t.value;
    var w=t.closest('.smgx-field'); if(w) w.classList.toggle('smgx-lit',String(t.value||'').trim()!=='');
  } else if(t.hasAttribute('data-rp')){
    var p=t.getAttribute('data-rp'),i=+t.getAttribute('data-ri'),k=t.getAttribute('data-rk');
    var rows=state.rep[p]||[{}]; while(rows.length<=i)rows.push({});
    rows[i][k]=t.value; state.rep[p]=rows;
  } else return;
  render();
}

/* ---------- build ---------- */
function build(){
  var d=def(),o={'@context':'https://schema.org','@type':state.sub||state.type};
  d.fs.forEach(function(fd){
    if(fd.t==='rep') return;
    var val=String(state.v[fd.p]||'').trim(); if(!val) return;
    if(fd.t==='dur') val=dur(val);
    if(fd.t==='num'&&!isNaN(Number(val))) val=Number(val);
    if(ENUM[fd.p]) val=ENUM[fd.p]+val;
    if(fd.p==='keywords'||fd.p==='knowsAbout'){var a=String(val).split(',').map(function(s){return s.trim();}).filter(Boolean);val=a.length>1?a:a[0];}
    setPath(o,fd.p,val);
  });
  d.fs.forEach(function(fd){
    if(fd.t!=='rep') return;
    var rows=(state.rep[fd.p]||[]).filter(function(r){for(var k in r){if(String(r[k]||'').trim())return true;}return false;});
    if(!rows.length) return;
    if(fd.p==='mainEntity') setPath(o,fd.p,rows.map(function(r){return {'@type':'Question',name:r.q||'',acceptedAnswer:{'@type':'Answer',text:r.a||''}};}));
    else if(fd.p==='itemListElement') setPath(o,fd.p,rows.map(function(r,i){var it={'@type':'ListItem',position:i+1,name:r.n||''};if(String(r.u||'').trim())it.item=r.u.trim();return it;}));
    else if(fd.p==='recipeInstructions') setPath(o,fd.p,rows.map(function(r){return {'@type':'HowToStep',text:r.v||''};}));
    else if(fd.p==='openingHoursSpecification') setPath(o,fd.p,rows.map(function(r){return {'@type':'OpeningHoursSpecification',dayOfWeek:r.d||'',opens:r.o||'',closes:r.c||''};}));
    else {var vals=rows.map(function(r){return r.v;}).filter(Boolean);setPath(o,fd.p,vals.length>1?vals:vals[0]);}
  });
  // stamp nested @type
  Object.keys(NEST).sort().forEach(function(path){
    var n=getPath(o,path);
    if(n&&typeof n==='object'&&!Array.isArray(n)&&!n['@type']){
      var re={};re['@type']=NEST[path];for(var k in n)re[k]=n[k];setPath(o,path,re);
    }
  });
  // per-type finishing
  if(o.mainEntityOfPage===undefined&&o.url&&(state.type==='Article')) o.mainEntityOfPage={'@type':'WebPage','@id':o.url};
  if(state.type==='WebSite'&&getPath(o,'potentialAction.target')){
    o.potentialAction={'@type':'SearchAction',target:{'@type':'EntryPoint',urlTemplate:getPath(o,'potentialAction.target')},'query-input':'required name=search_term_string'};
  }
  if(state.type==='Course'&&getPath(o,'hasCourseInstance.courseMode')){
    o.hasCourseInstance={'@type':'CourseInstance',courseMode:getPath(o,'hasCourseInstance.courseMode')};
  }
  if(state.type==='JobPosting'&&o.jobLocationType) o.applicantLocationRequirements={'@type':'Country',name:'Worldwide'};
  if(state.type==='Review'&&getPath(o,'itemReviewed.@type')===undefined&&o.itemReviewed) setPath(o,'itemReviewed.@type','Thing');
  return o;
}

/* ---------- preview ---------- */
function stars(n){n=Math.max(0,Math.min(5,Math.round(Number(n)||0)));return '<span class="st">'+'\u2605'.repeat(n)+'\u2606'.repeat(5-n)+'</span>';}
function host(u){try{return new URL(u).hostname.replace(/^www\./,'')+new URL(u).pathname.replace(/\/$/,'');}catch(e){return 'presscare.com \u203a page';}}
function previewHTML(o){
  var v=state.v, kind=def().pv;
  var img=(state.rep['image']||[])[0], imgu=(img&&img.v)||v['image']||v['thumbnailUrl']||((state.rep['thumbnailUrl']||[])[0]||{}).v||'';
  var t=v['headline']||v['name']||v['title']||v['itemReviewed.name']||'Your page title appears here';
  var desc=v['description']||v['reviewBody']||'The description you enter shows up here, trimmed to roughly the length Google allows.';
  var u=v['url']||v['offers.url']||'https://presscare.com/your-page/';
  if(kind==='faq'){
    var rows=(state.rep['mainEntity']||[]).filter(function(r){return r.q;});
    if(!rows.length) rows=[{q:'Your first question appears here'},{q:'And your second'}];
    return '<span class="u">'+esc(host(u))+'</span><span class="t">'+esc(t)+'</span>'+rows.slice(0,4).map(function(r){return '<div class="qa"><span>'+esc(r.q)+'</span><span>\u02c5</span></div>';}).join('');
  }
  if(kind==='crumb'){
    var c=(state.rep['itemListElement']||[]).filter(function(r){return r.n;});
    if(!c.length) c=[{n:'Home'},{n:'Section'},{n:'This page'}];
    return '<span class="crumbs">'+c.map(function(r){return esc(r.n);}).join(' › ')+'</span><span class="t" style="margin-top:5px">'+esc(t)+'</span><span class="d">'+esc(desc)+'</span>';
  }
  if(kind==='product'){
    var price=v['offers.price'],cur=v['offers.priceCurrency']||'',rv=v['aggregateRating.ratingValue']||v['reviewRating.ratingValue'];
    var avail=(v['offers.availability']||'').replace(/([A-Z])/g,' $1').trim();
    return '<span class="u">'+esc(host(u))+'</span><span class="t">'+esc(t)+'</span>'+
      (rv?'<span class="d">'+stars(rv)+' '+esc(rv)+(v['aggregateRating.reviewCount']?' ('+esc(v['aggregateRating.reviewCount'])+')':'')+'</span>':'')+
      (price?'<span class="d"><b>'+esc(cur)+' '+esc(price)+'</b>'+(avail?' · '+esc(avail):'')+'</span>':'')+
      '<span class="d">'+esc(desc)+'</span>';
  }
  if(kind==='recipe'){
    return (imgu?'<span class="thumb" style="background-image:url('+esc(imgu)+')"></span>':'<span class="thumb"></span>')+
      '<span class="t">'+esc(t)+'</span>'+
      (v['aggregateRating.ratingValue']?'<span class="d">'+stars(v['aggregateRating.ratingValue'])+'</span>':'')+
      '<span class="mt">'+[v['prepTime']?v['prepTime']+' min prep':'',v['cookTime']?v['cookTime']+' min cook':'',v['recipeYield']||''].filter(Boolean).join(' \u00b7 ')+'</span>';
  }
  if(kind==='video'){
    return (imgu?'<span class="thumb" style="background-image:url('+esc(imgu)+')"></span>':'<span class="thumb"></span>')+
      '<span class="t">'+esc(t)+'</span><span class="d">'+esc(desc)+'</span>'+
      '<span class="mt">'+[v['duration']?v['duration']+' min':'',v['uploadDate']||''].filter(Boolean).join(' \u00b7 ')+'</span>';
  }
  if(kind==='event'){
    var ds=v['startDate']?new Date(v['startDate']+'T00:00:00'):null;
    var box=ds?'<span class="datebox"><b>'+ds.getDate()+'</b><span>'+ds.toLocaleString(undefined,{month:'short'})+'</span></span>':'';
    return box+'<span style="display:inline-block;max-width:calc(100% - 70px)"><span class="t">'+esc(t)+'</span><span class="d">'+esc(v['location.name']||'Venue')+(v['location.address.addressLocality']?', '+esc(v['location.address.addressLocality']):'')+'</span>'+
      (v['offers.price']?'<span class="mt">From '+esc(v['offers.priceCurrency']||'')+' '+esc(v['offers.price'])+'</span>':'')+'</span>';
  }
  if(kind==='job'){
    var pills=[v['hiringOrganization.name'],v['jobLocation.address.addressLocality'],(v['employmentType']||'').replace('_',' '),
      (v['baseSalary.value.minValue']?(v['baseSalary.currency']||'')+' '+v['baseSalary.value.minValue']+(v['baseSalary.value.maxValue']?'\u2013'+v['baseSalary.value.maxValue']:'')+(v['baseSalary.value.unitText']?'/'+v['baseSalary.value.unitText'].toLowerCase():''):'')].filter(Boolean);
    return '<span class="t">'+esc(t)+'</span><span style="display:block;margin:6px 0">'+pills.map(function(p){return '<span class="pill">'+esc(p)+'</span>';}).join('')+'</span><span class="d">'+esc(String(desc).slice(0,150))+'</span>';
  }
  if(kind==='local'){
    return '<span class="t">'+esc(t)+'</span>'+
      (v['aggregateRating.ratingValue']?'<span class="d">'+stars(v['aggregateRating.ratingValue'])+' '+esc(v['aggregateRating.ratingValue'])+'</span>':'')+
      '<span class="d">'+[v['priceRange'],v['address.streetAddress'],v['address.addressLocality']].filter(Boolean).map(esc).join(' \u00b7 ')+'</span>'+
      (v['telephone']?'<span class="mt">'+esc(v['telephone'])+'</span>':'');
  }
  return '<span class="u">'+esc(host(u))+'</span><span class="t">'+esc(t)+'</span><span class="d">'+esc(desc)+'</span>'+
    (v['datePublished']?'<span class="mt">'+esc(v['datePublished'])+(v['author.name']?' \u2014 '+esc(v['author.name']):'')+'</span>':'');
}

/* ---------- readiness ---------- */
function drawStrip(){
  var d=def(),req=d.fs.filter(function(x){return x.tier==='req';}),rec=d.fs.filter(function(x){return x.tier==='rec';});
  var rq=req.filter(filled).length, rc=rec.filter(filled).length;
  var missing=req.filter(function(x){return !filled(x);});
  var thin=rec.filter(function(x){return !filled(x);});
  var h='<div class="smgx-stripbar">'+
    '<span class="smgx-tally"><i class="smgx-dot '+(missing.length?'req':'ok')+'"></i>Required '+rq+'/'+req.length+'</span>'+
    '<span class="smgx-tally"><i class="smgx-dot '+(thin.length?'rec':'ok')+'"></i>Recommended '+rc+'/'+rec.length+'</span></div>';
  if(missing.length){
    h+='<ul class="smgx-issues">'+missing.map(function(x){return '<li><button type="button" class="smgx-issue" data-go="'+x.p+'">Missing required: '+esc(x.l)+'</button></li>';}).join('')+'</ul>';
  } else if(thin.length){
    h+='<p class="smgx-clear">Every required field is in. Filling '+thin.length+' more recommended field'+(thin.length===1?'':'s')+' gives the listing more to draw with.</p>';
    h+='<ul class="smgx-issues">'+thin.slice(0,5).map(function(x){return '<li><button type="button" class="smgx-issue" data-go="'+x.p+'">Empty recommended: '+esc(x.l)+'</button></li>';}).join('')+'</ul>';
  } else {
    h+='<p class="smgx-clear">Required and recommended fields are all complete.</p>';
  }
  var s=document.getElementById('smgx-strip');
  s.innerHTML=h;
  s.onclick=function(e){
    var b=e.target.closest('[data-go]'); if(!b) return;
    var el=document.getElementById(id(b.getAttribute('data-go')));
    if(el){el.scrollIntoView({block:'center',behavior:'smooth'});setTimeout(function(){try{el.focus();}catch(err){}},320);}
  };
}

/* ---------- render ---------- */
function render(){
  var o=build();
  var json=JSON.stringify(o,null,2);
  var wrap=document.getElementById('smgx-wrap').checked;
  document.getElementById('smgx-code').textContent=wrap?'<script type="application/ld+json">\n'+json+'\n<\/script>':json;
  document.getElementById('smgx-filename').textContent=(state.sub||state.type).toLowerCase()+(wrap?'.html':'.jsonld');
  document.getElementById('smgx-preview').innerHTML=previewHTML(o);
  drawStrip();
}

/* ---------- import ---------- */
function absorb(obj){
  var ty=obj['@type']; if(Array.isArray(ty)) ty=ty[0];
  var target=null;
  ORDER.forEach(function(k){ if(k===ty) target=k; });
  if(!target) ORDER.forEach(function(k){ if((T[k].sub||[]).indexOf(ty)>-1) target=k; });
  if(!target) return false;
  state.type=target; state.sub=ty; state.v={}; state.rep={};
  T[target].fs.forEach(function(fd){
    if(fd.t==='rep'){
      var raw=getPath(obj,fd.p); if(!raw) return;
      if(!Array.isArray(raw)) raw=[raw];
      if(fd.p==='mainEntity') state.rep[fd.p]=raw.map(function(q){return {q:q.name||'',a:(q.acceptedAnswer&&q.acceptedAnswer.text)||''};});
      else if(fd.p==='itemListElement') state.rep[fd.p]=raw.map(function(li){return {n:li.name||(li.item&&li.item.name)||'',u:(typeof li.item==='string'?li.item:(li.item&&li.item['@id'])||'')};});
      else if(fd.p==='openingHoursSpecification') state.rep[fd.p]=raw.map(function(s){return {d:[].concat(s.dayOfWeek||'')[0],o:s.opens||'',c:s.closes||''};});
      else state.rep[fd.p]=raw.map(function(x){return {v:(typeof x==='string'?x:(x.text||x.url||x.name||''))};});
      return;
    }
    var val=getPath(obj,fd.p);
    if(val===undefined||val===null) return;
    if(Array.isArray(val)&&val.every(function(z){return typeof z==='string';})) val=val.join(', ');
    else if(typeof val==='object') return;
    val=String(val);
    if(fd.t==='dur') val=undur(val);
    if(ENUM[fd.p]) val=val.replace(/^https?:\/\/schema\.org\//,'');
    if(fd.t==='date') val=val.slice(0,10);
    state.v[fd.p]=val;
  });
  if(target==='WebSite'){var tt=getPath(obj,'potentialAction.target');if(tt) state.v['potentialAction.target']=(typeof tt==='string'?tt:tt.urlTemplate||'');}
  return true;
}
document.getElementById('smgx-impgo').addEventListener('click',function(){
  var box=document.getElementById('smgx-impbox'),msg=document.getElementById('smgx-impmsg');
  var raw=box.value.trim(); msg.className='smgx-impmsg';
  if(!raw){msg.textContent='Paste something first.';msg.classList.add('bad');return;}
  var blocks=[];
  var re=/<script[^>]*application\/ld\+json[^>]*>([\s\S]*?)<\/script>/gi,m;
  while((m=re.exec(raw))) blocks.push(m[1]);
  if(!blocks.length) blocks=[raw];
  var got=false,count=0;
  for(var i=0;i<blocks.length&&!got;i++){
    var parsed; try{parsed=JSON.parse(blocks[i].trim());}catch(e){continue;}
    count++;
    var cands=Array.isArray(parsed)?parsed:(parsed['@graph']&&Array.isArray(parsed['@graph'])?parsed['@graph']:[parsed]);
    for(var j=0;j<cands.length;j++){ if(absorb(cands[j])){got=true;break;} }
  }
  if(got){drawGroups();drawEditor();render();msg.textContent='Loaded as '+(state.sub||state.type)+'. Check every field before you ship it.';msg.classList.add('good');}
  else{msg.textContent=count?'Found JSON-LD, but not a type this tool builds.':'That did not parse as JSON-LD.';msg.classList.add('bad');}
});

/* ---------- actions ---------- */
document.getElementById('smgx-wrap').addEventListener('change',render);
document.getElementById('smgx-copy').addEventListener('click',function(){
  var txt=document.getElementById('smgx-code').textContent;
  if(navigator.clipboard&&navigator.clipboard.writeText) navigator.clipboard.writeText(txt).then(function(){toast('Markup copied');},fallback);
  else fallback();
  function fallback(){var ta=document.createElement('textarea');ta.value=txt;ta.style.position='fixed';ta.style.opacity='0';document.body.appendChild(ta);ta.select();try{document.execCommand('copy');toast('Markup copied');}catch(e){toast('Copy failed \u2014 select it by hand');}document.body.removeChild(ta);}
});
document.getElementById('smgx-dl').addEventListener('click',function(){
  var txt=document.getElementById('smgx-code').textContent;
  var name=document.getElementById('smgx-filename').textContent;
  var b=new Blob([txt],{type:'text/plain;charset=utf-8'}),a=document.createElement('a');
  a.href=URL.createObjectURL(b);a.download=name;document.body.appendChild(a);a.click();
  setTimeout(function(){URL.revokeObjectURL(a.href);document.body.removeChild(a);},400);
});

drawGroups();drawEditor();render();
})();
</script>