Structured data is one of those chores that looks small until you have forty product pages, six templates, and a CMS that rewrites fields without telling anyone. The hard part is rarely the JSON-LD itself; it's deciding who produces it, where it lives in the pipeline, and what happens when a marketer edits a headline. This article walks through three practical ways teams actually ship structured data in production, with the trade-offs I'd defend in a code review.
If you want a refresher on the underlying concepts (the @context, the @type, why JSON-LD won), the Lizely JSON-LD guide covers the mechanics. Here, the question is purely operational.
The Three Workflows Worth Naming
Engineers tend to romanticize the first option and underestimate the third. Let me give each one fair treatment.
-
Hand-authoring in templates. A developer edits a Twig, Liquid, or JSX partial and pushes JSON-LD straight into the
<head>. - Spreadsheet-driven generation. Marketing or SEO owns a Google Sheet that defines every entity; a script converts rows into payloads at build time.
- Purpose-built online generator. A form-based tool builds the payload, and a developer pastes the result into the template, often gated by a review step.
None of these is universally correct. The right answer depends on who owns the content, how often it changes, and how painful a regression would be.
Workflow A: Hand-Authoring in Templates
This is the path most senior engineers default to, and for good reason: the output is reviewable in the same diff as the rest of the template.
When it works. Small sites, stable entity types (Organization, WebSite, BreadcrumbList), and a single developer who owns the markup for the long haul.
What the diff actually looks like. In a Next.js layout component, for example, the block might read:
const jsonLd = {
"@context": "https://schema.org",
"@type": "Organization",
"name": "Acme Co.",
"url": "https://acme.example.com",
"logo": "https://acme.example.com/logo.png",
};
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
That snippet is fine — the payload is co-located with the component that renders the page chrome, the JSON.stringify call is supported everywhere modern, and a reviewer can read both pieces at once.
Where it breaks. The moment a non-engineer needs to change a price, an event date, or a FAQ answer, hand-authoring becomes a bottleneck. I've watched teams solve this by giving marketing CMS access to "the JSON-LD field," which is how you end up with a Product page whose @price reads "free-ish" because someone forgot a CMS validation rule.
Hardening checklist for hand-authored payloads.
- Render the payload server-side, never client-only, so crawlers without JavaScript still see it.
- Treat the
application/ld+jsonscript as part of the page's critical output, not a side effect. - Add a unit test that asserts the script tag exists and that
JSON.parseround-trips. - Add a second test that fetches the rendered HTML and confirms the payload validates against your target
ArticleorProductdefinition.
The discipline is real, but it's the same discipline you'd apply to any template logic. If your team already has it, this workflow is the cheapest to operate.
Workflow B: Spreadsheet-Driven Generation
This is the unsexy option that wins in mid-sized teams. A Google Sheet or Airtable holds one row per entity, with columns matching the schema.org properties you care about. A small script — Node, Python, whatever the team already uses — reads the sheet at build time and emits either a static file or a hydrated variable.
When it works. Product catalogs, event series, recipes, job postings — anywhere the same Product or Event shape repeats dozens or hundreds of times and the data lives in a non-engineer's tool already.
What the script does. Strip the boilerplate down to three responsibilities:
- Fetch rows from the sheet via its API.
- Map columns to schema.org properties, with explicit defaults for missing fields.
- Serialize to JSON-LD and write to a known output path.
The mapper is the part worth reviewing carefully. It's tempting to do row.price → "price", but a CSV column containing "$12.99" will quietly violate the schema's expectation of a numeric PriceSpecification. The mapper should normalize: parse numbers, format ISO 8601 dates, and reject rows that fail a schema check before they ship.
Where it breaks. Sheets drift. People add columns, rename fields, or paste in mixed-formatted numbers. The script needs a dry-run mode that diffs against the previous build and a way to surface those diffs to whoever owns the sheet.
Trade-offs the team should accept up front.
- One source of truth for non-engineers, but two places to look when something breaks.
- Build times go up slightly; cache the output unless the sheet actually changed.
- You now own a small piece of glue code; budget maintenance time for it.
The win is that marketing can publish a new product row without filing a ticket, and the JSON-LD follows automatically. For a catalog of 500 SKUs, that's the difference between a deploy every Friday and a deploy never.
Workflow C: Purpose-Built Online Generator
The third path is the one engineers are most suspicious of, and also the one most often miscategorized. A form-based generator isn't a substitute for an engineer — it's an accelerator for the steps that don't require one.
When it works. A one-off FAQPage for a landing page, a LocalBusiness block for a regional office, an Event payload for a single conference. Anywhere the markup is small, stable, and unlikely to repeat.
What the tool actually does. Walks you through the relevant fields with sensible defaults, validates as you type, and produces a payload you can paste into a template. The output is the same JSON-LD you'd write by hand; the value is in preventing the dumb mistakes (mismatched brackets, a trailing comma, a typo in @type that produces a no-op payload).
Where it breaks. Treating the generator as the workflow. If your team is copy-pasting output from a tool into the CMS every Friday, you don't have a tool problem, you have a workflow problem. The generator should output a payload once, the payload should land in version control, and from that point forward it should be treated like any other template.
Decision rules I actually use.
- Use the generator for the first payload of any new entity type. It's faster than typing from memory and it surfaces properties you would have forgotten.
- Never paste from the generator directly into production without a review. Treat the paste as a code change.
- Once you have more than two instances of the same entity type on the site, switch to Workflow A or B. Repetition is the trigger, not volume.
Picking the Right Workflow
The choice rarely lives at the level of the whole site; different entity types on the same site can use different paths. A reasonable split:
- Sitewide chrome (Organization, WebSite, BreadcrumbList): hand-authored in the layout. Changes rarely, owned by engineering.
- Repeating content (Product, Recipe, Event, JobPosting): spreadsheet-driven if the data already lives in a sheet, hand-authored if it doesn't and the count is small.
- One-off pages (FAQPage, HowTo, single Event): generator for the initial payload, then committed to the repo like any other template.
Two warning signs that you've chosen wrong:
- Engineers are the bottleneck for content that marketing should own.
- Markup silently disappears when someone redeploys.
Both are fixable, but only after you admit which workflow you actually have.
Frequently Asked Questions
Should I generate JSON-LD client-side or server-side?
Server-side, always. Client-rendered JSON-LD is invisible to crawlers that don't execute JavaScript, and you have no guarantee that the bot visiting your page is one that does. Render it during the same request that produces the HTML.
How do I keep payloads from rotting?
Treat the payload as part of the template's output, then add a test that fetches the rendered page and asserts the JSON-LD is well-formed and matches the expected shape. Schema.org itself is versioned loosely, but the W3C JSON-LD 1.1 specification is stable enough that you can lock your tests against it for years.
What about validation tools like Google's Rich Results Test?
Use them as a final gate, not as the source of truth. Validation passes don't guarantee the markup is correct; they guarantee it isn't obviously wrong. Combine them with property-level assertions in your own test suite.
How often should I revisit the schema.org types I'm using?
Quarterly is a reasonable cadence, or whenever you ship a new content type. The schema.org vocabulary evolves slowly, and most additions are additive. If your page describes something the vocabulary doesn't cover yet, it's fine to ship the closest existing type and revisit later.
This article was drafted with AI assistance and reviewed for technical accuracy before publishing.
Top comments (0)