Server-side rendering on Elastic Path: making product data visible to Google and AI
How Elastic Path storefronts render product data, why client-only rendering hides it from crawlers, and how to verify with curl and view-source.

Elastic Path Commerce Cloud is headless: product data lives in PXM (Product Experience Manager) and is exposed to a storefront as JSON through the Shopper Catalog API, and it is entirely up to the storefront you build (or generate from the Composable Frontend starter) whether that data ends up in the server-rendered HTML or is fetched client-side after the page loads. That's a meaningful decision, not a default, and it's easy to get wrong when a team optimizes a PDP for interactivity without checking what a crawler actually receives. Below is how Elastic Path's reference architecture renders product pages, where things commonly break, and how to confirm your own implementation is putting product data where Google and AI agents can read it.
Elastic Path's rendering model, in plain terms
Elastic Path Commerce Cloud does not ship a monolithic storefront with a built-in template engine. The commerce logic lives behind PXM (product attributes, pricing, variations, bundles, surfaced to a storefront via the Shopper Catalog API) and the composable set of Commerce Cloud APIs (cart, checkout, promotions). Presentation is a separate concern, built by you or scaffolded through Elastic Path's Composable Frontend starter kit, which generates a Next.js application pre-wired to those APIs.
That starter uses the Next.js App Router, with product listing and product detail routes organized under a (store) route group (products/[productId] in most example scaffolds, sometimes a catch-all like products/[...productSegment]). App Router gives you two very different ways to get product data onto a page:
- Server Components /
generateMetadata— the product is fetched from the Shopper Catalog API on the server, during the request (or at build time with revalidation), and the resulting HTML — title, description, price, availability — is part of the document the server sends back. - Client Components with
useEffect/SWR/React Query — the initial HTML ships largely empty, and the browser calls the Shopper Catalog API after hydration to fill in the product details.
Both are valid Next.js patterns and both are common in Elastic Path implementations, especially once a team layers on personalization, price-list-by-account, or availability widgets that legitimately need to run client-side. The problem is when the primary product content — name, description, price, key attributes — lives only in the client-fetched path.
Why client-only rendering is the risky default here
Because Elastic Path is API-first, there's a natural gravitational pull toward "just call the Shopper Catalog API from the client" — it's the same call the cart and personalization widgets already make, and it's the fastest way to prototype a PDP. The result is a page that looks complete in a browser but arrives at a crawler as a shell: a <div id="__next">, a loading skeleton, and a script tag. Googlebot does execute JavaScript, but it does so in a second, deferred rendering pass, on a delay that can run from seconds to days depending on crawl budget and site size, and other bots that quote or summarize product pages for AI assistants generally do not execute JavaScript at all. If your <title>, meta description, price, and product attributes only exist after a client-side fetch resolves, you're asking every consumer of that page — search engine or AI agent — to do extra work that many of them simply won't do.
Elastic Path's own guidance on this is direct: server-rendered and statically generated pages are "easy to read and crawl by bots," while JavaScript-heavy single-page apps make crawlers work harder to reach the same content — see Elastic Path's posts on HTML rendering in digital commerce and SEO for headless commerce. Their recommended pattern for PDPs is a JAMstack-style approach: generate the product page HTML (via SSR or static generation with revalidation) and keep only cart and checkout interactions client-rendered.
Making sure product data lands in the server-rendered HTML
If you're on the Composable Frontend / Next.js App Router architecture, the fix is usually structural, not cosmetic:
-
Fetch the product server-side. In the product route's Server Component, call the Shopper Catalog API's
GET /catalog/products/{product_id}endpoint (or filter the product list endpoint on itsslugattribute if that's how your routes are keyed) directly in the component body, not inside auseEffect. Server Components run on the server by default in the App Router, so this data is part of the initial HTML response. -
Populate
generateMetadatafrom the same data. Next.js'sgenerateMetadatafunction runs server-side and lets you set<title>, meta description, canonical URL, and Open Graph tags from the product record before the page is streamed:
export async function generateMetadata({ params }: { params: { productId: string } }) {
const product = await getProductById(params.productId);
return {
title: product.attributes.name,
description: product.attributes.description,
alternates: { canonical: `https://example.com/products/${params.productId}` },
openGraph: {
title: product.attributes.name,
images: [product.main_image?.link?.href].filter(Boolean),
},
};
}
-
Render the visible price, attributes, and availability from server data, not from a client-side price API call layered on top. If pricing genuinely must be personalized per account, render a server-fetched list price as the default and let the client call refine it — don't leave the field empty until JavaScript runs.
-
Emit
ProductJSON-LD from the server-rendered payload, inside the same component that renders the visible price and title, so the structured data and the visible content can't drift apart:
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Product",
"name": "Example Product",
"sku": "EX-1001",
"description": "Example product description.",
"offers": {
"@type": "Offer",
"priceCurrency": "USD",
"price": "49.99",
"availability": "https://schema.org/InStock"
}
}
</script>
- Watch your caching layer. If pages are served through ISR, a CDN, or edge caching in front of Elastic Path's APIs, confirm the cached HTML is the server-rendered variant, not a pre-hydration shell that depends on a follow-up client call. A stale cache of an empty shell is just as invisible to crawlers as no SSR at all.
If you're on a different frontend stack — a custom Node/Express app, a legacy MVC layer, or a different meta-framework — the same principle applies regardless of the templating engine: whatever calls the Shopper Catalog API needs to run before the HTML leaves your server, not after the browser paints the page.
How to validate
Don't trust what you see in a browser tab — the rendered DOM there already reflects JavaScript execution. Instead:
- View the raw response, not the DevTools Elements panel. Use
curl(which never runs JavaScript) to see exactly what the server sent:
curl -s https://example.com/products/example-product | grep -i "product-title\|application/ld+json\|<title>"
If the product name, price, and JSON-LD block are present in that output, they're server-rendered. If you only see a shell and a bundle script, they're not.
-
Compare view-source to the rendered DOM. In Chrome,
Cmd+Option+U(orview-source:) shows the raw HTML; the Elements panel in DevTools shows the DOM after JavaScript runs. If the price or description appears in Elements but not in view-source, it's client-rendered. -
Run the page through Google's Rich Results Test, which fetches and renders the page the way Googlebot does and will flag missing or invalid
Productstructured data. -
Check response timing and status. A
curl -Ion the product URL should return a200with real HTML in the body, not a200with a near-empty payload that only resolves after client-side fetches — that distinction matters for both SEO crawlers and any AI agent that fetches the URL directly rather than rendering it.
Verified as of July 2026 against Elastic Path's Composable Frontend example apps and Shopper Catalog API documentation; confirm current field names and route conventions against your Composable Frontend version, since starter-kit scaffolding and API response shapes do evolve between releases.
None of this addresses where the product data in that HTML comes from in the first place. Anglera enriches product records in the PIM or catalog you already run — attributes, specs, use-cases, identifiers — continuously and at scale, so the server-rendered page described above has genuinely complete data to render rather than a handful of manually maintained fields. Your PIM stores the data; Anglera does the work of keeping it rich enough to be worth rendering server-side.
