Going headless solves a lot of problems and quietly creates one: your structured data strategy stops working.
On a traditional CMS, a plugin reads the rendered page and outputs markup. That entire model depends on the CMS knowing what the page looks like. In a headless setup it doesn't: the CMS stores content, and something else entirely decides how it's presented. No plugin can generate markup for a page it never sees.
Which means structured data becomes your problem to architect. And the mistake most teams make is treating it as a frontend rendering task, bolted on at the end. In a headless system it's really a content modeling problem, and solving it in the content layer is what stops it decaying six months later.
This covers the three architectural approaches, how to model schema fields, notes for the major platforms, and the failure modes specific to decoupled setups.
Why the plugin model breaks
Worth being precise about what's actually lost, because it explains what has to replace it.
A traditional CMS plugin has three things: access to the content, knowledge of the template that will render it, and a hook at the moment of rendering. It can inspect a page, infer that it's a product page, pull the price from the visible markup, and emit matching JSON-LD.
Headless removes the second and third. Your CMS holds a product entry with a price field. It has no idea whether that renders as a product page on your website, a card in a mobile app, a row in a partner feed, or all three. There's no rendering moment to hook into because rendering happens somewhere else, possibly in several somewhere elses.
This has a consequence people underestimate: there is no single correct schema output for a piece of headless content. The same product entry might need Product markup on the web storefront and nothing at all in the mobile app. Schema is a web-presentation concern, and headless deliberately separates content from presentation.
That tension is the whole design problem. Resolve it deliberately or it resolves itself badly.
Three architectural approaches
1. Model schema in the CMS. Add explicit structured data fields to your content types: a seoTitle, a schemaType, an faqItems array. Editors fill them in, the frontend reads them and emits JSON-LD nearly verbatim.
Good: editorial control, no inference, values visible to the people writing content. Bad: more editorial burden, and it re-couples your CMS to web presentation (the thing you went headless to avoid). Also invites drift when editors fill schema fields that contradict the actual content.
2. Derive schema at the frontend. The CMS stores clean, presentation-agnostic content. The frontend maps content types to schema types and constructs JSON-LD from existing fields.
Good: stays true to headless principles, no editorial burden, markup can't contradict rendered content because it's built from the same data. Bad: mapping logic lives in the frontend and must be maintained per frontend. Two consumers means two implementations that will diverge.
3. Generate through a service. A dedicated layer (an API, an MCP connector, a build-time step) takes content and produces validated schema, which any frontend consumes.
Good: one implementation across all consumers, centralized validation, schema type knowledge stays current in one place. Bad: another moving part, and a dependency.
Most teams should start with approach 2 and add selective bits of 1 where inference genuinely can't work: FAQ blocks, custom sameAs lists, editorial overrides. Approach 3 earns its keep once you have multiple frontends or a large enough catalogue that maintaining mapping logic by hand becomes its own job.
Modeling schema fields properly
If you're adding structured data fields to your content model, a few principles save pain later.
Derive first, store second. Any value that can be computed from existing content should be. If you have title, publishedAt, and author fields, don't add schemaHeadline, schemaDatePublished, and schemaAuthor alongside them; you've just created three fields that can silently disagree with the originals. Add fields only for information genuinely not present elsewhere.
Store facts, not markup. Never give editors a raw JSON-LD textarea. It will contain invalid JSON within a month, nobody will validate it, and you've handed a code-editing task to people who reasonably don't want one. Store an faqItems array of question/answer pairs, not a serialized FAQPage blob.
Make the schema type explicit where it matters. A LocalBusiness entry should carry a businessType field constrained to valid subtypes: Dentist, Restaurant, Plumber. Google recommends the most specific applicable type, and specificity is exactly what inference gets wrong. Constrain it to a validated list rather than free text; our schema types directory has the full list of valid subtypes to check against.
Model entities once and reference them. Your organization is one entity. Authors are entities. Create a singleton organization entry and an author content type, reference them from articles, and build a stable @id for each. Duplicating organization details on every article produces markup describing dozens of distinct organizations that happen to share a name, which undermines exactly the kind of entity consistency that helps with AI search.
Keep sameAs on the entity. Author profile links and company profile URLs belong on the author and organization records, not repeated per article.
Platform notes
Contentful. Model a reusable seo content type and reference it from your page types rather than duplicating fields everywhere. Contentful's reference fields map naturally onto the entity pattern: an author reference becomes a Person entity with a stable @id. Use its validation constraints to restrict schema type fields to valid values, and be aware the Content Delivery API and Preview API return different data, so schema built from a preview payload can differ from production output.
Sanity. GROQ makes derived schema unusually pleasant, since you can project exactly the fields your mapping function needs in one query rather than over-fetching and filtering. Portable Text is the thing to watch: it's a structured document format, not HTML, so any schema field expecting plain text (description, articleBody) needs a serializer to flatten it. Passing raw Portable Text into JSON-LD produces object soup that validates as nothing useful.
Strapi. A component-based seo component reused across content types works well, and lifecycle hooks can validate schema-relevant fields before an entry saves, genuinely useful for catching a missing businessType or malformed date at the point of authoring rather than at build.
Payload. Its TypeScript-first config means you can type your schema fields alongside everything else, and combining it with schema-dts on the frontend gives you type checking end to end.
Headless WordPress. The awkward one, and worth calling out because it catches people. Running WordPress headless via WPGraphQL or the REST API means your existing SEO plugin still generates schema, for the WordPress frontend nobody visits. All that markup is emitted into a theme your users never see, while your actual frontend ships with none. Teams migrate to headless assuming the plugin still covers them and discover months later that structured data quietly disappeared at launch. If you're headless on WordPress, you need one of the three approaches above regardless of which plugins are installed.
Directus and other SQL-backed systems. Because content maps to database tables, deriving schema is straightforward, but you'll usually want a view or computed field layer so the frontend isn't reconstructing entity relationships from raw joins on every request.
The editorial workflow problem
Technical implementation is the easy half. The half that determines whether this still works in a year is process.
Who owns schema accuracy? In a plugin setup, nobody had to; it happened automatically. Decoupled, it's ambiguous by default. Developers own the mapping logic, editors own the content the mapping reads, and neither owns the output. Name an owner explicitly.
Validate before publish, not after. The most valuable thing you can build is a check in the editorial workflow: when an editor saves a product without a price, or an article without an author, they see the problem immediately. Catching it in a monthly audit means a month of pages shipped with incomplete markup.
Preview must reflect production. If your preview environment renders schema differently from production (because it hits a different API, or serves drafts through a different path), then reviewing markup in preview tells you nothing. Ensure the schema pipeline is identical in both.
Don't emit markup for drafts. Scheduled and draft content shouldn't produce indexed structured data. This sounds obvious and is a common bug in headless setups, where draft filtering lives in query logic that the schema builder sometimes bypasses.
Document the content-to-schema mapping. Which content type produces which schema type, and which fields populate which properties. Without it, the next developer either reverse-engineers it or quietly reimplements it differently.
Multiple frontends
Where headless genuinely earns its complexity (one content source feeding a website, a mobile app, partner feeds), structured data needs a clear policy.
Only web frontends emit JSON-LD. Structured data is a web crawling concern. A React Native app has no use for it.
One canonical URL per entity. If the same article is reachable at two URLs across two web properties, decide which is canonical and make sure @id values and mainEntityOfPage point consistently at it. Two frontends independently claiming the same content is the fastest route to duplicate entity confusion.
Centralize the mapping if you have more than one web consumer. Two frontends with independently maintained mapping logic will diverge, and the divergence surfaces as inconsistent markup describing the same entity differently. This is the point where a shared service or connector stops being over-engineering.
Validation in CI
Because there's no plugin guarding correctness, build your own guardrails. This is the highest-value thing in this article for a team with an existing headless setup.
Validate JSON-LD syntax at build. Parse every generated block. A malformed block is worse than none: it can invalidate everything on the page.
Type-check against schema.org. schema-dts gives you compile-time checking on property names and value types, catching a misspelling before it ships.
Assert required properties per type. Write tests: every Product has name and offers, every Article has headline, author, and datePublished. These are cheap to write and catch the CMS-side omissions that type checking can't see.
Snapshot-test representative pages. Lock in the JSON-LD output for one page per content type. Any unintended change to the mapping shows up in a diff review rather than in Search Console weeks later.
Crawl the built output. After a production build, verify the JSON-LD is actually in the served HTML. This catches the rendering-side failures (client-component emission, suspended boundaries) that unit tests can't see. Our Next.js schema guide covers those failure modes in detail.
Check types are still supported. Google has retired eleven structured data features since 2023. A build-time check against the currently supported list stops you maintaining markup for a feature that no longer exists; see what Google still supports in 2026.
A worked example: article to Article
Abstract advice is easy to nod along to and hard to act on, so here's the mapping made concrete for a common case: a headless blog with authors and categories.
Content model. An article type with title, slug, excerpt, body, coverImage, publishedAt, updatedAt, and a reference to an author. An author type with name, slug, bio, and a socialLinks array. A singleton organization with name, logo, and profileLinks.
What derives automatically. headline from title. description from excerpt. image from coverImage. datePublished and dateModified from the timestamp fields, converted to ISO 8601. mainEntityOfPage from slug plus your site's base URL. None of these need new CMS fields, and because they read the same values the page renders, they can't contradict what a visitor sees.
What comes from references. The author becomes a Person entity with @id built from the author slug, name from the author record, url pointing at the author's page, and sameAs from socialLinks. The publisher is an @id reference to the organization singleton, defined once in your layout. Fifty articles by the same author produce fifty references to one Person, not fifty separate people.
What genuinely needs a new field. Very little. If some articles carry an FAQ block, add an optional faqItems array of question/answer pairs; that information exists nowhere else in the model. Note this is worth doing for machine comprehension rather than a Google rich result, since FAQ rich results were retired in May 2026.
What to reject. A schemaHeadline field duplicating title. A raw jsonLd textarea. An authorName string alongside the author reference. Each creates a second source of truth that will eventually disagree with the first.
The general test: if a proposed schema field could be computed from something already in the model, it shouldn't be a field. That single rule prevents most long-term drift.
Common failure modes
Schema disappearing at headless migration. Covered above, and the most common of all. The old plugin still runs; nobody sees its output.
Markup that doesn't match rendered content. In headless this is structurally more likely, because markup and rendering can be built from different queries. If your schema reads a summary field while the page displays intro, you're describing content users can't see: a policy violation.
Portable Text or rich text passed raw. Structured document formats need serializing to plain text before entering JSON-LD.
Dates as locale strings. Schema needs ISO 8601. If your CMS returns formatted dates, convert before use.
Entity duplication. Every article carrying a full inline organization object instead of an @id reference.
Preview data in production markup. Draft or preview API payloads leaking into published schema.
No owner. The slow failure. Everything works at launch, content types get added over the following year, and nobody extends the mapping to cover them.
What we'd recommend
For most headless teams: derive schema at the frontend from existing content, add CMS fields only where inference genuinely can't work, model organizations and authors as referenced entities with stable @id values, validate in CI, and name an owner.
That covers a single web frontend well. Once you have several consumers, a large catalogue, or a need to track a supported-types list that keeps changing, maintaining mapping logic by hand becomes a real ongoing cost. That's the gap our MCP connector closes: content in, validated schema out, one implementation for every frontend. Check our pricing page for what's included on the free plan, or build it yourself; everything above is what you need either way.
Frequently Asked Questions
AI Schema Gen's MCP connector generates and validates structured data from your content across 827+ schema types, for any headless frontend. Start free or read the documentation.
Generate perfect schema in 30 seconds
AI Schema Gen handles everything automatically, free to start.
Get Started Free