Technical SEO Audit WorkshopRunning the crawl and gathering evidence · Lesson 6 of 14
Rendering and structured data checks: raw vs rendered, markup vs visible
Video lecture
Rendering and structured data checks: raw vs rendered, markup vs visible
The narrated lecture is in production
Every chapter is scripted and ready. Browse the chapters and read the full transcript now — the video will appear here when it’s published.
Chapters
Transcript of the narration, chapter by chapter.
0:00 Rendering and structured data checks
Server-side rendering is a big improvement, but it isn't a guarantee. Components can still fetch content on the client. Hydration can change canonicals. And structured data can quietly drift away from the price on the page. In this lecture you'll run three checks on Kiran Home's key templates: raw versus rendered parity, JSON-LD versus visible price and availability, and eligibility in Google's tools, and you'll turn the results into hard numbers for your root causes.
0:33 Check 1: raw vs rendered
Check one: raw versus rendered parity. Take twenty URLs per template, product, category and journal, and compare the raw crawl with the rendered crawl, or use the Playwright diff script from the Technical SEO Mastery course. Compare the canonical, the robots meta, the main content, the internal links and the JSON-LD. Anything that differs between raw and rendered is a risk, and anything that exists only after interaction effectively doesn't exist for crawlers.
1:05 Kiran Home findings (illustrative)
For Kiran Home, the illustrative results bring two new findings. Journal posts have a canonical that changes after hydration: the server sends one market's canonical, then a locale cookie switches it on the client. That feeds root cause one. And on product pages, the customers also bought links only exist after interaction, so they provide no discovery value. Category pages confirm what we already knew: only the first twenty-four products and no pagination links in the raw HTML.
1:39 Check 2: JSON-LD vs visible
Check two: structured data versus visible facts. The script in the lesson text fetches each product URL, reads the visible price from the page's price element, then parses every JSON-LD block, handling graphs and lists, and finds the Product offer's price, currency and availability. It flags invalid JSON-LD, missing Product markup, and price mismatches. Run it on a sample that includes variants and sale items, because that's where bugs hide.
2:09 Results (illustrative)
Kiran Home's illustrative results: of sixty sampled products, eighteen have no offers at all, because the template omits offers when a product has variants. And five show a sale price on the page while the JSON-LD still has the old price. That's root cause six with hard numbers, and it's exactly the kind of inconsistency that also leads AI assistants to quote wrong prices.
2:37 Check 3: eligibility
Check three: eligibility and reports. Run one URL per template through the Rich Results Test, using its rendered view, and note errors versus warnings. Errors block eligibility; warnings are optional properties. Check Search Console's product and merchant listing reports and the breadcrumbs report for trend changes since the relaunch. And don't recommend markup for features that no longer exist. FAQ rich results ended in May twenty twenty-six, so existing FAQ markup is harmless, but it's not a recommendation.
3:11 Valid ≠ correct
Here's a subtle point worth remembering. Valid isn't the same as correct. The Rich Results Test tells you whether markup is structurally valid and eligible. It doesn't tell you whether the price is right, or whether two blocks contradict each other. That's why check two exists. Your job as an auditor is to verify facts, not just syntax.
3:36 Example 2: three plugins, three sets of hours (illustrative)
Worked example two, a different client, illustrative. An Abu Dhabi clinic group has three plugins each outputting clinic markup with different opening hours. The Rich Results Test shows no errors, because all three blocks are valid. But the facts conflict. So the finding isn't invalid markup. It's conflicting facts. The fix: keep one source, generated from the same fields as the visible opening hours, and align the Business Profile hours too.
4:07 Write it up
How do you write these up? Each check produces a finding with a count, examples and a root-cause link. Journal canonical changes after hydration: twenty of twenty sampled journal URLs, linked to root cause one. Offers missing on variant products: eighteen of sixty, linked to root cause six. Stale sale prices: five of sixty, also root cause six. Customers also bought links only after interaction: a discovery finding linked to root cause four. Numbers and examples make tickets easy to prioritise.
4:42 Check 4: market parity
Because Kiran Home serves four markets, there's a fourth check: market parity. Take one product in each market folder and confirm that the canonical is its own URL, the hreflang set lists all four markets plus x-default, the visible currency matches the market, whether that's rupees, dirhams or pounds, and the JSON-LD price currency matches the visible currency. On the relaunched storefront, illustratively, the Arabic UAE template shows prices in dirhams but its JSON-LD says pounds, inherited from the UK template. Another example for root cause six, and a very easy ticket to write.
5:23 Watch me do it: 60-product markup check
Watch me do it. I'll run the structured data check on sixty Kiran Home products. Step one: I build the sample. Twenty regular products, twenty with variants, and twenty currently on sale, from all four markets. Step two: I run the price-check script. It prints one row per URL: OK, mismatch, no product markup, or invalid JSON-LD. Step three: I sort by status. Eighteen rows say no product markup, and they're all variant products. I open one and view source: there's a Product block, but no offers property at all. So the template drops offers when variants exist. Step four: five rows say mismatch, all sale items. The visible price is the sale price; the JSON-LD still has the full price. Step five: I check one URL per market for currency. The Arabic UAE product shows dirhams on the page, but the JSON-LD says pounds. Step six: I run one product and one journal URL through the Rich Results Test. Product eligibility is limited by the missing offers. Step seven: I add all of this to the summary table with counts, root-cause links and proposed tickets. And I list the existing FAQ markup on journal posts as a notable non-issue.
6:50 The summary table
Let's pull the checks into one summary table, because that's what goes into the workbook. Journal canonicals changing after hydration: twenty out of twenty sampled, root cause one, ticket: render the canonical from the URL path on the server. Customers also bought links only after interaction: all product templates, root cause four, ticket: server-render related links as anchors. Offers missing on variants and stale sale prices: root cause six, ticket: always output offers from live data. And the Arabic template's currency: root cause six, ticket: currency from market configuration. Every row has a count, a cause and a fix.
7:33 Mistakes, recap, try this now
Common mistakes. Treating valid in the Rich Results Test as correct. Checking one URL per template when the bug only appears on variants or sale items. And recommending markup for rich result types that no longer exist. Recap: check parity between raw and rendered, verify markup against visible facts on a realistic sample, confirm eligibility and trends, and write findings with counts. Try this now: run the price-check script on twenty of your own product or service pages, including at least five on sale or with variants.
Why this is its own step
Kiran Home runs a headless React storefront with server-side rendering. SSR reduces risk, but it doesn't eliminate it: components can still fetch content on the client, hydration can change canonicals, and JSON-LD can drift from the visible price. This lesson checks the three templates that matter most (product, category, journal) for raw vs rendered parity and structured data accuracy.
Check 1: raw vs rendered parity per template
Use the rendered and raw crawls from Module 1 (or the Playwright diff script from Technical SEO Mastery, Lesson 1.2) on 20 URLs per template. Compare:
| Element | Product | Category | Journal |
|---|---|---|---|
| Canonical identical raw vs rendered | Yes | Yes | No — rendered canonical switches market via cookie |
| Robots meta identical | Yes | Yes | Yes |
| Main content in raw HTML | Yes | Only first 24 products | Yes |
| Internal links in raw HTML | Breadcrumbs yes; "Customers also bought" no | Pagination no | Yes |
| JSON-LD in raw HTML | Yes | n/a | Yes |
Illustrative results. Two new findings: journal canonicals change after hydration (feeds RC1), and "Customers also bought" links exist only after interaction (discovery value lost).
Check 2: JSON-LD vs visible price and availability
# pip install requests beautifulsoup4
import json, re, requests
from bs4 import BeautifulSoup
def check(url):
s = BeautifulSoup(requests.get(url, timeout=20).text, "html.parser")
visible = s.select_one(".product-price__amount")
visible = re.sub(r"[^\d.]", "", visible.get_text()) if visible else None
offers = []
for tag in s.find_all("script", type="application/ld+json"):
try:
data = json.loads(tag.string or "{}")
except json.JSONDecodeError:
return url, "INVALID JSON-LD", None, None
items = data.get("@graph", [data]) if isinstance(data, dict) else data
for it in items:
if isinstance(it, dict) and it.get("@type") == "Product":
o = it.get("offers") or {}
o = o[0] if isinstance(o, list) else o
offers.append((str(o.get("price")), o.get("priceCurrency"), o.get("availability")))
if not offers:
return url, "NO PRODUCT MARKUP", visible, None
price, cur, avail = offers[0]
status = "OK" if visible and price and abs(float(price) - float(visible)) < 0.01 else "MISMATCH"
return url, status, visible, (price, cur, avail)
for u in open("product_sample.txt").read().split():
print(check(u))For Kiran Home (illustrative), 18 of 60 sampled products have offers missing entirely — the template omits it when a product has variants — and 5 show a sale price visibly while JSON-LD still has the old price. That's RC6 with hard numbers.
Check 3: eligibility and reports
- Run one URL per template through the Rich Results Test (rendered view) and note errors vs warnings.
- Check Search Console's Product snippets / Merchant listings and Breadcrumbs reports for trend changes since the relaunch.
- Don't recommend markup for retired features (for example FAQ rich results ended in May 2026) — note existing FAQPage markup as harmless.
Check 4: market parity for international templates
Because Kiran Home serves four markets, repeat checks 1 and 2 for one product in each market folder. Confirm for each market version: the canonical is its own URL, the hreflang set lists all four markets plus x-default, the visible currency matches the market (PKR, AED or GBP), and the JSON-LD priceCurrency matches the visible currency. On the relaunched storefront (illustrative), the /ar-ae/ template renders prices in AED but its JSON-LD says GBP — inherited from the UK template — which becomes another RC6 example.
Summarising the checks
| Check | Result (illustrative) | Root cause | Ticket |
|---|---|---|---|
| Journal canonical changes after hydration | 20/20 sampled | RC1 | Canonical from URL path on server |
| "Customers also bought" only after interaction | All product templates | RC4 | Server-render related links as anchors |
| Offers missing on variant products | 18/60 | RC6 | Always output offers (lowest price / AggregateOffer) |
| Stale sale prices in JSON-LD | 5/60 | RC6 | Generate from live price field |
/ar-ae/ JSON-LD currency GBP | All /ar-ae/ products | RC6 | Currency from market config |
Worked example 2: an Abu Dhabi clinic's schema plugin
A clinic group (illustrative) has three plugins each outputting MedicalClinic/LocalBusiness markup with different opening hours. Rich Results Test shows no errors — all three blocks are valid — but the facts conflict. The finding isn't "invalid markup", it's "conflicting facts": keep one source, generated from the same fields as the visible opening hours, and align Business Profile hours.
Common mistakes
- Treating "valid in the Rich Results Test" as "correct".
- Checking one URL per template when the bug only appears on variants or sale items.
- Recommending markup for rich result types that no longer exist.
Key takeaways
- SSR reduces but doesn't eliminate rendering risk; compare raw and rendered HTML per template.
- Content or links that appear only after interaction provide no discovery value.
- Verify JSON-LD against visible price and availability on samples that include variants and sale items.
- Valid markup isn't necessarily correct; conflicting valid blocks are a finding.
- Don't recommend markup for retired rich results such as FAQ (ended May 2026).
Check your understanding
Quick questions to lock in the lesson. They don’t count towards your certificate.
Put it into practice
Run the JSON-LD price check on 20 product or service pages, including at least five variants or sale items, and record mismatches with counts and examples.
Enrol for free to save your progress
Reading is always free. Enrol to keep your place, take the final assessment and earn a verifiable certificate.