Server-side rendering on commercetools: making product data visible to Google and AI
commercetools is headless, so nothing renders until your frontend does. Learn how to verify product data lands in server-side HTML, not just the browser DOM.

commercetools is a headless, API-first platform: it has no built-in storefront, no templates, and no opinion about rendering. Every product page you have is generated by a frontend layer you or a partner built on top of it — commercetools Frontend (the Next.js-based composable storefront), a custom Next.js/Remix/Nuxt build, or something else entirely. That means the question "will Google and AI crawlers see my product data?" isn't a commercetools question at all — it's a question about how that frontend layer renders. This guide covers how to check, and how to fix it if the answer is no.
Why this matters more than it used to
Googlebot has gotten good at executing JavaScript, but it does so in a second, delayed rendering pass, and it's still the exception among crawlers, not the rule. AI crawlers that feed ChatGPT, Claude, and Perplexity — GPTBot, ClaudeBot, PerplexityBot, and similar — fetch the raw HTTP response and parse it as text. They do not run a browser engine and do not execute your JavaScript bundle. If your product's name, price, specs, and availability only appear after client-side data fetching and hydration, Googlebot might eventually see them; most AI agents never will. For a category page or PDP, that's the difference between being cited (or ranked) and being invisible.
How commercetools product pages actually get rendered
There's no single answer here, because commercetools is composable by design — but two patterns cover most implementations:
commercetools Frontend (the official Next.js-based storefront). This is built on Next.js and its App Router, with an "API Hub" acting as a backend-for-frontend that relays product, category, and layout data to the frontend at request time. Because it's Next.js, individual routes can be rendered a few different ways — server-rendered per request, statically generated at build time, incrementally revalidated, or rendered client-side — and the choice is made per page, not globally. The commercetools documentation is explicit that this flexibility is there so teams can "optimize for performance and search engine optimization (SEO) as needed" — in other words, SSR isn't automatic just because you're on commercetools Frontend. It has to be the mode you actually chose for the product detail route.
Custom frontends. Many commercetools merchants build their own storefront directly against the Product Projections / GraphQL API, using Next.js, Nuxt, SvelteKit, Astro, or a fully custom stack. In these builds it's even more common to see the PDP shell (header, layout, chrome) delivered as static HTML while the actual product data — title, price, attributes, structured data — is fetched client-side after the page loads, often from a useEffect or a client-side data hook. That pattern is invisible to non-JS-executing crawlers no matter how good your commerce backend is.
The commercetools Next.js migration guidance for the Store Launchpad reference storefront is a useful signal of the direction here: recent versions moved away from patterns like getServerSideProps toward fetching data directly inside Server Components and using the App Router's generateMetadata function for page metadata — both of which keep data-fetching and metadata generation on the server, in the initial HTML response, rather than in the client bundle.
What "in the server-rendered HTML" actually means
Two checks matter, and they're different checks:
- Is the core content (product name, price, description, key attributes) present in the HTML that the server returns, before any JavaScript runs?
- Is structured data (JSON-LD
Productschema) present in that same server response, not injected afterward viadocument.headmanipulation or a client-side script?
If a Next.js route is genuinely server-rendered (or statically generated / ISR'd) and the product data and JSON-LD are produced inside a Server Component or a generateMetadata/metadata export, both checks pass. If the route is a client component that calls the commercetools API from the browser after mount, both checks fail — even though the page "looks right" once you load it in a browser, because a browser executes JavaScript and a non-JS crawler doesn't.
A minimal example of what a Server Component doing this correctly looks like:
// app/products/[slug]/page.tsx — Server Component, runs on the server
import { fetchProductBySlug } from "@/lib/commercetools";
export async function generateMetadata({ params }) {
const product = await fetchProductBySlug(params.slug);
return {
title: product.name,
description: product.metaDescription ?? product.shortDescription,
openGraph: { images: [product.heroImage] },
};
}
export default async function ProductPage({ params }) {
const product = await fetchProductBySlug(params.slug);
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify({
"@context": "https://schema.org",
"@type": "Product",
name: product.name,
description: product.shortDescription,
sku: product.masterVariant.sku,
offers: {
"@type": "Offer",
price: product.price.amount,
priceCurrency: product.price.currencyCode,
availability: product.inStock
? "https://schema.org/InStock"
: "https://schema.org/OutOfStock",
},
}),
}}
/>
<h1>{product.name}</h1>
<p>{product.shortDescription}</p>
</>
);
}
The failure mode looks almost identical in the browser but is fundamentally different on the wire: a client component with a "use client" directive at the top, fetching product inside a useEffect or a client data-fetching hook, and rendering the JSON-LD only after that fetch resolves.
How to validate
Don't trust what you see in the browser — a rendered DOM (via DevTools "Elements" tab or "Inspect") always looks complete, because your browser already ran the JavaScript. Check what a crawler actually receives instead:
- curl the URL directly and search for your product data in the raw response:
If the product name and JSON-LD price fields show up here, they're in the server response. If you only see a loading skeleton, an empty root container, or a script bundle with no product markup, it's client-rendered.curl -s https://yourstore.com/products/some-product-slug | grep -i "priceCurrency\|<h1" - View-source vs rendered DOM: open
view-source:https://yourstore.com/products/...(the raw HTML, no JS executed) and compare it against the DevTools Elements panel (the JS-executed DOM). If content exists in Elements but not in view-source, that content is invisible to non-JS crawlers. - Google's Rich Results Test (https://search.google.com/test/rich-results) and the URL Inspection tool in Search Console will show you Google's own rendered version and flag missing or malformed
Productstructured data — useful for the Google case specifically, though it won't tell you what AI crawlers see, since they don't render at all. - Fetch as a non-JS agent: a plain
curl -A "GPTBot"orcurl -A "ClaudeBot"request approximates what those crawlers get — it's the same raw HTML as the plain curl above, since none of them run JavaScript.
Verified as of July 2026 against commercetools Frontend Architecture and Stack, the Store Launchpad Next.js 14 migration guide, and Google Search Central's JavaScript SEO documentation; commercetools plan- and version-specific behavior (API Hub, App Router adoption) may vary by implementation, so confirm against the specific storefront codebase in use.
None of this changes what commercetools itself is responsible for: it's still the source of truth for the enriched attributes, specs, and identifiers that need to end up in that server-rendered HTML. That's the part Anglera works on continuously — enriching product data in your PIM or commerce platform so it's complete and current — so that once your frontend is rendering it correctly server-side, there's something genuinely rich for Google and AI agents to read.
