# Schema mismatch lab and audit worksheet

Pack version 1.0 · 16 September 2026. Original lab version 1.0.

Run the fictional HarborDesk example, then use the audit worksheet to compare each structured assertion with the approved facts for one real page variant. The script tests only its own localhost page; it is not a website scanner or general schema validator.

Guide: https://rankecho.io/resources/find/issues/schema-content-mismatch

Download the runnable file: https://rankecho.io/downloads/schema-content-mismatch-lab-v1.mjs

Save the downloaded file in an empty directory and rename it to example.mjs. Run node example.mjs with Node 24. No packages, account or external service are required. The lab contacts only its own loopback server.

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.

A local response test with a fixed-shape extractor. Use the audit worksheet below for your own site.

## 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.

| Check | Before | After |
| --- | --- | --- |
| Approved visible price | 39.00 USD; one-time license | Unchanged |
| app-template → offers.price | 29.00; conflicts with the page | 39.00; currency remains USD |
| stale-plugin → offers.price | 19.00; conflicts with the page and template | Redundant app record removed |
| stale-plugin → aggregateRating | 4.9 / 212; no supporting visible rating | Unsupported assertion removed |
| Organization and page controls | Separate organization, self-canonical, 401/404 controls | Preserved |


## 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.

- [Read Google's structured-data content guidelines](https://developers.google.com/search/docs/appearance/structured-data/sd-policies)
- [Resolve unsupported or disputed visible facts](https://rankecho.io/resources/fix/fact-sourcing)

## 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 field | What to record |
| --- | --- |
| Capture | Exact URL, date, locale, currency, selection, signed-out state and saved response/DOM/screenshot |
| Item and assertion | Item ID or relationship, field path, current value and matching visible fact |
| Source | Script/element location, verified emitting component and responsible maintainer |
| Decision | Confirmed mismatch, matching, legitimate separate item/offer, or unresolved; approved replacement and reason |
| Acceptance | Before/after evidence, unchanged controls, validation output and rollback owner |

- [Capture initial and rendered content consistently](https://rankecho.io/resources/find/issues/javascript-only-evidence)
- [Clarify plan and billing details before comparing prices](https://rankecho.io/resources/fix/pricing-page-for-ai-search)

## 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.

- [Review multiple-item guidance](https://developers.google.com/search/docs/appearance/structured-data/sd-policies)
- [Resolve identity and relationship conflicts](https://rankecho.io/resources/fix/entity-disambiguation)

## 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.

- [Check Schema.org's price definition](https://schema.org/price)
- [Check the currency field](https://schema.org/priceCurrency)

## 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 layer | What to establish |
| --- | --- |
| Your content review | Each assertion agrees with approved facts for the captured item and variant. |
| Schema Markup Validator | Inspect Schema.org vocabulary and markup; it does not apply Google's feature-specific checks. |
| Rich Results Test | Inspect the Google features detected and their reported requirements; preserve the dated output. |
| Production response | Recheck actual delivered markup, page behavior and controls after shipment, including relevant cache and browser variants. |
| Search evidence | Record later crawl/index or search-appearance observations separately from the repair result. |

- [Choose the appropriate structured-data testing tool](https://developers.google.com/search/docs/appearance/structured-data)

## 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.

- [Review Google's SoftwareApplication requirements](https://developers.google.com/search/docs/appearance/structured-data/software-app)

## 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.

- [Inspect the sample fix](https://rankecho.io/sample-fix)
- [Evaluate the Fix Engine](https://rankecho.io/product/fix-engine)
- [Review Google's AI-feature guidance](https://developers.google.com/search/docs/appearance/ai-features)
- [Separate discovery, visits and conversions](https://rankecho.io/resources/prove/citation-to-session-to-conversion)

## Your page capture

Keep the context identical across the visible fact and structured assertion. Leave missing observations unresolved. Do not change a price until its commercial owner confirms the intended offer.

- Exact URL and capture time: [Enter your response]
- Locale, currency, product or plan and purchase option: [Enter your response]
- Signed-out state, consent and relevant page selections: [Enter your response]
- Response status, headers and saved raw HTML location: [Enter your response]
- Matching rendered DOM and screenshot locations: [Enter your response]
- Approved fact, approving owner and source of truth: [Enter your response]

## One assertion record

Copy this record for each assertion. Do not infer a conflict from the number of script blocks: preserve legitimate separate items, related entities and distinct offers. A script ID is a clue; verify the emitting component with its maintainer.

| Field | Your record |
| --- | --- |
| Item identity and relationship to the captured offer | [Enter your response] |
| Exact field path and script or element location | [Enter your response] |
| Current structured value | [Enter your response] |
| Approved visible fact and matching context | [Enter your response] |
| Verified template, plugin, CMS field or script and maintainer | [Enter your response] |
| Decision: mismatch, matching, legitimate separate item or unresolved | [Enter your response] |
| Proposed change, reason and approval | [Enter your response] |
| Protected controls, previous configuration and rollback owner | [Enter your response] |

## Repair acceptance

Assign owners and save dated output for the checks you actually perform. The local example has not completed these checks for your website. Keep unsupported ratings out of your markup; correcting a price does not establish Google feature eligibility or search appearance.

| Validation layer | Owner | Evidence location and result |
| --- | --- | --- |
| Your content review | [Assign owner] | [Pending evidence] |
| Schema Markup Validator | [Assign owner] | [Pending evidence] |
| Rich Results Test | [Assign owner] | [Pending evidence] |
| Production response | [Assign owner] | [Pending evidence] |
| Search evidence | [Assign owner] | [Pending evidence] |

Inspect a sample fix: https://rankecho.io/sample-fix. Evaluate the Fix Engine: https://rankecho.io/product/fix-engine for a bounded correction with human review and manual shipment. Full-page writing is a separate content add-on: https://rankecho.io/pricing/content, not included with the visibility subscription.

## Complete original runnable example

Save only the code below as example.mjs if using this Markdown file instead of the separate download. It is identical to the original Copy artifact.

```javascript
// 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));
}
```
