Hydrogen is a great storefront framework. It's also where the largest share of invisible-to-AI Shopify stores come from. The reason isn't Hydrogen itself — it's that the default development pattern scatters product facts across client components that fetch data after hydration. GPTBot doesn't hydrate. This post is the playbook that restores Hydrogen storefronts to full crawler coverage without rewriting in Liquid.
The default-Hydrogen problem
Run curl against a Hydrogen PDP with a GPTBot user agent and you'll see a strange response: the product name, price, and a truncated description come through, but the review summary is an empty <div data-island="reviews"></div>, the upsells section is a skeleton loader, the FAQ is nowhere to be found, and the metafield specs block is a placeholder. Everything below the fold that shoppers ask AI engines about — and that the Liquid equivalent would render server-side — is client-only.
This is because Hydrogen starter templates (official Shopify templates and popular community starters alike) treat reviews, upsells, bundles, and spec tables as React components that fetch data in useEffect. On a browser with JavaScript enabled, everything renders fast and looks great. On GPTBot — which doesn't run JS — you're shipping a landing page with half the facts missing.

Why AI crawlers don't hydrate
GPTBot, PerplexityBot, and ClaudeBot are HTML fetchers, not browser engines. They fetch your URL, parse the response body, read the <script type="application/ld+json"> tags, and extract text content. They don't run React. They don't wait for hydration. They don't fire your client-side data fetches.
This is an economic choice, not a technical limitation. OpenAI fetches millions of URLs per day for training and search; a headless browser fleet that executes JS on every one would cost an order of magnitude more. They've made the trade-off that the web should ship first-byte content, and any framework that doesn't is treated accordingly.
Google is the main exception — Googlebot does render JavaScript on a second crawl pass. But even Google has a rendering queue with unpredictable latency, and if you're optimising for AI citation share (the subject of this blog), Googlebot isn't the measurement target.
The route-loader fix
Remix (which Hydrogen 2 is built on) has a clean separation between server-side data fetching (the loader) and client-side rendering. Everything returned from the loader lands in the SSR HTML. Everything fetched in a component's useEffect oruseLoaderData-driven refetch does not.
The fix is mechanical: pull every citeable field into the loader. Don't let your client components fetch product data. Batch queries via the Storefront API so you're not making ten round trips per request.

What to fetch in the loader
- Full product node — title, description, vendor, productType, tags, media, seo.
- Variants array with per-SKU metafields — avoid emitting averaged parent-level specs.
- All custom metafields your schema needs — material, warranty, certifications, countryOfOrigin.
- AggregateRating and reviews (if reviews drive citations — they usually do).
- Collection breadcrumb for itemListElement schema.
- Featured bundles, upsells, and FAQ blocks — whatever your theme surfaces below the fold.
Emit JSON-LD via the meta function
Remix's meta function runs server-side and its return value lands in the SSR HTML <head>. Use it to emit your Product schema. This is the only pattern that guarantees your JSON-LD ships in the first response:
export const meta: MetaFunction<typeof loader> = ({ data }) => {
if (!data?.product) return [];
const schema = {
"@context": "https://schema.org",
"@type": "Product",
name: data.product.title,
description: data.product.description,
offers: buildOffers(data.product.variants),
additionalProperty: data.product.metafields.map(toPropertyValue),
award: data.product.certifications,
aggregateRating: data.product.aggregateRating,
};
return [
{ title: data.product.seo.title },
{ name: "description", content: data.product.seo.description },
{ "script:ld+json": schema },
];
};Use client islands for interactivity only
Interactivity — quantity pickers, variant swatches, add-to-cart, size guides — should still be client-side React. That's the right place for them. But the source of truth for every fact a shopper might ask AI about needs to live in the loader output and the JSON-LD graph.
A good mental model: your page is a two-layer document. Layer one is the server-rendered facts and schema — built to be read. Layer two is the interactive React overlay — built to be used. Both ship in the same response, but the facts never depend on the interactions being live.
Handling Shopify apps in Hydrogen
Most Shopify reviews apps (Judge.me, Yotpo, Okendo, Stamped, Loox) work on Liquid stores via Script Tag injection. On Hydrogen, the official integration is usually a React component with a client-side fetch. For GEO this is the single biggest regression.
The fix depends on the app:
Best case — Storefront API query available
Judge.me, Yotpo, and Okendo all expose Storefront API endpoints. Call them from your loader (parallel to your product query) and fold the aggregateRating into your Product schema emission.
Fallback — synthetic aggregateRating
If the app doesn't offer a Storefront API query (rare on newer apps, common on older ones), maintain an independent reviews data store — a Shopify metaobject or a small JSON store — that your loader reads. Emit aggregateRating from that. Let the app widget hydrate in for visuals. The JSON-LD payload the crawler sees is the one that counts for citations.
Last resort — vendor pressure
If your reviews app vendor is React-only with no server-side data access on Hydrogen, it's a GEO-blocking vendor. We've seen Shopify Plus merchants swap reviews apps specifically because the incumbent vendor couldn't serve a Hydrogen migration without stripping schema from the crawler view.
A repeatable test plan
- curl -A 'GPTBot' https://yourshop.com/products/alora > hydrogen.html
- grep -c additionalProperty hydrogen.html — should be ≥ 5 for a well-covered PDP
- grep aggregateRating hydrogen.html — should contain populated ratingValue and reviewCount
- Paste the raw HTML into Google's Rich Results tool — should return a valid Product with no errors
- Run Surfient's QPA on your PDP template — if score is below 80/100, your loader coverage is incomplete
- Add a CI step that runs this test on every PR that touches a route file
Should you pick Hydrogen over OS 2.0?
Honestly: if GEO is your primary acquisition channel and your team can't guarantee the loader pattern discipline, OS 2.0 is safer. The default-Liquid stack ships correctly crawlable HTML out of the box. The failure mode is schema that's not emitted — a fix that's measured in hours, not framework migrations.
If you have a strong frontend team, a compelling reason to use Hydrogen (performance on specific surfaces, custom storefront logic, multi-region routing), and are willing to invest in loader-first development, Hydrogen can deliver better performance and equivalent GEO visibility. The choice isn't Hydrogen versus Liquid — it's whether your team treats crawler visibility as a first-class constraint or a post-launch hotfix.