Skip to content
Coding Script or NotebookGoogle ColabFree

Structured Data Automation Kit: Audit, Generate and Validate JSON-LD with Python

ForEntity SEOContent audits

By Lazarina Stoy· Updated 24 Oct 2026

Most schema markup tools work one page at a time: paste a URL, read the errors, fix, repeat. That stops working at a few hundred pages, and it never tells you where markup is missing. This free Python toolkit treats structured data as a pipeline. One script, sd_toolkit.py, has three commands that follow the three steps of my guide.

What each command does

  • audit reads each URL, extracts the JSON-LD, Microdata and RDFa already there, runs a rule-based page classifier to flag structured data opportunities, marks whether Google still shows a rich result for each one, and validates what it finds. One CSV row per URL.
  • generate builds JSON-LD from a data source (a CSV, or any SQL query) instead of a generator page by page. Each page gets one connected @graph with Organization, WebSite, WebPage, BreadcrumbList and the main entity (Article, Product, Event or Course), linked by @id. It writes a .json file and a ready-to-paste script tag per page.
  • validate checks a file or a live URL against the current schema.org vocabulary, Google’s required and recommended properties for rich results that still exist, and the page’s visible text.

Who it’s for

Technical SEOs and developers implementing schema across a whole site, especially on templates or product catalogues that already live in a spreadsheet or database.

How to use it

  1. Install the requirements locally, or upload sd_toolkit.py and example_data/ to Google Colab.
  2. Put the URLs you want to check in a text file and run audit. Review the opportunities before you generate anything.
  3. Fill a CSV with one row per page, using columns named after schema.org properties (the sample pages.csv shows the format), or point generate at a SQL query.
  4. Run generate to write the JSON-LD files.
  5. Run validate on the generated files, then on the live pages once they’re deployed.

Limits

  • The classifier suggests; it doesn’t decide. Every suggestion names the signal that fired so you can check it.
  • It doesn’t replace Google’s Rich Results Test, which has no public API. Spot-check a sample of templates there.
  • It reads the HTML the server returns, so markup injected by JavaScript (for example through Google Tag Manager) won’t show up.
  • Free to run. The first validation downloads the schema.org vocabulary (about 1.5 MB) and caches it.

The method and the reasoning behind the connected graph are in Structured Data Automation with Python. For the entity side of schema, see the Entity SEO and Knowledge Graph Strategies for Brands course.

What’s included

  • audit: extracts JSON-LD, Microdata and RDFa from each URL, flags structured data opportunities and whether Google still shows that rich result
  • generate: builds one connected @graph per page (Organization, WebSite, WebPage, BreadcrumbList, main entity) from a CSV or SQL query
  • validate: checks the current schema.org vocabulary, Google's required and recommended properties, and that the markup matches visible page text
  • Up-to-date rich result status, including FAQPage and HowTo retirements
  • Sample CSV and SQLite demo included
. Disclosure menus (APG pattern), hover intent, mobile dialog with focus trap, announcement dismiss, skip-link target, newsletter status message. No dependencies. */ (function () { 'use strict'; var d = document;/* ---------- skip link target: #main, else the page's
, else the first block after the header */ (function () { var mains = d.querySelectorAll('main'); if (mains.length > 1) { // Our post-content
plus another one: drop the landmark from ours. var ours = d.querySelector('main[data-mlfc-main]'); if (ours) { ours.setAttribute('role', 'none'); ours.removeAttribute('id'); } } var skip = d.querySelector('.mlfc-skip'); if (!skip) return; var t = d.getElementById('main'); if (!t || (t.getAttribute('role') === 'none')) { t = d.querySelector('main:not([role="none"]), [role="main"]'); if (!t) { var hd = skip.closest('.wp-block-template-part') || skip.closest('.mlfc-hdw'); t = hd && hd.nextElementSibling; } if (!t) return; if (!t.id) t.id = 'main'; skip.setAttribute('href', '#' + t.id); } if (!t.hasAttribute('tabindex')) t.setAttribute('tabindex', '-1'); skip.addEventListener('click', function () { setTimeout(function () { t.focus({ preventScroll: false }); }, 0); }); })();/* ---------- announcement dismiss */ d.querySelectorAll('[data-mlfc-ann]').forEach(function (bar) { var btn = bar.querySelector('[data-mlfc-ann-close]'); if (!btn) return; btn.addEventListener('click', function () { try { localStorage.setItem(bar.getAttribute('data-mlfc-ann'), '1'); } catch (e) {} var next = d.querySelector('.mlfc-logo'); bar.hidden = true; if (next) next.focus(); }); });/* ---------- disclosure dropdowns */ var items = [].slice.call(d.querySelectorAll('.mlfc-hdw [data-mlfc-dd]')); var fine = window.matchMedia && window.matchMedia('(hover: hover) and (pointer: fine)').matches; function btnOf(it) { return it.querySelector('button[aria-controls]'); } function panelOf(it) { return d.getElementById(btnOf(it).getAttribute('aria-controls')); } function isOpen(it) { return btnOf(it).getAttribute('aria-expanded') === 'true'; } function setOpen(it, open) { var b = btnOf(it), p = panelOf(it); if (!b || !p) return; b.setAttribute('aria-expanded', open ? 'true' : 'false'); p.hidden = !open; if (open) { // keep the panel inside the viewport p.style.left = ''; p.style.right = ''; var r = p.getBoundingClientRect(); if (r.right > window.innerWidth - 12) { p.style.left = 'auto'; p.style.right = '0'; } } } function closeAll(except) { items.forEach(function (it) { if (it !== except && isOpen(it)) setOpen(it, false); }); }items.forEach(function (it) { var b = btnOf(it), tIn, tOut; if (!b) return; b.addEventListener('click', function () { clearTimeout(tIn); clearTimeout(tOut); var open = !isOpen(it); closeAll(it); setOpen(it, open); }); if (fine) { // Hover intent: open after 300 ms, close 400 ms after the pointer leaves trigger + panel. it.addEventListener('mouseenter', function () { clearTimeout(tOut); tIn = setTimeout(function () { closeAll(it); setOpen(it, true); }, 300); }); it.addEventListener('mouseleave', function () { clearTimeout(tIn); tOut = setTimeout(function () { setOpen(it, false); }, 400); }); } it.addEventListener('focusout', function (e) { if (e.relatedTarget && !it.contains(e.relatedTarget)) setOpen(it, false); }); it.addEventListener('keydown', function (e) { if (e.key === 'Escape' && isOpen(it)) { e.stopPropagation(); setOpen(it, false); b.focus(); } }); }); d.addEventListener('click', function (e) { items.forEach(function (it) { if (isOpen(it) && !it.contains(e.target)) setOpen(it, false); }); }); d.addEventListener('keydown', function (e) { if (e.key !== 'Escape') return; items.forEach(function (it) { if (isOpen(it)) { setOpen(it, false); btnOf(it).focus(); } }); });/* ---------- mobile menu dialog */ var menuBtn = d.querySelector('.mlfc-menu-btn'); var dlg = d.getElementById('mlfc-mnav'); if (menuBtn && dlg) { var FOC = 'a[href],button:not([disabled]),input,select,textarea,[tabindex]:not([tabindex="-1"])'; var openMenu = function () { dlg.hidden = false; menuBtn.setAttribute('aria-expanded', 'true'); d.body.classList.add('mlfc-lock'); var c = dlg.querySelector('[data-mlfc-close]'); if (c) c.focus(); }; var closeMenu = function (restore) { if (dlg.hidden) return; dlg.hidden = true; menuBtn.setAttribute('aria-expanded', 'false'); d.body.classList.remove('mlfc-lock'); if (restore !== false) menuBtn.focus(); }; menuBtn.addEventListener('click', function () { dlg.hidden ? openMenu() : closeMenu(); }); dlg.querySelector('[data-mlfc-close]').addEventListener('click', function () { closeMenu(); }); dlg.addEventListener('keydown', function (e) { if (e.key === 'Escape') { e.preventDefault(); closeMenu(); return; } if (e.key !== 'Tab') return; var f = [].filter.call(dlg.querySelectorAll(FOC), function (el) { return el.offsetParent !== null || el === d.activeElement; }); if (!f.length) return; var first = f[0], last = f[f.length - 1]; if (e.shiftKey && d.activeElement === first) { e.preventDefault(); last.focus(); } else if (!e.shiftKey && d.activeElement === last) { e.preventDefault(); first.focus(); } }); // In-page links (e.g. #courses) close the menu first. dlg.addEventListener('click', function (e) { var a = e.target.closest && e.target.closest('a[href^="#"]'); if (a) closeMenu(false); }); var mq = window.matchMedia('(min-width: 1024px)'); var onMq = function () { if (mq.matches) closeMenu(false); }; if (mq.addEventListener) mq.addEventListener('change', onMq); else if (mq.addListener) mq.addListener(onMq); }/* ---------- newsletter: plain POST to Kit in a new tab (same pattern as mlfa2_newsletter()); say so in the live region */ d.querySelectorAll('form[data-mlfc-nl]').forEach(function (form) { var msg = form.querySelector('.mlfc-nl__msg'); form.addEventListener('submit', function () { if (msg) msg.textContent = 'Thanks. Kit opens in a new tab: confirm your subscription there and check your inbox.'; }); }); })();