Schema Markup Generator FAQ + HowTo

πŸ”’ Runs locally in your browser
Inputs
Optional, but recommended. Used for @id and (optionally) WebSite schema.
Recommended. For FAQPage, this maps to name. For HowTo, it’s the HowTo name.
Chars: 0 Aim ≀ 160
FAQ Q&A
Add at least 1 Q&A. Reorder with ↑/↓. Answers should be plain text (no HTML).
Useful for site-wide schema; keep it consistent across your pages. If enabled, it will be emitted alongside your FAQ/HowTo in a single @graph.
Choose FAQPage or HowTo, add content, then generate.
Output
Paste the result into your page’s <head> or near the end of <body>. For FAQPage, make sure the visible on-page content matches the Q&A.
Preview
Type: FAQPage 0 items 0 errors
(Nothing generated yet)
This is a human-readable preview of the JSON (not a live Google preview). Validate in Google’s Rich Results Test after publishing.

Notes

\n`; } function rebuild(showGenerationStatus = true) { // Toggle org/site fields els.orgSiteFields.style.display = els.optOrgSite.checked ? '' : 'none'; // Tab UI els.tabFAQ.classList.toggle('active', state.mode === 'faq'); els.tabHowTo.classList.toggle('active', state.mode === 'howto'); els.tabFAQ.setAttribute('aria-selected', state.mode === 'faq' ? 'true' : 'false'); els.tabHowTo.setAttribute('aria-selected', state.mode === 'howto' ? 'true' : 'false'); // Sections els.faqSection.style.display = state.mode === 'faq' ? '' : 'none'; els.howtoSection.style.display = state.mode === 'howto' ? '' : 'none'; // Rename/renumber renumberList(els.qaList, 'FAQ'); renumberList(els.stepList, 'Step'); updateCounters(); const { schema, errors } = buildSchemaObject(); const out = stringifySchema(schema); els.output.value = out; // Preview els.pillType.textContent = `Type: ${state.mode === 'faq' ? 'FAQPage' : 'HowTo'}`; let count = 0; if (state.mode === 'faq') { const me = schema['@graph'] ? (schema['@graph'].find(x => x['@type'] === 'FAQPage')?.mainEntity || []) : (schema.mainEntity || []); count = me.length; } else { const st = schema['@graph'] ? (schema['@graph'].find(x => x['@type'] === 'HowTo')?.step || []) : (schema.step || []); count = st.length; } els.pillCount.textContent = `${count} ${state.mode === 'faq' ? 'Q&A' : 'steps'}`; els.pillErrors.textContent = `${errors.length} errors`; els.previewBox.textContent = JSON.stringify(schema, null, 2); if (showGenerationStatus) { if (errors.length) { setStatus('err', `❌ Fix ${errors.length} issue(s) before using the markup:
β€’ ${errors.map(escapeHtml).join('
β€’ ')}
`); } else { const extra = els.optOrgSite.checked ? ' (includes Organization + WebSite)' : ''; setStatus('ok', `βœ… Generated ${state.mode === 'faq' ? 'FAQPage' : 'HowTo'} JSON-LD${extra}.`); } } return { schema, errors, out }; } async function copyText(text) { const t = (text || '').trimEnd(); if (!t) return false; try { await navigator.clipboard.writeText(t + '\n'); return true; } catch { const ta = document.createElement('textarea'); ta.value = t + '\n'; ta.style.position = 'fixed'; ta.style.left = '-9999px'; document.body.appendChild(ta); ta.focus(); ta.select(); try { document.execCommand('copy'); return true; } catch { return false; } finally { ta.remove(); } } } function downloadText(filename, text, mime = 'text/plain;charset=utf-8') { const blob = new Blob([text], { type: mime }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); a.remove(); setTimeout(() => URL.revokeObjectURL(url), 800); } function setMode(mode) { state.mode = mode; rebuild(false); } function setExample() { els.pageUrl.value = 'https://example.com/help/shipping-faq/'; els.pageName.value = 'Shipping FAQ'; els.itemDescription.value = 'Answers to common shipping questions, delivery times, tracking, and returns.'; els.optWrapScript.checked = true; els.optPretty.checked = true; // Org/Site example disabled by default (but fields prefill for convenience) els.optOrgSite.checked = false; els.orgName.value = 'Example Co'; els.orgUrl.value = 'https://example.com/'; els.orgLogo.value = 'https://example.com/logo.png'; els.orgSameAs.value = 'https://twitter.com/example, https://www.linkedin.com/company/example/'; els.siteName.value = 'Example Co'; els.siteUrl.value = 'https://example.com/'; if (state.mode === 'faq') { els.qaList.innerHTML = ''; els.qaList.appendChild(createQAItem({ q: 'How long does shipping take?', a: 'Standard shipping typically takes 3–5 business days. Expedited options may be available at checkout.' })); els.qaList.appendChild(createQAItem({ q: 'Do you provide tracking?', a: 'Yes. Once your order ships, we email a tracking link. You can also find tracking in your account order history.' })); els.qaList.appendChild(createQAItem({ q: 'What is your return policy?', a: 'We accept returns within 30 days of delivery as long as items are unused and in original packaging.' })); } else { els.pageUrl.value = 'https://example.com/help/replace-watch-battery/'; els.pageName.value = 'How to Replace a Watch Battery'; els.itemDescription.value = 'A step-by-step guide to replacing a standard coin-cell watch battery.'; els.howtoTotalTime.value = 'PT20M'; els.howtoSupply.value = 'Replacement battery, Small container'; els.howtoTool.value = 'Small screwdriver, Tweezers'; els.stepList.innerHTML = ''; els.stepList.appendChild(createStepItem({ title: 'Open the case back', text: 'Use a small screwdriver to carefully open the watch case back. Work slowly to avoid scratches.' })); els.stepList.appendChild(createStepItem({ title: 'Remove the old battery', text: 'Note the battery orientation, then remove the old battery using tweezers.' })); els.stepList.appendChild(createStepItem({ title: 'Install the new battery', text: 'Insert the new battery in the same orientation and ensure it is seated correctly.' })); els.stepList.appendChild(createStepItem({ title: 'Close the case', text: 'Reattach the case back and check that the watch runs normally.' })); } // Autofill siteUrl from pageUrl if possible if (!els.siteUrl.value) { const origin = toOriginMaybe(els.pageUrl.value); if (origin) els.siteUrl.value = origin; } rebuild(true); } function resetAll() { els.pageUrl.value = ''; els.pageName.value = ''; els.itemDescription.value = ''; els.howtoTotalTime.value = ''; els.howtoSupply.value = ''; els.howtoTool.value = ''; els.optWrapScript.checked = true; els.optPretty.checked = true; els.optOrgSite.checked = false; els.orgName.value = ''; els.orgUrl.value = ''; els.orgLogo.value = ''; els.orgSameAs.value = ''; els.siteName.value = ''; els.siteUrl.value = ''; els.qaList.innerHTML = ''; els.stepList.innerHTML = ''; els.qaList.appendChild(createQAItem()); els.qaList.appendChild(createQAItem()); els.stepList.appendChild(createStepItem()); els.stepList.appendChild(createStepItem()); els.output.value = ''; els.previewBox.textContent = '(Nothing generated yet)'; setStatus('', 'Choose FAQPage or HowTo, add content, then generate.'); rebuild(false); } // Events function bindTab(tabEl, mode) { tabEl.addEventListener('click', () => setMode(mode)); tabEl.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); setMode(mode); } }); } bindTab(els.tabFAQ, 'faq'); bindTab(els.tabHowTo, 'howto'); els.btnAddQA.addEventListener('click', () => { els.qaList.appendChild(createQAItem()); rebuild(false); }); els.btnAddStep.addEventListener('click', () => { els.stepList.appendChild(createStepItem()); rebuild(false); }); els.btnReset.addEventListener('click', () => { resetAll(); showToast('Reset.'); }); els.btnExample.addEventListener('click', () => { setExample(); showToast('Loaded example.'); }); els.btnBuild.addEventListener('click', () => { const { errors } = rebuild(true); showToast(errors.length ? `Generated with ${errors.length} error(s).` : 'Generated JSON-LD.'); }); els.btnCopy.addEventListener('click', async () => { const { out, errors } = rebuild(true); if (errors.length) { showToast('Fix errors before copying.', true); return; } const ok = await copyText(out); showToast(ok ? 'Copied to clipboard.' : 'Could not copy. Select and copy manually.', !ok); }); els.btnCopyDirect.addEventListener('click', async () => { const ok = await copyText(els.output.value); showToast(ok ? 'Copied output.' : 'Could not copy.', !ok); }); els.btnSelect.addEventListener('click', () => { els.output.focus(); els.output.select(); showToast('Selected all.'); }); els.btnDownload.addEventListener('click', () => { const { out, errors } = rebuild(true); if (errors.length) { showToast('Fix errors before downloading.', true); return; } const ext = els.optWrapScript.checked ? 'html' : 'json'; const namePart = state.mode === 'faq' ? 'faqpage' : 'howto'; const filename = `schema-${namePart}.jsonld.${ext}`; downloadText(filename, out, ext === 'json' ? 'application/ld+json;charset=utf-8' : 'text/html;charset=utf-8'); showToast(`Downloading ${filename}…`); }); const rebuildOn = ['pageUrl','pageName','itemDescription','howtoTotalTime','howtoSupply','howtoTool','optWrapScript','optPretty','optOrgSite','orgName','orgUrl','orgLogo','orgSameAs','siteName','siteUrl']; rebuildOn.forEach(id => { const el = $(id); if (!el) return; el.addEventListener('input', () => { // If pageUrl changed and siteUrl empty, auto-suggest origin. if (id === 'pageUrl') { const origin = toOriginMaybe(els.pageUrl.value); if (origin && !els.siteUrl.value) els.siteUrl.value = origin; } rebuild(false); }); el.addEventListener('change', () => rebuild(false)); }); // Init resetAll();