Getting enriched product data onto Elastic Path product pages
How an enriched attribute moves from Elastic Path PXM through catalog publishing to the Shopper Catalog API and renders on a live product page.

Elastic Path is headless: there is no theme layer or Liquid-style template that auto-renders a new field the moment you add it. Every attribute you want on a product page has to travel a specific path — stored in Product Experience Manager (PXM), published into a catalog, fetched by your storefront through the Shopper Catalog API, and explicitly mapped into a component. This guide walks that path end to end for a single enriched attribute, with the exact API shapes and a validation step so you can confirm it actually made it onto the page rather than just into the admin.
Where the attribute lives: PXM, and which mechanism to use
Elastic Path gives you two ways to attach non-core data to a product in PXM, and picking the right one matters for how (and whether) the data shows up downstream.
Custom attributes (shopper_attributes and admin_attributes) are schema-less key-value pairs stored directly on the product resource. shopper_attributes are customer-facing and merge into the published catalog; admin_attributes are internal-only and never leave the PXM API. This is the mechanism built for exactly the kind of continuous, high-volume enrichment a PIM or an enrichment pipeline produces — no template setup required, filterable with eq/like/in on dot-notation paths, and importable via CSV using shopper_attributes. prefixed column headers.
Extension templates (Commerce Manager → Merchandise → Products → Templates/Attributes) are the older, more structured route: you create a Template, then create typed Attributes (Boolean, Float, Integer, Date, String) and assign each one to exactly one template. These are better suited to a small, curated set of merchandiser-managed fields with strict typing than to bulk, machine-generated enrichment.
For a specification like a water-resistance rating pushed from a PIM or enrichment tool, shopper_attributes is almost always the right call. Setting it via the PXM Products API is a partial update — you only send the keys you're changing:
curl -X PUT "https://useast.api.elasticpath.com/pcm/products/<product_id>" \
-H "Authorization: Bearer <admin_access_token>" \
-H "Content-Type: application/json" \
-d '{
"data": {
"type": "product",
"id": "<product_id>",
"attributes": {
"shopper_attributes": {
"water_resistance_rating": "IPX7",
"material": "recycled-aluminum"
}
}
}
}'
Attribute names are capped at 64 characters (alphanumeric, underscore, hyphen only) and values must be strings up to 512 characters — worth knowing before you pipe in a long enrichment paragraph expecting it to fit in one attribute.
Publishing the attribute into a catalog
An attribute existing on a product in PXM does not mean a shopper (or a crawler) can see it. Three conditions have to be true before it reaches the storefront:
- The product's status is
live, notdraft. - The product is associated with at least one hierarchy node.
- A catalog references that hierarchy, and the catalog has been published (a "catalog release" has been created since the change).
If a product and its price book both define overlapping shopper_attributes, the catalog release merges them, with the price book's values taking precedence — worth checking if you maintain regional price books, since a stale price-book value can silently override the product's enriched value in one locale and not another. Edit an attribute after the fact and nothing changes on the storefront until you trigger a new catalog release.
Fetching it in the storefront
Once published, the attribute is available through the public-facing Shopper Catalog API, GET /catalog/products/:product_id, which is what any headless frontend — Elastic Path's own Next.js–based Composable Frontend, or a custom app — actually calls at render time:
curl "https://useast.api.elasticpath.com/catalog/products/<product_id>?include=main_image,files" \
-H "Authorization: Bearer <storefront_access_token>"
{
"data": {
"id": "b2c3d4e5-...",
"type": "product",
"attributes": {
"name": "Trailhead 40L Pack",
"sku": "TH-PACK-40L",
"shopper_attributes": {
"water_resistance_rating": "IPX7",
"material": "recycled-aluminum"
}
}
}
}
Elastic Path's Composable Frontend starter wraps this same call in the @epcc-sdk/sdks-shopper package's getByContextProduct function, which the reference storefront uses to fetch a product server-side, detect its type (standard, parent, child, bundle) from meta.product_types, and hand the response to a display component.
Rendering it on the page
In a Next.js-based storefront (the pattern Composable Frontend ships), the attribute becomes a server component that reads shopper_attributes off the fetched product and maps it into markup:
import { getByContextProduct } from "@epcc-sdk/sdks-shopper"
export async function ProductSpecs({ productId }: { productId: string }) {
const { data: product } = await getByContextProduct({
path: { product_id: productId },
})
const specs = product?.attributes?.shopper_attributes ?? {}
const rows = Object.entries(specs)
if (rows.length === 0) return null
return (
<dl className="grid grid-cols-2 gap-x-4 gap-y-1 text-sm">
{rows.map(([label, value]) => (
<div key={label} className="contents">
<dt className="font-medium capitalize">{label.replace(/_/g, " ")}</dt>
<dd>{String(value)}</dd>
</div>
))}
</dl>
)
}
Because this runs as a React Server Component, the markup is generated on the server and shipped in the initial HTML — not bolted on after hydration. That matters for AI crawlers and answer engines that don't execute JavaScript: what's in view-source is what they see.
It's worth doing the same attribute twice: once as visible copy, once as structured data, since many AI agents and rich-result parsers prefer schema.org markup over prose:
const additionalProperty = Object.entries(specs).map(([name, value]) => ({
"@type": "PropertyValue",
name,
value,
}))
const productJsonLd = {
"@context": "https://schema.org",
"@type": "Product",
name: product?.attributes?.name,
sku: product?.attributes?.sku,
additionalProperty,
}
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(productJsonLd) }}
/>
How to validate
Don't trust the browser's Inspect Element panel by itself — it shows the rendered DOM after JavaScript has run, which can mask a component that only populates client-side.
- View-source vs. rendered DOM. Load the live PDP, then compare
curl -s https://yourstore.com/products/trailhead-40l-pack | grep -i "water_resistance\|IPX7"(raw HTML, no JS) against what you see in DevTools → Elements. If the attribute is in the DOM but absent from the curl output, it's client-only and invisible to most crawlers — check whether the component is accidentally marked"use client"in Next.js. - Confirm the API response first. Before debugging the frontend, curl the Shopper Catalog API directly (the request shown above) and verify
shopper_attributesis present and populated. If it's missing there, the problem is upstream — the catalog hasn't been republished, or the attribute landed inadmin_attributesby mistake. - Validate the structured data. Run the page through Google's Rich Results Test to confirm the JSON-LD parses and the
additionalPropertyarray is populated, not just the baseProductfields.
Verified as of July 2026
Field names, endpoint paths, and the custom-attributes vs. extension-templates distinction reflect Elastic Path's current Composable Commerce (SaaS) documentation as of July 2026. Elastic Path also maintains a separate, older Self-Managed Commerce product line with a different Cortex-based API; if you're on that platform, the storefront-fetch and rendering mechanics differ even though the PXM concepts are similar. Confirm which product line your store runs on before following the code above literally.
Anglera doesn't touch any of this page-rendering plumbing — it's additive on the data side, continuously enriching attributes like the one above and pushing them into whichever field your PIM or Elastic Path store expects, shopper_attributes included. Your PIM stores the data; Anglera does the work of keeping it complete and current, so the templates and components you build following the steps above have something rich to render on day one and on day one thousand.
Sources: Custom Attributes — Elastic Path Documentation, Product Templates Attributes in Commerce Manager, Get a Product — Shopper Catalog API, elasticpath/composable-frontend — all-product-types example
