Home / Resources / Find / Schema Markup Not Matching Your Page? Find and Fix the Source
AI Search Intelligence

Schema Markup Not Matching Your Page? Find and Fix the Source

The short answer

Compare the exact public page with every structured-data record describing the same item. Record the conflicting field and the template, plugin or script that emits it; correct that source and recheck the delivered page. Start with the tested example below: two old prices and an unsupported rating are removed while the approved visible content stays the same.

Run the schema mismatch example

Save this complete example as example.mjs and run node example.mjs with Node 24. It needs no packages. HarborDesk, its prices and its rating are fictional test data.

// Fictional, dependency-free local example. Run with Node 24: node example.mjs
// The extractor understands only this fixture's HTML and inline JSON-LD shape.
import assert from 'node:assert/strict';
import { createServer } from 'node:http';

const url = 'https://harbordesk.example/apps/export';
const appId = `${url}#app`;
const body = '<main><h1>HarborDesk Export</h1><p>Desktop CSV export utility for Windows and macOS.</p><p>One-time license: <span id="amount">39.00</span> <span id="currency">USD</span>.</p></main>';
const organization = { '@context': 'https://schema.org', '@type': 'Organization', '@id': 'https://harbordesk.example/#org', name: 'HarborDesk', url: 'https://harbordesk.example/' };
const app = price => ({ '@context': 'https://schema.org', '@type': 'SoftwareApplication', '@id': appId, name: 'HarborDesk Export', url, applicationCategory: 'BusinessApplication', operatingSystem: 'Windows, macOS', offers: { '@type': 'Offer', price, priceCurrency: 'USD' } });
const script = (id, value) => `<script id="${id}" type="application/ld+json">${JSON.stringify(value)}</script>`;
function html(mode) {
  const core = app(mode === 'before' ? '29.00' : '39.00');
  // Deliberately unsupported sample rating, removed in the corrected state.
  const stalePlugin = { ...app('19.00'), aggregateRating: { '@type': 'AggregateRating', ratingValue: 4.9, ratingCount: 212 } };
  return `<!doctype html><html lang="en"><head><title>HarborDesk Export</title><link rel="canonical" href="${url}">${script('app-template', core)}${mode === 'before' ? script('stale-plugin', stalePlugin) : ''}${script('site-identity', organization)}</head><body>${body}<footer>HarborDesk</footer></body></html>`;
}
function inspect(document) {
  const main = document.match(/<main>[\s\S]*?<\/main>/)?.[0] ?? '';
  const amount = main.match(/id="amount">([^<]+)</)?.[1];
  const currency = main.match(/id="currency">([^<]+)</)?.[1];
  assert.ok(amount && currency, 'Fixture visible price is missing');
  const records = [...document.matchAll(/<script id="([^"]+)" type="application\/ld\+json">([\s\S]*?)<\/script>/g)].map(([, emitter, text]) => ({ emitter, value: JSON.parse(text) }));
  const apps = records.filter(({ value }) => value['@id'] === appId);
  const prices = apps.map(({ emitter, value }) => ({ emitter, price: value.offers.price, currency: value.offers.priceCurrency }));
  const mismatches = prices.filter(value => value.price !== amount || value.currency !== currency);
  const unsupportedRatings = apps.filter(({ value }) => 'aggregateRating' in value).map(value => value.emitter);
  return {
    visiblePrice: `${amount} ${currency}`, appRecords: apps.length, prices,
    priceMismatches: mismatches.map(value => value.emitter),
    conflictingPrices: new Set(prices.map(value => `${value.price} ${value.currency}`)).size > 1,
    unsupportedRatings,
    organization: records.find(({ value }) => value['@id'] === organization['@id'])?.value,
  };
}
let mode = 'before';
const server = createServer((req, res) => {
  res.setHeader('Content-Type', 'text/html; charset=utf-8');
  const path = new URL(req.url, 'http://localhost').pathname;
  if (path !== '/apps/export') {
    res.statusCode = path === '/workspace' ? 401 : 404;
    res.setHeader('X-Robots-Tag', 'noindex');
    return res.end(path === '/workspace' ? 'Sign in required' : 'Not found');
  }
  res.end(html(mode));
});
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
const origin = `http://127.0.0.1:${server.address().port}`;
const read = async (path, agent = 'schema-example') => {
  const response = await fetch(origin + path, { headers: { 'User-Agent': agent } });
  return { status: response.status, robots: response.headers.get('x-robots-tag'), html: await response.text() };
};
try {
  const before = await read('/apps/export');
  const beforeResult = inspect(before.html);
  assert.equal(before.status, 200);
  assert.deepEqual(beforeResult.priceMismatches, ['app-template', 'stale-plugin']);
  assert.equal(beforeResult.conflictingPrices, true);
  assert.deepEqual(beforeResult.unsupportedRatings, ['stale-plugin']);
  const excludedBefore = await read('/workspace');
  const missingBefore = await read('/missing');
  mode = 'after';
  const after = await read('/apps/export');
  const afterResult = inspect(after.html);
  assert.equal(after.status, 200);
  assert.equal(after.robots, null);
  assert.equal(afterResult.appRecords, 1);
  assert.deepEqual(afterResult.priceMismatches, []);
  assert.equal(afterResult.conflictingPrices, false);
  assert.deepEqual(afterResult.unsupportedRatings, []);
  assert.deepEqual(afterResult.organization, beforeResult.organization);
  assert.equal(after.html.match(/<main>[\s\S]*?<\/main>/)[0], body);
  assert.equal(before.html.match(/<main>[\s\S]*?<\/main>/)[0], body);
  const canonical = document => document.match(/<link rel="canonical"[^>]+>/)[0];
  assert.equal(canonical(before.html), canonical(after.html));
  assert.deepEqual(await read('/workspace'), excludedBefore);
  assert.deepEqual(await read('/missing'), missingBefore);
  assert.equal(excludedBefore.status, 401);
  assert.equal(missingBefore.status, 404);
  assert.equal(excludedBefore.robots, 'noindex');
  assert.equal(missingBefore.robots, 'noindex');
  assert.deepEqual(await read('/apps/export', 'Googlebot'), after);
  assert.throws(() => inspect(after.html.replace('"price":"39.00"', '"price":oops')), SyntaxError);
  mode = 'before';
  assert.deepEqual(await read('/apps/export'), before);
  console.log(JSON.stringify({
    node: process.version,
    before: beforeResult, after: afterResult,
    visibleBodyUnchanged: true, canonicalUnchanged: true,
    malformedJsonRejected: true, unrelatedOrganizationPreserved: true,
    excludedStatus: excludedBefore.status, missingStatus: missingBefore.status,
    userAgentResponseEqual: true, localRollbackEqual: true,
  }, null, 2));
} finally {
  await new Promise(resolve => server.close(resolve));
}
Version 1.0 · A local response test with a fixed-shape extractor. Use the audit worksheet below for your own site.
Quick actions

What does the tested example show?

A fictional HarborDesk Export listing offers a one-time desktop license for 39.00 USD. Its application template still emits 29.00 USD, while a second source labelled stale-plugin emits 19.00 USD and a 4.9 rating from 212 ratings. The fixed page body contains no ratings. All three JSON-LD blocks parse, including a separate Organization record, so JSON syntax alone misses the content problem.

The correction updates the application template to 39.00 USD and removes the redundant stale app record. The separate organization remains. These labels model two emitting sources; no real CMS or plugin was installed or tested.

CheckBeforeAfter
Approved visible price39.00 USD; one-time licenseUnchanged
app-template → offers.price29.00; conflicts with the page39.00; currency remains USD
stale-plugin → offers.price19.00; conflicts with the page and templateRedundant app record removed
stale-plugin → aggregateRating4.9 / 212; no supporting visible ratingUnsupported assertion removed
Organization and page controlsSeparate organization, self-canonical, 401/404 controlsPreserved

What counts as a content mismatch?

A mismatch means a structured assertion describes a different fact from the one established for that item and page variant. Examples include an old price, the wrong currency, an expired offer, or a review claim with no supporting page content. Google requires structured data to represent relevant, current, visible content; its general guidance also explains why automated technical checks can miss quality problems.

First establish which fact is correct. A page can have outdated copy as well as outdated markup. If the commercial owner has not confirmed the price, stop the price correction and resolve that uncertainty. The worked example deliberately fixes the visible body so it can isolate the markup change.

How do you audit one real page?

Choose one public URL and one precise context: signed out, selected plan or product, currency, locale and purchase option. Save the timestamp, response status and headers, raw HTML, and a screenshot of the displayed facts. If consent, JavaScript or a selector changes the page, capture the rendered DOM in that same context. A USD monthly option and an EUR annual option cannot be compared as though they were one offer.

Find all relevant JSON-LD blocks, including arrays and @graph nodes, and inspect Microdata or RDFa if used. Locate the item through its identity, URL and offer relationship. Record the exact field path and script or element location. Trace the output back to a template, plugin configuration, CMS field, tag manager or build step; a script ID is a clue, not proof of ownership.

Use one worksheet row per assertion. Mark a row unresolved when the visible fact, entity association or emitting source is uncertain. This avoids turning a missing observation into a confirmed defect.

Worksheet fieldWhat to record
CaptureExact URL, date, locale, currency, selection, signed-out state and saved response/DOM/screenshot
Item and assertionItem ID or relationship, field path, current value and matching visible fact
SourceScript/element location, verified emitting component and responsible maintainer
DecisionConfirmed mismatch, matching, legitimate separate item/offer, or unresolved; approved replacement and reason
AcceptanceBefore/after evidence, unchanged controls, validation output and rollback owner

Should you delete duplicate JSON-LD?

Inspect the relationships before deleting anything. Google permits multiple items on a page, including linked items. Two blocks can describe different things, and repeated IDs can contribute information about one entity. The defect in this example is contradictory pricing and an unsupported rating for the same app; the number of script blocks is not the diagnosis.

Here the team chooses the application template as the sole emitter for this app listing because the second source is redundant. On your site, confirm which component owns each field and preserve legitimate organizations, breadcrumbs, related items and distinct offers. Test the full delivered output after disabling a plugin feature: it may have responsibilities beyond the record you are repairing.

How should you correct the price fields?

Use the approved amount for the offer being described. Schema.org separates price from priceCurrency: the example uses the string 39.00 and USD, with the decimal point inside the amount and no currency symbol. Compare amount, currency and purchase context together; a numerically matching amount in the wrong currency is still a different offer.

This example covers one one-time software license. Subscriptions, per-seat units, trial terms, price ranges, taxes and regional offers need their own field mapping and current feature guidance. Keep the billing explanation visible and get the commercial owner's decision before adapting a simple price field to those cases.

Which checks confirm the repair?

Run the copied example in an empty folder with Node 24.19.0, the version used for the recorded result. It prints before and after observations and exits with an assertion error if a required comparison fails. Inspect the JSON to see each emitting source and its amount. No dependency installation or account is required.

The executed test made localhost HTTP requests, parsed the fixture's inline records, preserved the main body and canonical, retained the unrelated organization, rejected deliberately malformed JSON, and checked unchanged 401/noindex and 404/noindex responses. A synthetic Googlebot string received the same response. Restoring the local mode reproduced the earlier response.

The extractor only understands this fixture's fixed HTML and JSON shape. Its rating check is specific to a body known to contain no ratings. It does not expand JSON-LD, infer CSS visibility, execute client scripts or validate arbitrary websites. The status branches are fixed controls, not a working login system. A browser, production cache, real CMS and Google testing service were not exercised.

Validation layerWhat to establish
Your content reviewEach assertion agrees with approved facts for the captured item and variant.
Schema Markup ValidatorInspect Schema.org vocabulary and markup; it does not apply Google's feature-specific checks.
Rich Results TestInspect the Google features detected and their reported requirements; preserve the dated output.
Production responseRecheck actual delivered markup, page behavior and controls after shipment, including relevant cache and browser variants.
Search evidenceRecord later crawl/index or search-appearance observations separately from the repair result.

Is the corrected software app eligible for rich results?

The corrected fixture still lacks a rating or review. Google's SoftwareApplication feature requires one, alongside the app name and offers.price. Correcting the price therefore does not complete the feature requirements. Keep unsupported ratings out of the markup and review the current app documentation before deciding what the page can honestly provide.

Treat the checklist result, an external validator result and actual search appearance as separate records. The example establishes a specific content correction; it did not receive a passing Rich Results Test or appear in Google Search.

How does this become a reviewable RankEcho fix?

Package one URL, the approved fact, each conflicting assertion, the verified emitting source, and the proposed diff. Name the maintainer who will ship it. Acceptance should cover the intended correction and the page features that must survive; retain the previous configuration or commit and a way to confirm restoration.

Inspect the sample fix, then evaluate the paid Fix Engine for organizing a bounded correction with human review and manual shipment. CMS settings and code changes remain with your implementation team. The workflow does not provide a universal schema validator or a native Search Console connection.

Free audits check Perplexity and Gemini once per prompt. Paid and trialing accounts add ChatGPT, Claude, and Google AI Overviews when configured for the account, for up to 5 engines. RankEcho does not currently run Microsoft Copilot checks.

Google's AI features require no special AI schema. After verifying the repair, measure search clicks, identified AI referrals and product actions separately; correcting a mismatch does not establish a traffic increase or its cause.

Frequently asked questions

Can JSON-LD parse successfully and still be wrong?

Yes. The two old prices in the example parse successfully but disagree with the approved visible price. Parsing tests syntax; compare the actual claims separately.

Should every page have only one schema block?

No. Inspect what each record describes. The example removes one redundant app source and preserves a separate organization record.

Can I scan my website with the copied example?

The example tests its own fictional localhost page. Use the worksheet and appropriate validation tools for a real site; the fixed-shape extractor is not a general website scanner.

What if the visible page price is wrong?

Confirm the intended offer with its commercial owner before changing either layer. Then correct all affected representations and record which facts were approved.

Sources reviewed

Provider eligibility and measurement claims below were checked against primary documentation. These records do not establish a universal selection formula, causation, or a guaranteed ranking, impression, recommendation, or citation.

6 claim-level source records
Checked 2026-09-12 · Primary-source diagnostic review · Confidence is recorded per claim.
Claim reviewedOfficial sourceReview record
Google requires relevant, current structured data that represents visible content. Technical validation cannot establish every quality condition, and multiple related items may appear on one page.Google: general structured data guidelinesChecked 2026-09-12 · Primary documentation reviewed September 12, 2026 · Supports content agreement and the multiple-item distinction; the example is an original local experiment. · Confidence: High
Google distinguishes its Rich Results Test from the Schema Markup Validator, which checks Schema.org markup without Google's feature-specific validation.Google: structured data testing toolsChecked 2026-09-12 · Primary documentation reviewed September 12, 2026 · Neither public testing service was run on the fictional example; their roles are instructions for the reader. · Confidence: High
Google's SoftwareApplication feature requires a name, offers.price, and a rating or review. Currency is recommended for paid apps; meeting requirements does not guarantee display.Google: software app structured dataChecked 2026-09-12 · Primary documentation reviewed September 12, 2026 · The corrected example intentionally omits the unsupported rating and therefore lacks the required rating/review for this feature. · Confidence: High
Schema.org defines price as an offer or price-specification amount and recommends a decimal point and a separate currency property instead of a currency symbol in the value.Schema.org: priceChecked 2026-09-12 · Primary documentation reviewed September 12, 2026 · Supports the fictional 39.00 amount; it does not validate the app, its license terms, or Google eligibility. · Confidence: High
Schema.org defines priceCurrency as the currency for the price and uses standard currency codes such as USD.Schema.org: priceCurrencyChecked 2026-09-12 · Primary documentation reviewed September 12, 2026 · Supports the explicit currency field. The fixture does not test conversions or localized offers. · Confidence: High
Google's AI features use ordinary Search eligibility and require no special AI schema. Eligibility does not guarantee inclusion.Google: AI features and your websiteChecked 2026-09-12 · Primary documentation reviewed September 12, 2026 · Google-specific guidance; the example measures no AI answer, citation, search visit, or business outcome. · Confidence: High
Evaluate the Fix Engine →
Last updated 2026-09-12 · RankEcho · Operated by Nexus Decision Systems LLC