Joomla Product Structured Data: What Google Checks

What Google reads on a product page, what it requires and what it recommends, how to describe variants, the details that fail quietly, and a ten-minute check at the end.

Structured data tells a search engine what a page is without making it guess: this product, at this price, in stock, rated this well. On a product page it is a block of Schema.org JSON-LD. Google reads it for two kinds of result, which its product structured data overview calls product snippets and merchant listings. Product snippets are for pages where people cannot buy the product, such as reviews. Merchant listings are for pages where customers can buy from you. A shop’s product page is the second kind, and its rules ask for more, so those are the rules this post follows.

Correct markup makes a page eligible and nothing more. Google’s structured data guidelines say it does not guarantee a rich result even for markup that passes its test. The same guidelines set the rule behind most of what follows: the markup must be a true representation of the page, and must not describe anything a visitor cannot see.

What Joomla ships

Joomla 5.0 added a System – Schema.org plugin with a set of type plugins: Article, BlogPosting, Book, Custom, Event, JobPosting, Organization, Person and Recipe. They attach to articles and contacts, where you fill in the fields on the edit screen. There is no Product type, in a Joomla 6.1 installation or in the 6.3 and 7.0 development branches at the time of writing. The Custom type takes JSON you write yourself, but only on an article. A shop’s product pages are served by the cart, and Joomla does not know what a product is, so the cart has to write the markup. The rest of this post is the checklist to hold it to.

One page, one product

Google lists JSON-LD as its recommended format: a script block that sits apart from the visible HTML. For merchant listings it also recommends having the product markup in the page’s initial HTML, and warns that markup added by JavaScript can make shopping crawls less frequent and less reliable. That matters for the two things that change most often, price and stock.

Then there is a trap that Joomla layouts walk into. A product page rarely shows one product. Related products, recently viewed items and a listing module in a position under the component all render product cards, and a card template that marks itself up as a Product hands the crawler five candidates for one page. Google’s product snippet page is plain about it: product rich results only support pages that focus on a single product, or on variants of the same product. So the page’s own product is the only one marked up. Cards on a product page carry no product markup, and everything about the product, its ratings included, goes into that one node rather than into a second block.

The node, field by field

This is the markup a Solidshop store writes for a product without variants, with the addresses shortened:

<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "Product",
  "name": "Enamel Mug",
  "url": "https://www.example.com/shop/enamel-mug",
  "image": [
    "https://www.example.com/files/shop/1/media/thumbs/enamel-mug_800x450.jpg",
    "https://www.example.com/files/shop/1/media/thumbs/enamel-mug-detail_800x450.jpg"
  ],
  "description": "A hard-wearing 350ml enamel mug.",
  "sku": "MUG-001",
  "gtin13": "5012345678900",
  "brand": { "@type": "Brand", "name": "Acme" },
  "offers": {
    "@type": "Offer",
    "url": "https://www.example.com/shop/enamel-mug",
    "price": 9.5,
    "priceCurrency": "USD",
    "availability": "https://schema.org/InStock",
    "priceSpecification": {
      "@type": "UnitPriceSpecification",
      "priceType": "https://schema.org/StrikethroughPrice",
      "price": 12,
      "priceCurrency": "USD"
    }
  }
}
</script>

Google’s merchant listing page requires three things on the product: a name, an image (it prefers photos that clearly show the product, for example against a white background) and an offers node. The offer requires a price and a priceCurrency as a three-letter ISO 4217 code. The price must be greater than zero, which product snippets do not insist on.

The price is a number. Schema.org’s usage notes ask for a full stop as the decimal point, no thousands separator, and no currency sign in the value, since priceCurrency carries the currency. A template that prints its formatted display price into the markup, such as “$1,299.00” or “1.299,00 €”, breaks all three rules at once.

Everything else in the sample is recommended rather than required: description (strongly recommended, in Google’s words), brand, the identifiers sku and gtin13, and availability as a Schema.org URL. The recommended list also has itemCondition, aggregateRating and review, and the two policies covered further down. Fill in what is true for your shop; a recommended property is not an invitation to guess.

Variants: one group, one product per variant

A T-shirt in three colours and four sizes is twelve things to buy. The simple way to mark it up is one Product with twelve offers, which gives a crawler twelve prices and twelve stock states with nothing to say which colour and size each belongs to. Google’s product variant markup fixes that with a ProductGroup whose hasVariant lists each variant as a Product of its own, and it makes the variants eligible to show in merchant listings. Trimmed to two variants:

{
  "@context": "https://schema.org",
  "@type": "ProductGroup",
  "name": "Cotton T-shirt",
  "url": "https://www.example.com/shop/cotton-t-shirt",
  "description": "Soft, breathable unisex cotton T-shirt.",
  "productGroupID": "21",
  "variesBy": [
    "https://schema.org/size",
    "https://schema.org/color"
  ],
  "hasVariant": [
    {
      "@type": "Product",
      "name": "Cotton T-shirt - S / Red",
      "sku": "TEE-S-RED",
      "gtin13": "5012345678917",
      "image": "https://www.example.com/files/shop/1/media/thumbs/tee-red_800x450.jpg",
      "size": "S",
      "color": "Red",
      "offers": {
        "@type": "Offer",
        "url": "https://www.example.com/shop/cotton-t-shirt?variant=60",
        "price": 20,
        "priceCurrency": "USD",
        "availability": "https://schema.org/OutOfStock"
      }
    },
    {
      "@type": "Product",
      "name": "Cotton T-shirt - S / Black",
      "sku": "TEE-S-BLACK",
      "gtin13": "5012345678924",
      "image": "https://www.example.com/files/shop/1/media/thumbs/tee-black_800x450.jpg",
      "size": "S",
      "color": "Black",
      "offers": {
        "@type": "Offer",
        "url": "https://www.example.com/shop/cotton-t-shirt?variant=61",
        "price": 21,
        "priceCurrency": "USD",
        "availability": "https://schema.org/InStock"
      }
    }
  ],
  "aggregateRating": {
    "@type": "AggregateRating",
    "ratingValue": 5,
    "reviewCount": 1,
    "bestRating": 5,
    "worstRating": 1
  }
}

Google’s technical guidelines for variants come down to five rules:

  • Every variant has a unique ID, a sku or a gtin. Copying the parent SKU onto every variant defeats the purpose.
  • The group has one too, in productGroupID, which Google describes as the parent SKU.
  • variesBy names what differs, as full Schema.org URLs. Google supports six: color, size, suggestedAge, suggestedGender, material and pattern. Each variant then carries its own value of each. A variant’s name should be more specific than the group’s.
  • Shared facts sit on the group: the brand, and a rating that represents all variants.
  • On a single-page product, one canonical URL for the whole group, normally the address with no variant picked, and a distinct URL for each variant through a query parameter. Opening that URL must preselect the variant, which Google spells out as showing the right image, price and availability, and letting the shopper add that variant to the cart.

One more rule follows from the last. Once a crawler knows a variant URL it will come back to it, so a link to a variant you have since deleted must still open the product page, with nothing picked, rather than break it.

The details that fail quietly

Each of these can pass a validator and still make the markup say something the page does not.

The price the shopper reads

Mark up the price the page shows. Many shops enter prices without tax and display them with it, and a template that prints the computed price while the markup prints the stored one tells a search engine a lower price than the shopper sees. That breaks the true-representation rule above, and it is invisible until someone compares the two.

A sale

The active price goes in price. The old price goes in a priceSpecification with priceType set to https://schema.org/StrikethroughPrice, as in the mug sample, and Google asks you not to give the active price a priceType at all. Only mark a sale while the old price is higher. An “original” price below the current one is not a sale, whatever the product form allowed.

GTINs that are GTINs

A GTIN is the number under a product’s barcode: the UPC or EAN on the box, or an ISBN-13 on a book. Google takes them as gtin8, gtin12, gtin13 or gtin14, recommends the most specific property that applies, and wants the number in digits rather than as a URL. The barcode field in a shop often holds something else, such as a code of your own for the warehouse, and that must stay out. The last digit of a real GTIN is a check digit calculated from the others, so a typo or a made-up code fails the check and is easy to filter.

Stock per variant

Each variant’s availability is its own, and it should follow the rule your checkout applies. If a variant does not track stock and checkout sells it, the markup says in stock; if checkout refuses it, the markup says out of stock. A product-wide flag applied to every variant gets one of them wrong.

A plain-text description

The description field in a Joomla product form is usually an editor, so it holds HTML. The markup should hold the text: tags removed, entities decoded, and a space kept where one paragraph ends and the next begins.

Ratings and reviews

aggregateRating and review go on the product’s own node, and on the group when there are variants. Product is among the types Google’s review snippet guidelines accept; the restriction to reviews of other businesses applies to the local business and organisation types, not to products. Each review needs an author whose name is a real person’s or a team’s. On a Solidshop store the Reviews add-on merges the rating and the latest reviews into the product’s node.

Shipping and returns

The two recommended facts most shops leave out are the shipping cost and the return policy. You do not have to repeat them on every product. Google recommends one store-wide return policy, nested under your Organization markup, and since November 2025 a store-wide shipping policy in the same place, ideally on the page that describes your shipping. A policy given on a single product still wins for that product.

The other route needs no markup. The Shipping and returns settings in Search Console are now open to every site Google identifies as an online merchant, with or without a Merchant Center account, and what you set there takes precedence over structured data. Solidshop does not write either policy yet, so on a Solidshop store Search Console is the way to supply them today.

The worked example: a Solidshop store

Solidshop writes the product markup itself, in the page’s initial HTML, with nothing to switch on. Today every product page carries one Product node, and the product cards and listing modules below it carry no product markup of their own. Custom fields set to Include in structured data are added as product properties, as the custom fields guide explains, and the store’s front page carries an Organization node.

Version 1.7.0, the next release, rebuilds the product node along the lines of this post, and the two samples above are its output. A product with variants becomes a ProductGroup, each variant with its own SKU, image, price, stock and a link that opens the page with it picked. A barcode becomes a GTIN only when it passes the check digit. Options named Color, Colour, Size, Material or Pattern, and any option shown as colour swatches, are listed in variesBy. An original price above the price is marked as a sale, and prices are the ones the page shows, as numbers. What you can do now: give each variant its own SKU or barcode and an image. The products guide has the details.

Check your work

  1. Run the Rich Results Test on a product URL. The help page explains the results: an item with errors is invalid, an item with warnings is still valid. Read each warning against the recommended list above. For a site Google cannot reach, such as a staging copy, choose Code instead of URL and paste the page source.
  2. Paste the same URL into the Schema Markup Validator, which Google describes as checking all Schema.org markup without its own feature rules. It shows the full node.
  3. Print the JSON-LD from the command line and count the product nodes. There should be one.
    curl -sL "https://www.example.com/shop/enamel-mug" | grep -o '<script type="application/ld+json"[^>]*>.*</script>'
  4. Compare a variant URL with the product page. Both must print the same canonical address.
    curl -sL "https://www.example.com/shop/cotton-t-shirt?variant=60" | grep -i -o '<link[^>]*rel="canonical"[^>]*>'
  5. Open the Merchant listings report in Search Console a few days later. Google keeps a separate Product snippets report, but for pages that sell, the merchant listings report already includes its checks.

What each page is

robots.txt says where a crawler may go, the sitemap says what is there, and structured data says what each page is. Making a Joomla Store Visible to AI Assistants covers what an assistant reads on top. Solidshop writes the product markup from the free core; get it from the extensions page, or read the products guide if your store is already running.