Server-side rendering on Shopify: making product data visible to Google and AI
How Shopify renders product pages, why client-only content hides data from Google and AI crawlers, and how to confirm it's in the server HTML.

Shopify's Storefront Renderer is server-rendered by default, but plenty of the widgets, page builders, and custom PDP sections merchants bolt on top of a theme are not. If your spec table, metafield-driven attributes, or custom description block only appears after a client-side JavaScript call, it can be invisible to search engines and to the growing set of AI agents that read raw HTML without executing scripts. This guide covers how Shopify actually builds a product page's HTML, the common ways product data quietly becomes client-only, and how to check which category your store falls into.
How Shopify renders a product page today
For standard Liquid themes (Dawn and any Online Store 2.0 theme), Shopify's Storefront Renderer processes your theme layout file, the product template, and its sections entirely on Shopify's servers before the response reaches the browser. Shopify's engineering team has described this pipeline in detail, including a full-page cache layer that lets the majority of requests return in well under 100ms at the median, with tail latency (the slowest 10%) still generally under a second — the HTML you get back already contains the rendered product title, price, description, and any Liquid-rendered metafields, images, and variant data. There's no client-side render step required to see that content; a plain HTTP request returns it.
App content follows the same model when it's built correctly. Theme app extensions render app blocks through Liquid's content_for 'blocks' tag, which means an app block's markup is generated server-side alongside the rest of the section, just like a native theme block. The older ScriptTag API, by contrast, injects JavaScript that runs in the browser and writes to the DOM after the page loads — the content it adds was never part of the HTTP response. Shopify has been winding ScriptTag down in favor of theme app extensions: it's already blocked for new use on Order Status and Thank You pages, with full retirement there on a 2025–2026 timeline, and apps that still rely on it for storefront pages risk failing App Store review even where it technically still runs.
Headless storefronts built on Shopify's Hydrogen are also SSR by default, though the underlying framework has shifted: Hydrogen ran on Remix conventions through 2024 and has since moved to React Router in framework mode (React Router 7), deployed on Oxygen hosting. In both generations, the pattern is the same — a route's loader function runs on the server, and the resulting product data is included in the initial HTML streamed to the client, with React hydrating on top of it. That guarantee holds only for data fetched inside loader. If a component instead fetches product data client-side — for example, inside a useEffect call to the Storefront API after the component mounts — that data behaves exactly like content from a client-rendered single-page app: absent from the initial response, present only after JavaScript executes.
Where product data quietly goes client-only
The rendering model is server-side by default, but four patterns commonly reintroduce client-only content on otherwise Liquid-rendered pages:
- Metafields wired up via client-side fetch instead of Liquid. Creating a metafield definition in the admin does not put it on the page — someone has to reference it in a template using Liquid's
product.metafieldsobject for Shopify to render it server-side (see the fenced example below). Several spec-table and comparison-chart apps instead query the Storefront API from the browser after page load, which is easier to build but means the attribute data isn't in the HTML response at all. - Page builder apps (drag-and-drop PDP builders) that render sections via client-side JavaScript rather than as native Liquid sections or theme app extension blocks. Worth checking per app and per section, since implementations vary.
- Reviews, Q&A, and UGC widgets that inject their content (including any embedded product attributes) through a script tag after load, rather than through server-rendered markup.
- Hydrogen/custom storefronts where product detail is fetched in a client component instead of the route loader, often to avoid a server round trip for a "load more variants" or "related specs" panel.
None of these are wrong choices for interactivity — they're wrong only when they're the sole path for content you want indexed or read by an AI agent, since a request that doesn't execute JavaScript never sees it.
Making sure product data is in the server-rendered HTML
Three practical fixes cover most cases:
- Reference metafields directly in Liquid, not through a client-side API call, whenever the value should be crawlable:
{% if product.metafields.specs.material %}
<p>Material: {{ product.metafields.specs.material.value }}</p>
{% endif %}
- Emit product structured data with Shopify's
structured_datafilter (or a manually maintained JSON-LD block) inside the product template, so Google and AI systems parsing JSON-LD get a machine-readable summary in the same response as the visible HTML:
<script type="application/ld+json">
{{ product | structured_data }}
</script>
This filter outputs a schema.org Product object for products without variants, or a ProductGroup for products with variants. If your theme already ships a hardcoded JSON-LD block (common in older Dawn-based themes), check for duplicates — two competing Product schemas on one page is a frequent Search Console warning, and Shopify's own filter won't automatically override a hardcoded one.
- Use theme app extensions and app blocks instead of ScriptTag/Asset-injected JavaScript for any app content that should be part of the page's substance rather than a widget layered on top. If you're evaluating a PDP app, ask the vendor directly whether it renders through Liquid/app blocks or through client-side JavaScript — for Hydrogen storefronts, ask whether product data loads in the route's
loaderor in a client component.
How to validate
Compare the raw response to the rendered page, per URL:
curl -s -A "Mozilla/5.0 (compatible; Googlebot/2.1)" https://yourstore.com/products/your-handle \
| grep -i "material\|application/ld+json"
If the attribute or JSON-LD block you expect doesn't show up in that output, it isn't in the server response — no matter how it looks in the browser.
- View-source vs. rendered DOM: open
view-source:https://yourstore.com/products/your-handleand search for a specific attribute value or price. Then open DevTools, inspect the same element in the live DOM, and see whether it's present in both, or only in the DOM after scripts run. - Google's Rich Results Test: paste the product URL in and check whether Google's renderer detects your
Product/ProductGroupJSON-LD and pulls the fields you expect (price, availability, brand). - Shopify's Theme Inspector for Chrome: useful for confirming which parts of a page came from Liquid render time versus client-side scripts, section by section.
Verified as of July 2026
Shopify's Storefront Renderer, Liquid theme architecture, theme app extensions, and the structured_data filter are current mechanisms per shopify.dev as of this writing; Hydrogen's SSR behavior reflects its current React Router 7/Oxygen-based architecture (having moved on from Remix in 2024–2025). Field names, app APIs, and Hydrogen conventions are subject to Shopify's normal release cadence — recheck shopify.dev before implementing on a specific theme or plan.
None of this matters if there's nothing worth rendering. Anglera enriches the underlying product data — attributes, specs, use-cases, identifiers — continuously in the background, so whichever of these rendering paths you choose, the PDP has rich, current content to put there in the first place. Your PIM stores the data; Anglera does the work of keeping it complete.
