---
title: "Rendering and structured data checks: raw vs rendered…"
description: "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…"
url: https://optimizeall.com/learn/technical-seo-audit-in-practice/rendering-and-structured-data-checks
updated: 2026-10-05
---

Technical SEO Audit Workshop · Running the crawl and gathering evidence · lesson 6 of 14 · 14 min

# Rendering and structured data checks: raw vs rendered, markup vs visible

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

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

## Video lecture: Rendering and structured data checks: raw vs rendered, markup vs visible

Lecture coming soon · 13 chapters · about 8 minutes. Read the full transcript below.

1. Rendering and structured data checks
2. Check 1: raw vs rendered
3. Kiran Home findings (illustrative)
4. Check 2: JSON-LD vs visible
5. Results (illustrative)
6. Check 3: eligibility
7. Valid ≠ correct
8. Example 2: three plugins, three sets of hours (illustrative)
9. Write it up
10. Check 4: market parity
11. Watch me do it: 60-product markup check
12. The summary table
13. Mistakes, recap, try this now

## Lecture transcript

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

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

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

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

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

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

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

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

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

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

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

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

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

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

## Try it

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.

- [Previous: Log-file analysis lab: Cloudflare logs to crawl insights in Python](https://optimizeall.com/learn/technical-seo-audit-in-practice/log-file-analysis-lab)
- [Next: From findings to root causes](https://optimizeall.com/learn/technical-seo-audit-in-practice/from-findings-to-root-causes)
- [All lessons of Technical SEO Audit Workshop](https://optimizeall.com/learn/technical-seo-audit-in-practice)
