All posts
Ray Iyer
Ray Iyer
Co-founder, Anglera

Adding Product JSON-LD on BigCommerce — and keeping it in sync

How to add schema.org Product JSON-LD on BigCommerce, which fields (gtin, brand, offers) actually matter, and how to keep markup synced with the live page.

Adding Product JSON-LD on BigCommerce — and keeping it in sync

BigCommerce's Cornerstone theme ships with a basic Product JSON-LD block, but "basic" is doing a lot of work in that sentence — GTIN, brand, and ratings are frequently missing, and multi-variant products confuse the offer shape. This guide covers where the markup lives on a Stencil storefront, which fields Google and AI crawlers actually weight, and how to make sure the JSON-LD never drifts from what a shopper sees on the page.

Where the markup actually lives

On a Stencil-based storefront (Cornerstone and its derivatives), Product JSON-LD is rendered as a Handlebars partial included on the product detail page — typically templates/components/products/schema.html, pulled into templates/pages/product.html. Because it's rendered server-side from the same product context object that populates price, stock, and variant selectors, it updates automatically whenever the underlying catalog data changes — no separate sync job required, as long as you're editing the theme's data bindings and not hardcoding values.

There are two supported ways to touch it:

  1. Edit the theme file directly. In the control panel, go to Storefront → Themes → Advanced → Edit Theme Files (or edit locally with the Stencil CLI and push). This gives you full access to the Handlebars context — product.gtin, product.mpn, product.brand, product.reviews, everything.
  2. Inject via Script Manager, under Storefront → Script Manager. Its built-in location options are broad — "Storefront pages" (everything except checkout and order confirmation), Checkout, Order confirmation, or All pages — there's no native "product pages only" scope, so a script placed this way has to check at runtime (e.g., testing the URL pattern or a product-page global like window.BCData) that it's actually on a PDP before injecting anything. It also only has access to what's already rendered in the DOM, so it's a weaker option if you need identifiers like GTIN that aren't always printed on the visible page. Treat it as a stopgap for stores that can't get theme-file access, not the long-term approach.

Either way, first check whether your theme already emits a Product block via an application/ld+json script tag — search view-source for that MIME type. If one exists, edit it in place. Shipping a second, competing Product block on the same page is a common way to fail rich-result eligibility.

The fields that matter

FieldWhere it comes fromNotes
nameproduct.titleRequired. Must match the visible page heading.
brandproduct.brand.name / product.brand.urlBigCommerce Brands are a separate catalog object linked by brand_id; Google's structured-data guidelines expect brand as a nested Brand (or Organization) node, not a plain string.
skuproduct.skuProduct-level SKU field in the catalog.
gtinproduct.gtin (REST/GraphQL: gtin, alongside mpn and upc)Optional catalog field, set per product (and per variant for MPN/UPC on variant-level products). Schema.org's gtin property generalizes the older gtin8/gtin12/gtin13/gtin14 properties and auto-detects the right length, so a plain gtin key validates fine — but Google's own Merchant Center identifier guidance still recommends using "the most specific GTIN that applies," which is why Cornerstone's template picks the length-specific key (gtin13, etc.) at render time rather than always emitting gtin. Either form is acceptable; don't rewrite Cornerstone's default just to switch keys.
offersproduct.price (or product.price.price_range for variant products) plus product.condition, product.pre_order, product.out_of_stockNeeds price, priceCurrency, and availability at minimum.
aggregateRatingproduct.rating, product.num_reviewsOnly render this block when settings.show_product_reviews is true and review count is above zero — an empty aggregateRating is worse than none.

These map directly to the product identifier fields BigCommerce documents in the catalog (GTIN, UPC, MPN), and to the properties Google's Product snippet documentation treats as required or strongly recommended: name is required, and you need at least one of offers, review, or aggregateRating present for snippet eligibility, with brand, image, and the identifier trio (sku/mpn/gtin) recommended on top of that.

A working example

Here's a trimmed, annotated version of the pattern BigCommerce's own Cornerstone theme uses — Handlebars driving live catalog data into JSON-LD:

<script type="application/ld+json">
{
  "@context": "https://schema.org/",
  "@type": "Product",
  "name": {{{JSONstringify product.title}}},
  {{#if product.sku}}"sku": "{{product.sku}}",{{/if}}
  {{#if product.mpn}}"mpn": "{{product.mpn}}",{{/if}}
  {{#if product.gtin}}"gtin{{length product.gtin}}": "{{product.gtin}}",{{/if}}
  "url": "{{product.url}}",
  {{#if product.brand}}
  "brand": {
    "@type": "Brand",
    "name": {{{JSONstringify product.brand.name}}}
  },
  {{/if}}
  "description": {{{json (ellipsis (sanitize product.description) 4000)}}},
  "image": "{{getImage product.main_image 'zoom_size' (cdn theme_settings.default_image_product)}}",
  {{#and settings.show_product_reviews product.reviews.list.length}}
  "aggregateRating": {
    "@type": "AggregateRating",
    "ratingValue": "{{product.rating}}",
    "reviewCount": "{{product.num_reviews}}"
  },
  {{/and}}
  "offers": {
    "@type": "Offer",
    "priceCurrency": "{{currency_selector.active_currency_code}}",
    "price": "{{#if product.price.with_tax}}{{product.price.with_tax.value}}{{else}}{{product.price.without_tax.value}}{{/if}}",
    "availability": "https://schema.org/{{#if product.out_of_stock}}OutOfStock{{else}}InStock{{/if}}",
    "url": "{{product.url}}"
  }
}
</script>

Rendered for an actual product, that produces:

{
  "@context": "https://schema.org/",
  "@type": "Product",
  "name": "Bosch 18V Cordless Drill Kit",
  "sku": "BSH-DRL-18V-KIT",
  "mpn": "GSR18V-28FC",
  "gtin13": "4059625012345",
  "url": "https://tools.example.com/bosch-18v-cordless-drill-kit/",
  "brand": { "@type": "Brand", "name": "Bosch" },
  "description": "Brushless 18V drill/driver kit with two 4.0Ah batteries...",
  "image": "https://cdn.example.com/products/bosch-drill-zoom.jpg",
  "aggregateRating": { "@type": "AggregateRating", "ratingValue": "4.6", "reviewCount": "128" },
  "offers": {
    "@type": "Offer",
    "priceCurrency": "USD",
    "price": "199.00",
    "availability": "https://schema.org/InStock",
    "url": "https://tools.example.com/bosch-18v-cordless-drill-kit/"
  }
}

Two accuracy issues worth fixing

Variant price ranges. Cornerstone's default template swaps in product.price.price_range for multi-variant products and outputs minPrice/maxPrice inside an @type: "Offer" block — but minPrice/maxPrice aren't valid Offer properties in schema.org; they belong to AggregateOffer (lowPrice, highPrice, offerCount). If your catalog has variant-priced products, switch the @type to AggregateOffer and rename the fields, or better, emit one Offer per SKU so each GTIN/price pair is unambiguous.

Third-party reviews. If you run a review app (Yotpo, Okendo, Judge.me, etc.) instead of native BigCommerce reviews, it usually injects its own aggregateRating/review block. Keep only one source of review structured data per page — two competing aggregateRating nodes is a common cause of Rich Results Test warnings.

How to validate

  • View-source vs. rendered DOM: curl -s https://yourstore.com/product-name/ | grep -A 40 'application/ld+json' shows exactly what a non-JS crawler receives — Stencil renders this server-side, so curl and the browser should match.
  • Google's Rich Results Test (search.google.com/test/rich-results): paste the live URL, confirm a single Product entity resolves, and check that Offer/AggregateOffer values match what's on the page.
  • Schema Markup Validator (validator.schema.org) catches property-name errors, like the minPrice/AggregateOffer mismatch above, that Google's tool may not flag.
  • Spot-check a few SKUs after any catalog import or price sync — GTIN and MPN are easy fields to leave blank at scale.

Verified as of July 2026: field names and the Cornerstone template pattern reflect BigCommerce's current Stencil documentation and public theme source; Script Manager placement options were confirmed against current BigCommerce Developer Center docs. Re-check after major theme or Cornerstone version upgrades.

Getting this markup right assumes the underlying fields — GTIN, MPN, brand, use-case attributes — are actually populated in your catalog, which is where most retailers stall. Anglera enriches that product data continuously in your PIM or BigCommerce catalog itself, so the JSON-LD above always has a real GTIN and brand to render instead of an empty conditional block.

Ray Iyer

About the author

Ray IyerCo-founder, Anglera

Ray is a co-founder of Anglera, building the product-data infrastructure for agentic commerce — turning messy catalogs into structured, AI-readable data that buyers and answer engines can find. Previously product at Uber; Stanford CS.

See it on your own SKUs.

A 30-minute walkthrough on your categories and your supplier data.

Book a demo