SEO Schema Audit
Structured data only works when it describes what a visitor can actually see. Google's policy is explicit: schema must reflect content visible in the served HTML. Most schema failures come from violating this in ways that look fine in the React tree.
Render through growth-report-format. Open it alongside this skill.
Tools this skill calls
| Need | Tool | Key arguments |
|---|---|---|
| Declared schema types + FAQ schema-vs-visible comparison | mcp__google_search_mcp__audit_page |
urls (1–20), response_format: "json" |
| Whether Google has indexed the page and what it saw | mcp__google_search_mcp__gsc_url_inspect |
site_url, inspection_url |
Default property for Agentman work: GSC sc-domain:agentman.ai. Any other site: resolve the target first.
audit_page is the fastest path to failure mode 1. It returns:
schemaTypes— every distinct@typeon the page,@graphflattenedfaq—{schemaCount, visibleCount, missingFromPage}when FAQPage schema exists,nullwhen it does not__INVALID_JSON__insideschemaTypeswhen a ld+json block failed to parse (search engines ignore those entirely)
visibleCount below schemaCount is the violation. missingFromPage names the exact questions.
Provenance rules
- Every finding names the call that produced it.
audit_pagereturns no performance data — no CTR, impressions, or position. Do not attach performance claims to schema findings.- A failed or empty call is reported, never filled in from memory.
- Never cite a prior audit that is not in the current context.
What audit_page cannot settle
- It reads server-rendered HTML only — which is the right lens for this skill, since that is what Google parses.
- It does not resolve ancestor visibility: a question inside a hidden container can read as visible. So
visibleCount == schemaCountmeans "nothing obviously wrong", not proof. When the stakes are high (a page you are about to submit for rich results), confirm by eye. - It does not validate Offer correctness, breadcrumb ordering, or required-property completeness. Those are manual checks below.
The three failure modes to hunt for
1. Schema describing invisible content
Run audit_page on the URL set. Any page where faq.visibleCount < faq.schemaCount is failing this test, and faq.missingFromPage lists the questions Google cannot see.
The classic trap: accordion components unmount collapsed panels. Radix UI, Headless UI, and most disclosure components remove closed content from the DOM entirely — so a FAQ page emits FAQPage schema for answers that are not in the HTML. Fix by force-mounting the content and hiding it with CSS instead (Radix: forceMount on AccordionContent plus data-[state=closed]:hidden). Make this an opt-in prop on the shared accordion so other usages keep their animations.
For non-FAQ schema types, audit_page reports which types are present but does not verify each claim. Fetch the page and confirm every referenced string exists in the same document.
2. Fabricated offers
Every Offer must state a real, current price:
- Never emit
"price": "0"or invent a "free trial" because a template defaulted that way. If the product costs money, a $0 offer is a misrepresentation Google can act on. - If pricing is per-unit or usage-based, use the real entry price with
priceCurrencyand describe the unit. - If a product is only sold as part of a suite with no standalone price, omit the Offer entirely rather than fabricate one.
- Pull every price from the site's single pricing module — the same import the visible pricing UI uses — so schema can never drift from displayed prices.
3. Hand-written schema drifting from copy
Never maintain schema as a separate literal. Export the content array (FAQ items, feature lists, prices) from the component that renders it, and build the JSON-LD from that same array. One source; drift becomes impossible.
Schema selection by page type
- Product / agent / feature page:
SoftwareApplicationorProduct(+ realOfferif priced standalone) - Pricing page:
ProductwithOfferper tier from the pricing module - Pages with a visible FAQ:
FAQPagefrom the exported FAQ array - Any page below the homepage:
BreadcrumbListmatching the visible breadcrumb/nav path - Directory detail pages:
BreadcrumbList+FAQPagewhere a real FAQ renders
Verification
audit_pageacross the affected URLs — confirmschemaTypesholds what you expect, no__INVALID_JSON__, andfaq.visibleCount == faq.schemaCount.- Fetch the page for claims outside the FAQ block and confirm each referenced string is in the served document.
- Run the URL through Google's Rich Results Test.
gsc_url_inspectto confirm Google has actually indexed the current version.- Re-check after any component library upgrade — unmount behavior is exactly the kind of thing a minor version changes silently.
Output — the Findings section
Per page: URL, schema types present, FAQ counts, the specific violation, and the fix. Use the structural-findings table from growth-report-format (Page | Issue | Severity | Fix). Severity: a FAQ visibility violation or invalid JSON-LD is High; a missing optional property is Low.
Worked example
Input: a review found eight product pages emitting Offer schema with "price": "0" and a "free trial" that didn't exist — the real product billed $25 per submission. Separately, three pages emitted FAQPage schema whose answers were absent from the served HTML: the accordion library unmounted every closed panel, so only the questions existed in the document.
Output:
- Offers rebuilt from the shared pricing module:
"price": "25.00","priceCurrency": "USD", unit described in the offer name. Suite-priced products with no standalone price lost theirOfferentirely rather than getting an invented one. - The shared accordion component gained an opt-in
alwaysRenderContentprop (forceMount+data-[state=closed]:hidden), so FAQ answers exist in the HTML while collapsed — and every other accordion on the site kept its original unmount behavior and animations. - Each page's FAQPage JSON-LD was rebuilt from the same exported array the accordion renders, eliminating the drift that caused the mismatch in the first place.
Today that first diagnosis is one audit_page call: the three bad pages would have returned faq.visibleCount of 0 against a schemaCount of 5, with every question listed in missingFromPage.