Crystallize logo

Structured data: JSON-LD for products, offers and specs from Crystallize

Schema.org JSON-LD tells search engines and answer engines what a page is about in a form they don't have to guess. A Crystallize product already has the facts: variants with their own SKU, GTIN, price and stock, typed specifications with units, brands, topics and relations. This page maps them to JSON-LD, and covers images for structured data and link previews.

One graph per page

Emit one <script type="application/ld+json"> per page with an @graph, and give every entity an @id (the page URL plus a fragment). A product page, its brand page and the organisation then describe the same entities, linked by @id, instead of repeating them.

// `<` is escaped, so content from the catalogue can't close the script tag.
export function JsonLd({ data }: { data: unknown }) {
  return <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(data).replace(/</g, '\\u003c') }} />;
}

Products and variants

A Crystallize product with several variants is a ProductGroup, with one Product per variant in hasVariant. Each variant has its own sku, gtin, image and Offer. A product with one variant can be a plain Product.

{
  "@context": "https://schema.org",
  "@graph": [
    {
      "@type": "ProductGroup",
      "@id": "https://your-store.example/en/drills/cordless-drill-driver-18v#product",
      "productGroupID": "ITEM_ID",
      "name": "Cordless drill driver 18V",
      "url": "https://your-store.example/en/drills/cordless-drill-driver-18v",
      "description": "Brushless two-speed drill driver with 60 Nm of torque.",
      "brand": { "@id": "https://your-store.example/en/brands/example-brand#brand" },
      "hasVariant": [
        {
          "@type": "Product",
          "@id": "https://your-store.example/en/drills/cordless-drill-driver-18v#DD18-BARE",
          "name": "Cordless drill driver 18V (bare tool)",
          "sku": "DD18-BARE",
          "gtin13": "7000000000001",
          "image": ["https://media.crystallize.com/your-tenant/…/@768/drill-driver.jpeg"],
          "isVariantOf": { "@id": "https://your-store.example/en/drills/cordless-drill-driver-18v#product" },
          "offers": {
            "@type": "Offer",
            "sku": "DD18-BARE",
            "url": "https://your-store.example/en/drills/cordless-drill-driver-18v",
            "price": 149,
            "priceCurrency": "EUR",
            "priceSpecification": [
              { "@type": "UnitPriceSpecification", "price": 149, "priceCurrency": "EUR", "valueAddedTaxIncluded": true },
              { "@type": "UnitPriceSpecification", "price": 123.14, "priceCurrency": "EUR", "valueAddedTaxIncluded": false }
            ],
            "availability": "https://schema.org/InStock",
            "inventoryLevel": { "@type": "QuantitativeValue", "value": 12, "unitCode": "C62" },
            "itemCondition": "https://schema.org/NewCondition",
            "seller": { "@id": "https://your-store.example/#organization" }
          }
        }
      ]
    }
  ]
}
  • `price` is what the shopper pays in that market, including VAT for consumers. List both prices in priceSpecification with valueAddedTaxIncluded, so a trade buyer's agent can read the net price too.
  • Volume prices (Discovery <priceVariant>PriceTiers) are more UnitPriceSpecification entries with eligibleQuantity: { "@type": "QuantitativeValue", "minValue": 10, "unitCode": "C62" }.
  • `availability` comes from the variant's stock (defaultStock or a location's stock): InStock above zero, OutOfStock at zero. Leave it out if the tenant has no stock data.
  • `inventoryLevel` is the stock figure itself, if you are happy to publish it.
  • Each market is its own page (/en, /no), with its own currency and VAT. Say which region an offer is for with eligibleRegion.

Identifiers

schema.org, Google Merchant Center and GS1 treat sku, gtin13 (or gtin8, gtin12, gtin14) and mpn as separate facts. Keep the merchant SKU in Crystallize's SKU, and the GTIN in its own non-translatable field on the variant. National trade numbers become identifier entries: { "@type": "PropertyValue", "propertyID": "NOBB", "value": "…" }.

Typed specs

Specifications modelled as typed components (numeric with units, boolean, selection) map one to one onto additionalProperty. A label/value table can't, because nothing in it says which part is a number or a unit.

{
  "@type": "PropertyValue",
  "name": "Torque, hard joint",
  "propertyID": "EF002842",
  "value": 60,
  "unitText": "Nm",
  "unitCode": "NU"
}
  • `unitText` is the unit as Crystallize stores it. `unitCode` is the UN/CEFACT Recommendation 20 code: VLT for volts, AMH for ampere-hours, NU for newton-metres, KGM for kilograms, MMT for millimetres, RPM for revolutions per minute (C94 for a rate per minute, such as impacts). Crystallize's unit list has no slot for the code, so keep a map in the storefront, and pick unit strings that map one to one.
  • Bounds: a field such as maxDrillingDiameter is also maxValue, and a minimum is minValue.
  • Selections are text values with a valueReference to a DefinedTerm (name, termCode), so an option key and its translated label travel together.
  • Booleans: publish only what is stated. Discovery returns an unset boolean as false.
  • `propertyID` carries a classification feature code when you have one (ETIM EF…, for example).

Simple physical facts have their own properties: weight, width, height and depth as QuantitativeValue.

Categories, brands and relations

  • `category`: the product-type path as plain text ("Drills > Drill driver"), which is what Google Merchant reads, plus a CategoryCode when the topic carries a classification code in its meta (ETIM EC…, GS1 GPC, UNSPSC): { "@type": "CategoryCode", "codeValue": "EC001391", "inCodeSet": { "@type": "CategoryCodeSet", "name": "ETIM" } }.
  • `keywords`: the names of keyword topics.
  • `brand`: an @id reference to a Brand defined on the brand page.
  • Item relations become isAccessoryOrSparePartFor, isConsumableFor or isRelatedTo, pointing at the related product's @id.
  • What is in the box is includesObject on the Offer: TypeAndQuantityNode with amountOfThisGood and typeOfGood.
  • Warranty is warranty on the Offer: a WarrantyPromise with durationOfWarranty in years (unitCode: "ANN").
  • Rentals are Offers with businessFunction: "http://purl.org/goodrelations/v1#LeaseOut" and leaseLength, with depots as availableAtOrFrom Places.

Listings and navigation

  • Category pages: a CollectionPage with an ItemList of the products shown, each ListItem with position and url.
  • `BreadcrumbList` on every page, from Discovery's breadcrumbs, which end with the item itself.
  • Front page: Organization (or OnlineStore) with @id, logo and areaServed, and a WebSite with a SearchAction to your search URL.

Read the facts once

Build the JSON-LD, the visible spec sheet and the Markdown version from the same typed rows, read once from Discovery. Then they can't disagree, and an answer engine that compares them finds the same value everywhere. See llms.txt and Markdown.