Pricing markup fails in production for a reason that has nothing to do with schema: the price in the JSON-LD and the price on the page come from different places, and one of them changes. Everything below follows from a single architectural decision - generate the visible pricing and the structured data from the same authoritative model - and from a testing discipline that keeps six separate questions separate. Vocabulary validity, what a rich-result test detects, what Google has indexed, what Google can fetch right now, what is happening across the whole template, and whether the markup still equals your pricing authority are six different questions. Four platform tools answer the first five questions, with URL Inspection providing two distinct views. A fifth, custom parity test answers the sixth - and you have to build it yourself.
Two labels mark evidence boundaries on this page. Documented means the guidance that follows is directly supported by published Google or Schema.org documentation, quoted. Recommendation means a threshold, workflow, ownership model or pass rule that CoreAEX prescribes and the documentation does not. This page is dense in pass rules, so the distinction matters more here than on most pages: an untagged acceptance criterion below is ours, not Google's. Untagged text is ordinary explanation or a conclusion following from something already labelled.
One Pricing Source of Truth
The failure mode has a shape. Someone writes the JSON-LD by hand when the pricing page is built. Six months later marketing changes the Growth plan from $79 to $89 in the CMS. The visible page updates. The JSON-LD does not, because nobody remembers it is there. The page now publishes two different prices, one of them wrong, and no validator will notice - the markup is still perfectly valid.
Recommendation The fix is architectural rather than procedural. One pricing model - a database table, a config file, a billing-system export, a CMS content type - is the authority. The visible page renders from it. The JSON-LD is generated from it. Any exports, feeds or emails derive from it. Nothing downstream holds its own copy of a price.
Documented This is worth insisting on because Google's requirement here is about content quality, not about markup hygiene. The general structured data guidelines state it as a content rule: Don't mark up content that is not visible to readers of the page
, and Don't use structured data to deceive or mislead users
. A stale price in JSON-LD is not a technical defect that happens to violate a validator preference. It is inaccurate content published in a machine-readable layer.
Three practical consequences of taking the source-of-truth principle seriously:
- Hand-written JSON-LD on a pricing page is a defect, even when it is currently correct, because it will drift. Generate it.
- A price should appear once in your codebase. If a grep for the current price returns more than one authoritative location, you have already lost.
- The tag manager is not a source of truth. Injecting pricing markup through a tag manager puts the price in a system that marketing edits independently of the pricing model, which is the original problem with an extra step.
JSON-LD Placement and Graph Design
Documented Format. Google recommends JSON-LD and supports three formats. The introduction to structured data lists JSON-LD, Microdata and RDFa, marking JSON-LD (Recommended)
and describing it as the easiest solution for website owners to implement and maintain at scale
. Recommended is not exclusive: Microdata and RDFa remain supported, and a site already using one of them is not obliged to migrate.
Documented Placement. Google describes JSON-LD as a script tag embedded in a <script> tag in the <head> and <body> elements of an HTML page
- either is acceptable. The location rule that does bite is about which page the markup sits on: Put the structured data on the page that it describes, unless specified otherwise by the documentation
. Pricing markup belongs on the pricing page, not injected site-wide from a layout template onto every URL.
Graph design and stable identifiers. A pricing page usually describes more than one thing: the seller, the product, and several offers. Rather than repeating the organisation inside every offer, declare each node once with a stable @id and reference it. The identifiers should be durable URLs you control and do not change when the page is redesigned.
{
"@context": "https://schema.org",
"@graph": [
{
"@type": "Organization",
"@id": "https://example.com/#org",
"name": "Northwind Software Ltd",
"url": "https://example.com/"
},
{
"@type": "Product",
"@id": "https://example.com/#northwind",
"name": "Northwind Analytics",
"url": "https://example.com/pricing",
"brand": { "@id": "https://example.com/#org" },
"offers": [
{
"@type": "Offer",
"@id": "https://example.com/pricing#growth-monthly",
"name": "Growth, billed monthly",
"url": "https://example.com/pricing#growth-monthly",
"price": 79,
"priceCurrency": "USD",
"seller": { "@id": "https://example.com/#org" }
}
]
}
]
}
Validated against the published Schema.org vocabulary release, version 30.0 (released March 19, 2026): zero vocabulary errors, no pending or new terms. What it does not do is establish Google eligibility - that depends on which feature you are targeting and is settled by the type-selection page in this cluster, not by this structure.
Recommendation Two conventions worth adopting, neither of them a platform requirement. Give each offer an @id that matches the anchor a customer would actually land on, so that markup identity and page navigation agree. And keep the graph flat: one @graph array with nodes referencing each other by @id is easier to generate from a pricing model, and easier to diff in code review, than deeply nested objects.
Server versus JavaScript Delivery
Documented Google can process structured data that JavaScript creates. The documentation is explicit: Google Search can understand and process structured data that's available in the DOM when it renders the page
, and If the rendered HTML contains the structured data, Google Search will be able to process it.
So "Google can't read JavaScript structured data" is not true and should not be the argument for server-rendering it.
The argument for server-rendering pricing specifically is made of four narrower, separately-sourced points, each of them documented.
Documented Rendering is queued, not immediate. Google describes its JavaScript processing pipeline as three sequential phases - crawling, then rendering, then indexing - and states that Googlebot queues all pages with a 200 HTTP status code for rendering, unless a robots meta tag or header tells Google not to index the page. The page may stay on this queue for a few seconds, but it can take longer than that.
For content that rarely changes this is unremarkable. For a price that changed this morning it adds a step between publication and processing.
Documented Google names a reliability caveat for Product markup specifically. Verbatim: Using Product markup? Be aware that dynamically-generated markup can make Shopping crawls less frequent and less reliable, which can be an issue for fast-changing content like product availability and price.
Note the scope carefully: this is about Shopping crawls, and it is conditional. It is not a statement about Google Search generally.
Documented Merchant Center will not accept it at all. If your pricing also feeds Merchant Center, the requirement is absolute rather than probabilistic: Structured data markup must be present in the HTML returned from the web server. The structured data markup can't be generated with JavaScript after the page has loaded.
One system documents processing the rendered DOM; the other documents refusing anything not in the server response. The same client-side implementation is readable by one and invisible to the other.
Documented Not every client is a browser. Google's own JavaScript guidance offers the general version: Keep in mind that server-side or pre-rendering is still a great idea because it makes your website faster for users and crawlers, and not all bots can run JavaScript.
Documented It is worth being exact about which AI systems have documented anything here, because Google has. Its AI features guidance states that to be eligible to appear as a supporting link in AI Overviews or AI Mode, a page must be indexed and eligible to be shown in Google Search with a snippet
, and that There are no additional technical requirements
. So eligibility for AI Overviews and AI Mode depends on ordinary Google Search technical requirements, and Googlebot controls crawling access for these Search features. The server-rendering guidance above therefore remains relevant to pages that must first be crawled and indexed - without establishing how Google's AI systems subsequently select or process those pages. Google also says There's also no special schema.org structured data that you need to add
, while listing Ensuring structured data matches the visible text on the page
among the fundamentals that remain worthwhile.
What is absent is documentation from the other operators. We reviewed OpenAI's crawler and publisher documentation, Perplexity's bot documentation and Anthropic's ClaudeBot documentation and located no statement in any of them about executing JavaScript or reading structured data. That is an absence in the sources we checked, scoped to those operators - not a finding about how their crawlers behave. And none of it, Google's guidance included, establishes that pricing markup changes what an AI answer says about your prices.
Recommendation Render pricing markup server-side. The reasons above are cumulative rather than any one of them being decisive, and the cost of doing it is low compared to the cost of a price that is right on the page and absent from the response.
Pricing Toggles and Hidden States
Almost every SaaS pricing page has a monthly/annual toggle, and many have a currency selector or a seat slider. Each of them creates the same question: what does the page say before anyone touches anything?
The answer that keeps you out of trouble is that the default state has to be a complete, honest offer on its own, and the markup describes that default state. A toggle set to "annual" by default, showing a monthly-equivalent figure, with JSON-LD stating the monthly price, is a page whose three layers disagree.
Documented Google's constraint is the visibility rule already quoted: Don't mark up content that is not visible to readers of the page
. A price that only appears after a click is, at minimum, an argument you do not want to be having. Two readings are defensible - that toggled content is present in the DOM and therefore visible, or that it is not visible until interacted with - and the documentation does not settle which applies.
Recommendation Given that ambiguity, the safe implementations are the ones that avoid needing a ruling:
- Give each billing option its own offer in the markup, so both are described rather than one being chosen. This is the same conclusion the complex-pricing page in this cluster reaches for different reasons.
- Render both states in the HTML and toggle visibility with CSS, rather than fetching the alternate price on click. What is in the server response is unambiguous.
- Don't let a toggle be the only place a material term appears. A seat minimum or a commitment length that surfaces only in one toggle state is hidden from anyone who does not click.
- A price behind a login is an access problem, not a visibility one. Authenticated users are readers too; the reason a gated price cannot support Google's Product features is that Google cannot reach it. The merchant listing documentation states the requirement directly:
Be sure that your page is accessible to Google and not blocked by a robots.txt file, the noindex tag, or login requirements.
Treat it as crawl and index access. It is not a general rule that authenticated content can never carry structured data - Google's separate paywalled-content guidance covers supportedCreativeWorktypes, which is a different feature from a pricing offer.
Locale and Currency URLs
Two separate mechanisms get confused here, and they solve different problems.
Documented Currency is a URL-structure requirement. Google's merchant listing documentation states it directly: When offering products for sale in multiple currencies, have a distinct URL per currency. For example, if a product is available for sale in Canadian and US dollars, use two distinct URLs, one per currency.
A client-side currency switcher on a single URL does not meet that structure, whatever it does for usability.
hreflang is about language and region, not currency. It is worth being precise, because "we have hreflang set up" is often offered as an answer to the currency question and it is not one. Google's localized-versions documentation covers language and regional variants; we located no statement in it about pages differing only by currency or price. Where you do publish language or regional variants, two documented rules apply: Each language version must list itself as well as all other language versions
, and If two pages don't both point to each other, the tags will be ignored.
Both are checkable in CI, which is where they belong.
Recommendation The practical arrangement that satisfies both: a stable, canonical URL per priced variant; the visible currency on that URL matching the priceCurrency in its markup; hreflang used for language and regional variants where they genuinely exist; and no geo-redirect that serves a different price at the same address.
Documented That last point has a documented consequence beyond Search. Merchant Center's automatic item updates state a condition that geo-personalised pricing fails outright: Your landing page cannot change based on information about the customer, such as if you adapt prices based on a customer's IP address or browser type.
The Validation Stack: Six Questions, Four Platform Tools, One Custom Test
The central implementation error on this topic is treating "it validates" as one fact. It is six, and the checks that answer them are not interchangeable. Note that URL Inspection is one product answering two of them - its indexed view and its live test report different things.
- Is the vocabulary well formed?
- What supported rich-result types, and what tool-reported issues, are detected?
- What does Google's indexed view report?
- What can Google fetch and render right now?
- What template-wide items and issues appear in Search Console's sampled reports?
- Does the markup equal the pricing source of truth?
The first five are answered by four platform tools - URL Inspection covers two of the questions through its indexed and live views. The sixth has no platform tool and requires your own parity test.
| Tool | What it establishes | What a pass does not establish |
|---|---|---|
| Schema Markup Validator validator.schema.org |
That the markup is syntactically well-formed and uses Schema.org types and properties in recognised combinations. Schema.org's own summary: "Identifies syntax mistakes in the markup." | Anything about any consuming system. Schema.org states it plainly: It does not try to check your content against the information needs of specific services, tools or products.A clean pass here is consistent with a page eligible for no Google feature at all. |
| Rich Results Test |
Which supported rich-result types the tool detected in the representation it tested, and what errors or suggestions it reports for them. Google describes it as showing which rich result types were found on the page, as well as any errors or suggestions for your structured data. A clean pass means the tool found no blocking issue for that supported type. |
Display. Google does not guarantee that your structured data will show up in search results, even if your page is marked up correctly according to the Rich Results Test.Nor current indexing, nor compliance with every content-quality policy, nor parity with your visible pricing, nor anything about markup outside Google's supported types. |
| URL Inspection - indexed view Search Console |
What Google actually parsed and stored the last time it indexed the page. The only tool here that reports Google's real state for a URL. | The current live page. Verbatim: This is not a live test. The results shown are from most recently indexed version of a page, not the live version on the web.And appearance: 'URL is on Google' doesn't actually guarantee that your page will appear in Search results. |
| URL Inspection - live test Search Console |
That Google can fetch and render the page as it stands now, and what structured data is detectable in that render. This is a live test: the tool fetches and examines the URL in real time. |
Indexing. Verbatim: There is no definitive test that can guarantee whether your page will be included in the Google index.It also does not test for all possible indexing issues, including whether this is a duplicate or alternate page. |
| Rich result reports Search Console |
A site-wide, sampled inventory of what Google parsed across crawls - the only view that answers "does this break across the whole template?" | Completeness or currency. The reports aren't a comprehensive list of all detected items. They show a sample of detected items, and a valid item is one that can appear on Google as a rich result- can, not will. |
| Nothing | — | None of the platform validators above checks whether your markup equals your pricing source of truth. Parity is the failure this page exists to prevent, and it is the one question no platform tool answers. It requires a custom test in CI or the publishing workflow - defined in the next section. |
That last row is the point of the table. A page can pass every platform tool above with a price six months out of date, because each of them checks the markup against a specification and none checks it against your pricing authority.
Documented Two operational details that catch teams out. The Rich Results Test fetches anonymously - This tool accesses the page as Google-InspectionTool (that is, not using your credentials, but as Google)
and Any resources that are behind a firewall or password-protected will not be available to the test
- so it cannot reach a protected staging environment directly. Google documents a workaround: if your page is running on your local machine without a public URL, or if it is hosted behind a firewall, you can still test the page by exposing a tunnel to your page for the testing tool
, naming ngrok, and framing it as another step in your release process
. And if you use the Rich Results Test's code-paste mode rather than a URL, Google recommends against it for JavaScript-generated markup: We recommend that you use the URL input instead of the code input because there are JavaScript limitations when using the code input (for example, CORS restrictions).
CI and Release Gates
Recommendation Everything in this section is a CoreAEX pass rule. Google documents none of it, and a reader should not defend any of these thresholds to an engineering team as a platform requirement.
The checks worth automating are the ones a human reviewer reliably fails to do consistently:
| Gate | What it checks | Fail the build? |
|---|---|---|
| Parses | Every JSON-LD block on the page is valid JSON | Yes |
| Vocabulary | Every type and property exists in the Schema.org release you target, and each property is declared on the type carrying it | Yes |
| Parity | Every price, currency and plan name in the JSON-LD matches the same field in the pricing source of truth | Yes - this is the gate the tools cannot provide |
| Required properties | The documented required properties for the feature you target are present | Yes |
| Server-rendered | The markup appears in the raw HTTP response, not only in the rendered DOM | Yes, if Merchant Center is in scope |
| No unknown price | No offer carries price: 0 unless the source of truth says the plan is free | Yes |
| Stale validity dates | No priceValidUntil or validThrough in the past | Yes |
| Pending/new-term inventory | Report which Schema.org pending or recently-added ("new") terms the build emits, so the list is known rather than discovered | No — warn |
| hreflang reciprocity | Each variant lists itself and every sibling, and every sibling points back | Yes, where variants exist |
Three notes on building these.
The vocabulary check should run against a downloaded Schema.org release file, not a live fetch. A build that fails because a third-party site was briefly unreachable is a build nobody trusts. Pin the release version, check it into the repository, and upgrade it deliberately.
Documented Do not write a rule that rejects ListPrice. Google's merchant listing documentation states two things about priceType in different sections of the same page, without cross-referencing each other: the detailed property table says You must set the priceType to the https://schema.org/StrikethroughPrice value (no other values are supported).
The pricing overview separately says Strikethrough prices set the priceType property to StrikethroughPrice (for a transition period, ListPrice is also allowed).
Neither passage explains the other. Prefer StrikethroughPrice in what you emit; do not build a gate that treats ListPrice as an error while the two sections leave the relationship unstated.
Parity is a diff, not a validation. The check is not "does this JSON-LD look right" but "does this JSON-LD equal the pricing model." Write it as a comparison between two data structures, run it on every build, and let it fail loudly. If your markup is genuinely generated from the source of truth, this test passes trivially and costs nothing - and the day it starts failing is the day someone has introduced a second copy of a price.
Post-Deployment Verification
Deployment is not the end of the process, and the gap between deploying and observing is longer than most release checklists assume.
Immediately after release, three checks are available and all of them are about your own infrastructure rather than Google's: fetch the raw HTTP response and confirm the markup is in it; run the Rich Results Test against the live production URL; and run the URL Inspection live test, which also gives you the rendered HTML and a screenshot. All three answer "did we ship what we meant to ship."
Documented What you cannot check immediately is anything about Google's stored state, because Google has not looked yet. The documentation is direct about this: Remember that Google does not crawl your page immediately after you publish a fix. Therefore Search Console (and Google Search) can continue to show an error for a page that you have fixed until the page is crawled again.
Google publishes several timings in this area and they measure different things. They are set out separately below for that reason - treating them as four estimates of one duration is how a release checklist acquires a deadline nobody can meet.
| What is being timed | Google's stated duration | Source scope |
|---|---|---|
| A page sitting in the render queue | "a few seconds, but it can take longer than that" | JavaScript rendering, before indexing |
| Noticing changes to your pages, generally | "in most cases it takes a few days to notice changes in your pages" | General debugging guidance |
| Indexing after a submitted request | "Indexing typically takes only a day or so, but can take much longer in some cases" | URL Inspection "Request indexing" |
| Fulfilling a crawl request | "a crawl request can take a week to fulfill" | Troubleshooting missing search results |
| Search Console issue validation | "typically takes up to about two weeks, but in some cases can take much longer" | The Validate Fix workflow, not rich result appearance |
None of these is a timeline for a rich result appearing. We located no Google statement giving one, which is consistent with Google declining to promise the outcome at all: Google does not guarantee that your structured data will show up in search results, even if your page is marked up correctly according to the Rich Results Test.
Recommendation So the sequence that works is: verify your own delivery on release day; re-check the URL Inspection indexed view after Google has recrawled, not on a fixed date; and use the rich result reports to watch template-wide health over weeks rather than to confirm a single deploy. Do not promise a stakeholder that a fix will be visible by a particular day.
Ownership and Change Control
Pricing markup drifts because it belongs to nobody in particular. The pricing model belongs to finance or product, the page belongs to marketing, the template belongs to engineering, and the JSON-LD belongs to whoever last touched it.
Recommendation Naming an owner for each of five things is usually enough to stop the drift. None of this is documented by any platform; it is an operating model.
- The pricing source of truth - who can change a price, and through what interface.
- The generation code - who owns the template that turns the pricing model into JSON-LD, and who reviews changes to it.
- The release gates - who maintains the CI checks above, and who is allowed to skip one.
- Post-deployment verification - who actually runs the day-one checks, and where the result is recorded.
- Incident response - who is called when a wrong price is discovered in production, and what the rollback is.
Two rules that make the model hold.
Recommendation Treat every price change as a governed release event, not an informal copy edit. What that event actually is depends on your architecture: a CMS publish, a billing-system sync, a configuration build, or an application deploy. Generated markup does not imply any one of those - it can be produced at request time, at build time, or on deploy. Whichever path yours takes, the source-of-truth, parity and post-publication checks run before the change counts as finished.
Documented The quarterly review should re-open the platform documentation, not just re-check the implementation. Three of the Google pages this article cites changed during 2026, and one rich result feature was discontinued outright. Google's changelog records it precisely: on May 8, 2026 Google added a deprecation notice to the FAQ rich result documentation because This feature will no longer appear in Google Search starting May 7, 2026
, and on June 15 it removed the documentation for the feature entirely. Note what was and was not withdrawn: Google discontinued its FAQ rich result, while FAQPage remains core Schema.org vocabulary with no deprecation notice of its own. An implementation that targeted Google's FAQ rich result in March could still emit valid FAQPage markup in June, but Google would no longer use that markup to produce the discontinued rich result.
What This Page Doesn't Settle
Which type to use, and how to model the commercial terms. Type selection and the property-by-property question of what Google documents consuming are settled by the type-selection page and the complex-pricing page in this cluster. This page assumes those decisions are made.
What to do when something is already broken. This page is about preventing drift and verifying a release. Diagnosing a price that is missing, wrong or stale in production - and working out which layer it broke at - is the troubleshooting page's job.
Whether any of this changes what AI answers say about your pricing. Google documents that its AI features use the ordinary Search technical requirements and that no special markup is needed for them, which is a statement about eligibility and crawling rather than about extraction. We located no equivalent documentation from OpenAI, Perplexity or Anthropic on JavaScript execution or structured-data use. Neither the documentation that exists nor the documentation that does not establishes whether pricing markup changes what a generated answer says about your prices. That is an empirical question and the subject of original CoreAEX research in progress.
Pricing markup drifting out of date?
If your prices live in more than one place and nobody is quite sure which one is authoritative, that's the conversation worth having before writing any more JSON-LD. Book a session.
Sources
Sources:
Format support, JSON-LD placement and the recommendation are from Intro to structured data (last updated December 10, 2025). Visibility, freshness and location requirements, and the non-guarantee statement, are from General structured data guidelines (last updated July 10, 2026). Rendering behaviour and the Product/Shopping caveat are from JavaScript-generated structured data (last updated December 10, 2025) and JavaScript SEO basics (last updated March 4, 2026). Currency URL structure and the priceType wording are from Merchant listing structured data (reviewed 29 August 2026; a specific last-updated date could not be independently confirmed for this page); hreflang reciprocity from Localized versions of your pages (last updated December 22, 2025); staging and crawl-lag guidance from Debugging your pages (last updated December 10, 2025). Google's position on its own AI features is from AI features and your website (last updated December 10, 2025); the FAQ rich result deprecation and removal are recorded in Google Search documentation updates (entries dated May 8 and June 15, 2026). Tool behaviour is from Search Console Help: Rich Results Test, URL Inspection tool, Rich result reports, Validation details and Missing features in Google Search results - none of these Search Console Help pages displays a last-updated date, so they were reviewed on August 29, 2026 and that is the only date we can give for them. Schema Markup Validator behaviour is from schema.org's validator documentation and its 2021 announcement. The Merchant Center server-HTML requirement and the personalisation exclusion are Merchant Center documentation, not Search: Set up structured data and About structured data markup. Vocabulary validation of the example used the published Schema.org release version 30.0 (March 19, 2026).
About the author
Zarko Zivkovic is the founder of CoreAEX, building technical SEO, AEO, and AI-visibility systems for B2B SaaS companies. Connect on LinkedIn.