# The state of structured data in Spanish ecommerce

> What structured data Spanish online stores publish on their product pages: JSON-LD, Product, Offer, ratings and GTIN, and whether it matches the page.

URL: https://clione.ai/studies/structured-data-spanish-ecommerce/
Lang: en

An online store that sells in Spain has two readers on every [product page](/glossary/product-page/): the shopper, who looks at the photo, the price and the reviews, and the program that reads the page to work out what it sells, at what price and whether it's in stock. The second reader is the one [structured data](/glossary/structured-data/) speaks to: a block that describes the product in a fixed format, almost always [JSON-LD](/glossary/json-ld/) with the [Schema.org](/glossary/schema-org/) vocabulary. **The question this study asks is simple, and the public figures don't answer it with Spanish data**: what structured data do the product pages of stores selling in Spain actually publish, and how much of it is correct?

Correct means four things at once: the markup is there, it says what it needs to say, it reaches whoever reads the page even if they don't run JavaScript, and it matches what the shopper sees. We measure all four, page by page. We also publish what went right, because a study that only counts failures overstates the problem.

## Why it matters to a store selling in Spain

Buying online is already the norm in Spain: **59.6% of people aged 16 to 74 bought something online in the last three months**, according to the 2025 survey by INE, Spain's National Statistics Institute, and 37.9% used a generative AI tool over the same period.[^ine-tich-2025]

On Google, what a machine can read from a product page follows written rules. For a page to appear in [merchant listings](/glossary/merchant-listings/), Google's experiences for pages that sell a product, the [Product](/glossary/product-schema/) markup needs three required properties (the name, the image and an offer), and the offer needs a price and its currency.[^google-merchant-listing] **Only pages where the shopper can buy the product are eligible**, not pages that link to another store that sells it.[^google-merchant-listing] A page that talks about a product without selling it, a review for example, can qualify for a product snippet, and all it needs is the name plus a review, an aggregate rating or an offer.[^google-product-snippet]

The rest is recommended: the [GTIN](/glossary/gtin/) and the other global identifiers, the [MPN](/glossary/mpn/), the brand and the aggregate rating ([AggregateRating](/glossary/aggregaterating/)).[^google-merchant-listing] Recommended doesn't mean decorative. According to GS1 Spain, the 13-digit GTIN identifies units meant for the point of sale, and it can only be generated from a GS1 company prefix:[^gs1es-gtin] it's the shortest way to say, without ambiguity, which exact product this is.

And one rule weighs more than all the others: **structured data has to be a true representation of the page's content**, and you don't mark up content that the page's readers can't see.[^google-sd-policies] A price in the markup that doesn't match the one on the page isn't a technical detail. It's the page contradicting itself.

## What's already known, and why it isn't enough

There are public figures on structured data, and they're good ones. HTTP Archive's Web Almanac analyzes millions of websites worldwide every year: the 2025 edition starts from 16,213,084 sites, taken from the Chrome UX Report and crawled in July 2025.[^almanac-2025-methodology] Its SEO chapter reports that **50% of home pages carried structured data in 2025**, on desktop and on mobile, and that 43% carried JSON-LD; among inner pages, 39% carried JSON-LD on desktop and 37% on mobile.[^almanac-2025-seo] The same chapter says most sites deliver their structured data in the initial HTML, and that **JavaScript added it in only 2% of crawls**.[^almanac-2025-seo]

The 2024 structured data chapter gives the figure for one specific type: Product appeared in JSON-LD on 0.77% of the pages analyzed.[^almanac-2024-structured-data] The base is the whole web, with its blogs, its newspapers and its company pages, so the figure doesn't say what share of product pages carry Product.


The Web Almanac figures are worldwide: they don't separate Spanish stores, they don't separate stores from other sites, and they don't separate product pages from home pages or articles. They're context, not an answer. A worldwide JSON-LD figure doesn't tell you how many pages on a Spanish store give their price in euros, their GTIN or their rating, or whether that data matches the page.


Nor can the worldwide picture of ecommerce platforms be carried over to Spain. The Web Almanac itself explains that **the leading platform changes from one region to another**, because of language, local payment methods, the agency ecosystem and each vendor's history.[^almanac-2025-ecommerce] And the platform and its theme can decide much of a product page's markup, as the study [From stored data to published data](/studies/stored-vs-published-data/) shows on Shopify and BigCommerce. That's why this study reads Spanish stores and records each one's platform.

## How we measure it

The data comes from the [Corpus](/corpus/), the crawl of online stores run by the team that publishes this library. That same team builds a tool that reads stores' structured data, so the method is written for anyone to repeat with public tools: an HTTP request, a headless browser and a JSON parser.

### The sample

A store enters the sample if it sells to shoppers in Spain: prices in euros, shipping to Spain and a Spanish-language version of the product page. Within that frame, the sample is spread across sectors and platforms, so that no single sector or platform decides the result. Each store's platform is recorded as its HTML and headers give it away, and one that can't be identified is recorded as unknown, not guessed.

On each store we read a fixed number of product pages, picked at random from the product URLs in its [XML sitemap](/glossary/xml-sitemap/). A store with fewer product pages than that number goes in with all it has. Product pages only: [category pages](/glossary/category-page/) and home pages are left out, because they're a different question.

### Two reads of every page

1. **The HTML without JavaScript.** A GET request that runs nothing: what a [crawler](/glossary/web-crawler/) that doesn't render receives. We keep the status code, the final URL, the headers and the body.
2. **The rendered DOM.** The same URL in a headless Chromium, which loads the page and runs its JavaScript, as Google does in its rendering phase.[^google-js-seo] We keep the DOM, the visible text and every `application/ld+json` block.

Comparing the two reads tells you where the markup comes from: whether it was already in the HTML the server sends, through [server-side rendering](/glossary/server-side-rendering/), or whether it only appears after [JavaScript rendering](/glossary/javascript-rendering/). The difference matters because Google can read JSON-LD that a script injects,[^google-sd-intro] but warns that dynamically generated product markup can make Shopping crawls less frequent and less reliable, a problem for [price and availability](/glossary/price-and-availability/),[^google-sd-js] and because **not every bot can run JavaScript**.[^google-js-seo] What each AI crawler does with JavaScript is covered in the guide [What an AI crawler sees in your store](/guides/what-an-ai-crawler-sees/).

### What we measure on each page

- **Format.** Whether there's JSON-LD, [microdata](/glossary/microdata/) or [RDFa](/glossary/rdfa/), and how many blocks of each. Google supports all three and recommends JSON-LD, the easiest to maintain at scale.[^google-sd-intro]
- **Product.** Whether there's a Product node, and with which properties: name, image, description, brand and offer. Schema.org defines Product as any product or service on offer.[^schema-product]
- **[Offer](/glossary/offer-schema/).** Whether the offer gives a price, a currency and availability, the three properties Schema.org uses to describe it.[^schema-offer] On a Spanish store, the expected currency is the euro.
- **AggregateRating.** Whether the product declares an aggregate rating, with its value and the number of ratings or reviews behind it.[^schema-aggregaterating]
- **Identifiers.** Whether there's a GTIN in any of its forms, and an MPN.[^schema-product] A GTIN also gets checked from the inside: its last digit is a check digit, calculated from the digits before it, and it's there to catch errors.[^gs1es-digito] A GTIN with the wrong check digit isn't a valid GTIN.
- **Validity.** Whether each block can be parsed: a JSON-LD document is always a valid JSON document,[^jsonld-11] and a block with a syntax error tells nobody anything. And whether the Product has the properties Google requires for a merchant listing.[^google-merchant-listing]
- **Consistency with the page.** Whether the price, availability, name and rating in the markup are the ones the shopper sees in the page's text.[^google-sd-policies] And whether there are two or more Product nodes for the same product and, if so, whether they agree or contradict each other.

Before comparing, each value is normalized the same way in the markup and on the page: whitespace collapsed, HTML entities decoded and the price as a number plus a currency. That way a price written on the page with a decimal comma and the euro sign, and the same number written in the markup with a decimal point and `EUR`, count as the same price.

### How we classify each page

- **No markup**: no structured data at all.
- **Markup without Product**: there's structured data, but none of it describes the product.
- **Incomplete Product**: a property required for a merchant listing is missing.
- **Complete Product**: it has a name, an image and an offer with a price and a currency.
- **JavaScript only**: the Product is missing from the HTML and present in the rendered DOM.
- **Invalid**: the block can't be parsed as JSON.
- **Inconsistent**: the markup gives a price, availability or rating that differs from the visible one.
- **Conflicting duplicate**: two Product nodes give different values for the same property.

A page can fall into more than one class: a complete Product can also be JavaScript only and inconsistent.

## Results

| What we measured                                        | Pages   | Stores  | Base    |
| ------------------------------------------------------- | ------- | ------- | ------- |
| Any structured data                                     | Pending | Pending | Pending |
| JSON-LD                                                 | Pending | Pending | Pending |
| Only microdata or RDFa                                  | Pending | Pending | Pending |
| A Product node                                          | Pending | Pending | Pending |
| Complete Product for a merchant listing                 | Pending | Pending | Pending |
| Offer with price, currency and availability             | Pending | Pending | Pending |
| Currency in euros                                       | Pending | Pending | Pending |
| Aggregate rating (AggregateRating)                      | Pending | Pending | Pending |
| GTIN                                                    | Pending | Pending | Pending |
| GTIN with a correct check digit                         | Pending | Pending | Pending |
| MPN                                                     | Pending | Pending | Pending |
| [Brand](/glossary/brand-schema/)                        | Pending | Pending | Pending |
| Product only with JavaScript                            | Pending | Pending | Pending |
| At least one invalid block                              | Pending | Pending | Pending |
| Price in the markup differs from the visible one        | Pending | Pending | Pending |
| Availability in the markup differs from the visible one | Pending | Pending | Pending |
| Rating in the markup with no ratings in view            | Pending | Pending | Pending |
| Two conflicting Product nodes                           | Pending | Pending | Pending |

| Platform | Stores  | Pages   | Complete Product | GTIN    | JavaScript only | Inconsistent |
| -------- | ------- | ------- | ---------------- | ------- | --------------- | ------------ |
| Pending  | Pending | Pending | Pending          | Pending | Pending         | Pending      |

## What didn't fail, and what went unmeasured

Some things this method doesn't measure from the start:

- **Whether Google shows the page with [rich results](/glossary/rich-results/).** Reading the markup tells you whether the page is eligible, not whether Google will show it that way: structured data doesn't guarantee it.[^google-sd-intro]
- **Whether an AI cites the store.** Google says structured data isn't a requirement for its generative AI features, and that there's no special markup to add for them.[^google-ai-guide] This study measures what the page publishes, not what an assistant does with it.
- **What each bot reads.** We read the HTML with the crawl's [user agent](/glossary/user-agent/), not with each company's, and a store can serve something else under another name.
- **The rest of the store.** Category pages, home pages and content pages are left out.

## What it means

Until there are results, this is the frame for reading them, rule by rule, with what each rule's source says:

- **A page without Product, or without an offer with a price and a currency, isn't eligible for Google's merchant listings.** Those are the required properties.[^google-merchant-listing]
- **A page without a GTIN is still eligible.** The GTIN is recommended, not required,[^google-merchant-listing] and what the page loses is the unambiguous way to say which product it sells.
- **A Product that only exists with JavaScript reaches Google with more risk, and doesn't reach some bots at all.** Google warns of less frequent and less reliable Shopping crawls,[^google-sd-js] and a bot that doesn't run JavaScript doesn't see it.[^google-js-seo]
- **Markup that contradicts the page is the most serious defect on the list.** It breaks the rule of faithfully representing the content,[^google-sd-policies] and it's the only one on the list that hands whoever reads the markup false data.

To check all this on your own product page, the comparison [Validating structured data vs verifying it's published](/comparisons/validate-vs-verify/) explains the difference between markup that's valid and markup that's published. Google's tool for the job is the [Rich Results Test](/glossary/rich-results-test/), run on the URL rather than the code, as Google recommends for markup that JavaScript generates.[^google-sd-js]

## Limits

- **A sample, not a census.** The percentages hold for the pages and stores measured, with their split by sector and platform, not for all of Spanish ecommerce.
- **One crawl, one moment.** A page that's fine today can change tomorrow with a new theme, an app or a sync: that's [data drift](/glossary/data-drift/).
- **The starting list decides.** Stores that aren't on the candidate list can't enter the sample, and that list has its own biases.
- **Consistency is measured by automatic extraction.** Reading a page's visible price by machine can go wrong, for example with discounts or variants, so a subsample is checked by hand.
- **Reading isn't testing.** The study changes nothing on any store, so it can't show why a page is the way it is; it can only describe it.

## The data

Each page read is one row of the dataset, with these columns: `run_id`, `datetime_utc`, `store_id`, `platform`, `sector`, `url`, `http_status`, `formats`, `jsonld_blocks`, `has_product`, `product_props`, `has_offer`, `price`, `price_currency`, `availability`, `has_aggregaterating`, `rating_value`, `rating_count`, `gtin`, `gtin_check_ok`, `mpn`, `brand`, `in_raw_html`, `in_rendered_dom`, `parse_errors`, `visible_price`, `visible_availability`, `visible_rating`, `consistent`, `product_nodes`, `conflict`, `classes` and `notes`. Stores are published with a code, not their name.

The rest of the concepts for describing your catalog to a machine are laid out in order in the [structured data reading path](/learn/structured-data/).
