Salesforce B2C Commerce (the platform formerly known as Demandware, sold today as part of Commerce Cloud and the Agentforce Commerce stack) was designed for tier-one retail brands long before “structured data” was a feature anyone shipped. It shows in the defaults: the Storefront Reference Architecture (SFRA) is a clean server-rendered MVC stack, but rich JSON-LD isn’t a built-in concern — it’s something the implementation team adds during a build.
This guide walks through the B2C Commerce catalog model, the storefront architectures (SFRA and the composable PWA Kit storefront), and the cartridge pattern for injecting Schema.org JSON-LD without modifying the reference code directly.
If you’re auditing an existing SFCC deployment instead of building on it, the Salesforce Commerce Cloud AI readiness audit guide is the companion piece.
The B2C Commerce catalog model
A B2C Commerce catalog is organized around a small set of entity types, each with its own implications for how product schema gets rendered.
| Entity | Role | Schema implication |
|---|---|---|
| Master product | The “parent” — holds variation attributes (color, size) and shared content | Render as Product with embedded hasVariant array or as ProductGroup |
| Variant | A buyable SKU under a master, with its own GTIN, price, and inventory | Each variant has its own Product shape with isVariantOf pointing at the master |
| Product set | A merchandising bundle that links related sellable products together (a “look”) | Render as a top-level Product with isRelatedTo rather than hasVariant |
| Bundle | A fixed-composition kit sold as one SKU | Render as a single Product; the components are not separately offered |
| Standard product | A single sellable SKU with no variants | A single Product with offers |
The master/variant pattern is the one that bites implementations most often. SFCC’s variation attributes are flexible enough that a team can model the same catalog in multiple ways — and only one of those ways renders cleanly to AI agents. See the variant handling in product schema guide for the modeling decision; this guide focuses on the rendering side.
Custom attributes
Beyond the system attributes (name, id, brand, price,
availability), almost every SFCC catalog uses custom attributes to
carry the data AI agents care about: material, fit, country of
origin, ingredient list, age range, compatibility, certifications.
These live on the product entity as system or custom attribute
groups, configured in Business Manager. The script API surfaces
them via product.custom.<attributeId>. The implication for schema:
the data is in the catalog, it just isn’t surfaced in the
default product detail page output. The cartridge pattern below is
the place to bridge it.
SFRA — the server-rendered storefront
SFRA is the reference storefront architecture B2C Commerce ships today. It succeeded the older SiteGenesis architecture — Salesforce has not formally retired SiteGenesis, but SFRA is the recommended starting point for new builds — and uses an MVC pattern:
- Controllers handle routing
- Models convert the script API objects into pure JSON view models
- ISML templates render the markup
- Cartridges wrap all of the above and stack via the cartridge path so site-specific code overrides the reference code without modifying it in place
The architectural detail to internalize: you never edit the
app_storefront_base cartridge directly. Site-specific code
lives in a separate cartridge (commonly
app_<brand>_storefront), which is placed earlier on the
cartridge path so its templates and decorators win.
For structured data, this means the JSON-LD injection lives in a site-specific cartridge that overrides the head template or extends the product detail template — never in the base.
The default SFRA output
The honest answer: verify what your SFRA build ships before assuming. The reference architecture has evolved across releases, and which structured-data primitives appear in the default templates depends on which version your team built on and what the implementation partner customized.
The reliable assumption is that anything beyond minimal
Product markup — variants modelled as hasVariant, offers with
real inventory state, aggregateRating and review, brand as
an Organization, GTIN family identifiers — is something your
team added. If you can’t find it in the rendered HTML for a
production PDP, treat it as missing rather than assumed.
The composable storefront — PWA Kit and beyond
For headless deployments, Salesforce ships the Composable Storefront (PWA Kit) — a React-based storefront that consumes data from B2C Commerce via the Shopper API (SCAPI). The relationship between the two:
- SFRA — server-side rendering, ISML templates, runs on the Commerce Cloud platform itself
- Composable Storefront — client/server React app, runs on Managed Runtime (Salesforce-hosted) or self-hosted, consumes SCAPI
For structured data, the difference is the rendering surface:
- SFRA — JSON-LD is emitted from ISML at server-side render time. AI crawlers see it in the initial HTML response.
- Composable Storefront — JSON-LD can be emitted server-side
(via the SSR path) or client-side (after hydration). Server-side
is the only path AI crawlers reliably see. Don’t rely on
client-side
useEffectinjection to land in an AI agent’s view of the page.
The implication for a headless build: if the team is rendering
JSON-LD with next/head or equivalent on the server, you’re fine.
If they’re injecting it via React effects on the client, the
crawlers and agents that don’t execute JavaScript will miss it
entirely. The SSR-vs-CSR distinction is a foundational AI readiness
concern on every headless storefront; the principle is the same
whether the back end is SFCC, Adobe Commerce, or Shopify.
OCAPI vs SCAPI
For completeness, the API distinction matters for any team building a headless storefront or a downstream system that needs to read catalog data:
- OCAPI — the legacy Open Commerce API. Salesforce maintains a versioning and deprecation policy that progresses APIs through current → deprecated → retired status; OCAPI versions are working through that lifecycle as SCAPI builds out.
- SCAPI — the newer Commerce API (Shopper API for storefront reads, Admin API for back-office writes). The recommended path for new builds and the API the Composable Storefront uses by default.
For a structured-data project, the API choice doesn’t change what needs to be rendered — Schema.org doesn’t care how the data was fetched. It changes who renders it. If a headless storefront is hydrating from SCAPI, the JSON-LD render lives in the storefront code, not in SFCC.
The cartridge pattern for adding JSON-LD (SFRA)
The canonical pattern for adding rich product schema to an SFRA storefront:
Step 1 — pick a cartridge
Put the code in a site-specific cartridge that already exists
(e.g. app_<brand>_storefront). If structured data is a larger
project that will span multiple brands or sites, create a
dedicated int_structured_data cartridge and place it on the
cartridge path ahead of the site cartridges.
Step 2 — build a model
Don’t render schema directly from ISML. Build a model that takes
a product script API object and returns a pure JSON object
matching the Schema.org shape you want:
// cartridges/int_structured_data/cartridge/models/productSchema.js
'use strict';
const URLUtils = require('dw/web/URLUtils');
function ProductSchemaModel(product, options) {
const isVariant = product.variant;
const master = isVariant ? product.variationModel.master : product;
this['@context'] = 'https://schema.org';
this['@type'] = 'Product';
this.name = product.name;
this.sku = product.ID;
this.description = product.shortDescription
? product.shortDescription.markup
: '';
this.image = collectImages(product);
if (product.brand) {
this.brand = {
'@type': 'Brand',
name: product.brand,
};
}
if (product.UPC) {
this.gtin = product.UPC;
}
if (product.manufacturerSKU) {
this.mpn = product.manufacturerSKU;
}
this.offers = buildOffers(product, options);
if (isVariant) {
this.isVariantOf = {
'@type': 'ProductGroup',
productGroupID: master.ID,
name: master.name,
};
}
}
module.exports = ProductSchemaModel;
(Helpers — collectImages, buildOffers — read from
product.getImages('large'), product.priceModel, and
product.availabilityModel respectively. Keep them in the same
module so the rendering boundary stays tight.)
Step 3 — extend the PDP template
Override or extend product/productDetails.isml from the
site-specific cartridge to include the JSON-LD block:
<isscript>
var ProductSchemaModel = require('*/cartridge/models/productSchema');
var schema = new ProductSchemaModel(pdict.product, {
url: URLUtils.https('Product-Show', 'pid', pdict.product.id).toString()
});
</isscript>
<script type="application/ld+json">
${JSON.stringify(schema)}
</script>
The */cartridge/... prefix on the require lets the cartridge
path resolve to whichever cartridge defines the model — important
when you want to share the model across SFRA and a custom
cartridge layer.
Step 4 — guard against the empty case
JSON.stringify(undefined) returns undefined, which renders as
the literal string undefined in the script tag. Always check
the model returned a populated object before emitting the script
tag, and skip the tag entirely on error rather than ship broken
JSON.
Page Designer and structured data
If your team is building content pages with Page Designer (the visual page builder), the structured data implications are different:
- Product detail pages rendered through the standard SFRA path get the JSON-LD from the cartridge pattern above.
- Editorial pages (homepage, category landings, brand stories)
rendered through Page Designer components do not emit schema by
default — Page Designer components only render what they are
built to render. If those pages should carry
BreadcrumbList,WebPage, orArticleschema, the relevant Page Designer component needs to be authored to emit it.
The component-level pattern: define a JSON-LD-emitting Page Designer component that authors can drop into a page, and have it read from the component’s configured fields. Don’t try to make authoring concerns (which page lives where) leak into the schema shape.
Headless and the composable storefront
For a Composable Storefront build, the same model can be written in TypeScript and consumed in the server-side render path of the framework (Next.js’s App Router, Remix, etc.). Two rules that port from the SFRA pattern:
- Emit at server-side render time, not after hydration. AI crawlers don’t reliably execute JavaScript. JSON-LD injected client-side is invisible to a meaningful share of the agents you care about.
- One model, many rendering surfaces. The schema-building logic lives in a shared TypeScript module. The SSR layer calls it; the build-time prerender (if you have one) calls it; if a future RSC or edge layer enters, that calls it too. The shape of the schema is independent of the runtime.
Common implementation traps
| Trap | Why it bites |
|---|---|
| Embedding JSON-LD with hand-written ISML string concatenation | One escape failure (a quote in a product name, a backslash in a description) and the JSON is invalid. Always use JSON.stringify on a built object. |
| Pulling localized strings from the wrong locale | SFCC has request.locale, the customer’s locale, and the catalog’s locale. Schema should always render in the customer-facing locale of the rendered page. |
Hardcoding availability: InStock | Inventory state changes by the minute on a busy store. Read it from product.availabilityModel.availabilityStatus and map every state, including NOT_AVAILABLE (OutOfStock) and BACKORDER (PreOrder or BackOrder depending on policy). |
| Forgetting B2B/B2C overlap on a Commerce Cloud install | If the same install serves both B2C and B2B traffic with different pricing logic, the schema’s priceCurrency and price need to render from the right pricebook for the visiting customer’s segment. |
| Variant URL drift | SFCC variant URLs are constructed from variation attribute selections. Make sure the url in the schema points at the canonical variant URL, not a redirect or a query-string-stripped master URL. |
| Page Designer pages shipping no schema at all | If a category landing is built in Page Designer, the default page template does not emit category-level schema. Add it explicitly. |
Validation
Same as every other platform: render a real PDP, view source, copy the JSON-LD into Google’s Rich Results Test and Schema.org’s validator, and fix what breaks. The validating structured data guide has the full workflow.
If you want a continuous read across the full catalog rather than a one-off check, that’s what Lumio does — every product, every release, every change.
Cluster — structured data on SFCC
- Product schema for Adobe Commerce — the sibling guide for the other tier-one enterprise platform
- How to audit a Salesforce Commerce Cloud site for AI readiness — the audit companion
- Variant handling in product schema — master/variant modeling decisions
- Validating structured data — the validation workflow