Automating structured data means generating JSON-LD from the same CMS fields that render each page, through one shared function per content type, and checking the output automatically before release. It keeps schema accurate as content changes, avoids the conflicting duplicates that appear when themes and plugins each add their own, and scales to thousands of pages without hand editing. The work has three parts: map fields to schema properties, generate one connected graph per page, and validate it in your pipeline.
Step 1: map CMS fields to schema properties
| Content type | Schema type | CMS field | Property |
|---|---|---|---|
| Blog post | BlogPosting | Title, publish date, content-updated date, author | headline, datePublished, dateModified, author (@id of a Person) |
| Service | Service | Name, summary, areas served | name, description, areaServed, provider (@id of the Organization) |
| Product | Product and Offer | Name, SKU, price, stock, images | name, sku, offers.price, offers.priceCurrency, offers.availability, image |
| Location | LocalBusiness | Address, phone, hours | address, telephone, openingHoursSpecification |
| Team member | Person | Name, role, profile links | name, jobTitle, sameAs, worksFor |
Only map fields that are visible on the page. Google’s guidelines require structured data to describe content users can see.
Step 2: generate one graph per page
- Output the site-wide Organization and WebSite nodes once, with stable @id values.
- Add page-specific nodes that reference them by @id, rather than repeating them.
- Generate on the server, so every crawler receives it in the HTML.
- Disable schema output from any other source, such as a theme or plugin, that would create a second version.
Framework examples: Laravel Blade and the Next.js checklist. This site’s WordPress theme generates its graph the same way and switches it off automatically when an SEO plugin is active.
Step 3: validate automatically
This Python script finds JSON-LD blocks in a page, checks the JSON is valid, checks required properties for common types, and flags more than one Organization node. Run it against key templates on staging in your deployment pipeline.
#!/usr/bin/env python3
"""Check JSON-LD on a page for required properties per type, and duplicate Organization nodes.
Usage: python3 check_jsonld.py page.html"""
import json, re, sys
REQUIRED = {
"Organization": ["name", "url"],
"Article": ["headline", "datePublished", "author"],
"BlogPosting": ["headline", "datePublished", "author"],
"Product": ["name", "offers"],
"Offer": ["price", "priceCurrency", "availability"],
"LocalBusiness": ["name", "address", "telephone"],
"Person": ["name"],
}
def nodes(data):
if isinstance(data, list):
for d in data: yield from nodes(d)
elif isinstance(data, dict):
if "@graph" in data: yield from nodes(data["@graph"])
if "@type" in data: yield data
for k, v in data.items():
if k != "@graph" and isinstance(v, (dict, list)): yield from nodes(v)
def main():
html = open(sys.argv[1], encoding="utf-8").read()
blocks = re.findall(r'<script[^>]+application/ld\+json[^>]*>(.*?)</script>', html, re.S | re.I)
problems, orgs = [], 0
for i, b in enumerate(blocks, 1):
try:
data = json.loads(b)
except ValueError as e:
problems.append(f"block {i}: invalid JSON ({e})"); continue
for n in nodes(data):
types = n["@type"] if isinstance(n["@type"], list) else [n["@type"]]
if set(n) <= {"@id", "@type"}:
continue # a reference to a node defined elsewhere
if "Organization" in types: orgs += 1
for t in types:
for prop in REQUIRED.get(t, []):
if prop not in n:
problems.append(f"{t} missing {prop}")
if orgs > 1: problems.append(f"{orgs} Organization nodes (expected 1)")
print("\n".join(dict.fromkeys(problems)) if problems else "JSON-LD looks complete.")
sys.exit(1 if problems else 0)
if __name__ == "__main__":
main()
We tested it on a sample page with an Offer missing its availability, a post missing its publish date, and a second Organization node of the kind a plugin often adds. It reported:
Offer missing availability
BlogPosting missing datePublished
2 Organization nodes (expected 1)
The required lists above are a practical minimum. For Google’s rich results, check each type’s documentation for required and recommended properties, and use the Rich Results Test on a sample of pages after deployment.
Keep it accurate over time
- Use a content-changed timestamp for dateModified, not the time of any database update.
- Regenerate schema whenever the underlying fields change, which happens automatically when it is built at render time.
- Review Search Console’s rich result reports monthly for new errors.
Frequently asked questions
Should schema come from an SEO plugin or custom code?
Either works if there is exactly one source. Plugins suit standard sites; custom code suits custom content types and headless builds.
Can AI generate schema for us?
It can draft mappings, but generated schema must be built from real page data by code, not written by a model page by page, or it will drift from the content.
Does more schema help AI visibility?
Accurate schema helps systems identify your company, people and products. Extra types that do not describe the page add nothing. See schema and AI Overviews.
Sources
- Google Search Central, “General structured data guidelines” and type-specific documentation
- schema.org type definitions