Why a JavaScript-only Shopify product page is not in the Google index
Google fetches HTML first. Rendering JavaScript is a second wave that can wait, fail, or never see the files it needs. A Shopify product URL that ships an empty app shell — no title, price, or description in the first HTML — often stays out of the index even when the in-browser page looks complete. Standard Liquid themes already print those fields on the server. The failure case is a custom storefront, Hydrogen route, or app block that loads product JSON only after hydration. Put title, price, and description in the initial HTML. Confirm View Source against URL Inspection’s rendered HTML, and confirm robots.txt does not block the CSS and JS Google needs to render. A recrawl of an empty shell still gives Google an empty shell.
A short history of two-wave rendering on product URLs
Classic Shopify themes ran Liquid on the server. The first byte of a product handle already contained the name, the money amount, and the description. Googlebot could index that document from the HTTP body. That split still matches what Google’s JavaScript SEO basics describe: crawl the URL, parse the HTML, queue a render, index from the rendered HTML when the render runs.
Web Rendering Service made the second wave real and also delayed. Googlebot queues 200 pages for rendering unless a robots meta or header forbids indexing. The wait can be seconds or much longer. Not every crawler executes JavaScript. Google still calls server-side rendering or prerendering a good idea.
Custom storefronts inverted the default. Hydrogen and headless stacks can fetch the Product in a server loader — or they can leave a CSR shell and call the Storefront API in the browser. App blocks on Liquid can repeat the mistake: a placeholder that fills after fetch. SERP write-ups (CSR empty shells, delayed indexing, Hydrogen description SSR) converge on one rule: if a shopper needs the field to buy, it belongs in the first HTML.
What those posts skip: View Source is wave one; Inspection’s rendered HTML is wave two; they can disagree. robots.txt that Disallows CDN paths or app JS means Google Search will not render JavaScript from blocked files. The PDP then looks thin even when Chrome looks fine.
Why a missing product URL is a business problem
The product page is the URL you pay to rank and paste into ads. If Google never stores it, shopping queries cannot select it, and a recrawl during a sale week will not mint a result from a shell. Merchants blame “slow indexing.” The usual cause: the document Google can keep does not contain the offer.
A Liquid Dawn product that View Source already fills is not this ticket. The ticket is JavaScript-only primary content: title, price, description, and often JSON-LD, absent from the HTTP body.
SpeedyIndex can send Googlebot Smartphone to the URL. If the first HTML is still an empty root node, selection has nothing useful to store. Pay-per-Result refunds unindexed URLs. It does not SSR your theme.
The SpeedyIndex team treats a JS-only product URL as a render and HTML problem first. Compare View Source with Inspection’s rendered HTML, allow the CSS and JS the renderer needs, put title, price, and description in the first response, then submit.
That sequence keeps tokens attached to pages that can enter the index.
A nine-step workflow for one Shopify product URL
Use one canonical product address. Ignore collection filters and ?variant= twins until the PDP is indexable.
-
Confirm the URL is absent, not merely unranked. Action: record GSC coverage and a public retrieval test of the exact URL. Tool: Search Console URL Inspection; a Google index checker. Setting: full product handle URL; dated log; third-party checks do not need GSC verification. Observable: URL is not on Google / crawled not indexed versus a live yes. When: before theme edits and before any submit. Success: you know it is missing from the index, not sitting on page two. Failure: you rebuild SSR because the product ranks 18th.
-
Split View Source from the live DOM. Action: search View Source (or curl) for the product title, the price, and a sentence from the description, then search the live Elements panel. Tool: View Source; curl; DevTools Elements. Setting: logged-out, default variant. Observable: title, price, and description present in source, or only in the hydrated DOM. When: first technical step on any headless or app-heavy PDP. Success: the three fields exist in the raw HTML. Failure: source is an empty shell; Elements is a full PDP.
-
Read URL Inspection rendered HTML and the screenshot. Action: Test live URL, then open View tested page. Tool: Search Console URL Inspection. Setting: same canonical as step 1. Observable: rendered HTML contains the offer fields; screenshot is a product, not a spinner. When: right after the source vs DOM split. Success: wave two matches the shopper page, and wave one already had the main fields. Failure: screenshot empty, or rendered HTML still lacks title, price, or description.
-
Check robots, noindex, and canonical before blaming React. Action: fetch headers and HTML for
noindex/X-Robots-Tag, self-canonical, password gates, androbots.txtDisallow on/products. Tool: curl;robots.txt.liquid; a noindex tag checker. Setting: published product, not a draft. Observable: indexing allowed; no password; canonical is this PDP. When: in parallel with step 2. Success: remaining work is HTML and render. Failure: “hidden from search engines,” appnoindex, or canonical to a collection. -
Audit robots rules that block CSS and JS the renderer needs. Action: open the shop’s
/robots.txtand look for Disallow on CDN,/assets/, app proxies, or hashed.js/.css. Google’s JS guide states Search will not render JavaScript from blocked files or on blocked pages. Tool: live robots.txt; GSC robots tester. Setting: do not paste a “block all assets” snippet intorobots.txt.liquid. Observable: Googlebot may fetch the JS and CSS that paint the PDP. When: any custom robots file or broad Disallow aimed at scrapers. Success: product path and renderer assets allowed; cart, checkout, search, and account stay blocked. Failure: you blocked the CDN and Google stores a broken layout. -
Put title, price, and description in the first HTML. Action: for Liquid, print them in the product template; for Hydrogen, fetch
title, price, anddescriptionordescriptionHtmlin the route loader and render during SSR; remove a client-only fetch that is the only source of those fields. Accordion UI can stay; the text must already exist in the server HTML. Tool: theme editor / Liquid; Hydrogen loader and Product fragment. Setting: default variant price in the first response; description from the product editor, not a metafield loaded only inuseEffect. Observable: curl of the PDP shows the three fields without JavaScript. When: after robots and noindex are clean. Success: wave one contains the offer. Failure: you add a spinner and hope the render queue is fast. -
Emit crawlable links and JSON-LD in the same first response. Action: collection → product links as
<a href>to/products/handle; print Product JSON-LD in Liquid or SSR HTML, not only after an app injects it. Tool: theme product section; Hydrogen document head; Inspection. Setting: one Product graph; disable duplicate app JSON-LD. Observable: source HTML contains product links and Offer fields when you need rich results. When: same release as step 6. Success: Google can discover sibling products without click handlers. Failure: hash routes or buttons withouthref. -
Fix HTTP truth for missing products. Action: unknown handles must not return 200 with an empty shell; return 404 or redirect to a server 404. For client routers, Google documents a JS redirect to a server 404 or a
noindexon true error states to avoid soft 404s. Tool: Hydrogen loader status; Liquid availability. Setting: out of stock can stay 200 with honest availability; unknown handles should not. Observable: junk handles no longer return 200 with a shell. When: coverage mentions soft 404, or curl of a junk handle is 200. Success: real products 200 with content; missing products 404. Failure: the SPA prints “Product not found” with 200 and nonoindex. -
Recrawl once, then recheck on day 7. Action: Request indexing once in GSC after curl shows the offer; optional SpeedyIndex submit with precheck. Tool: URL Inspection; SpeedyIndex Standard or Drip-Feed. Setting: precheck drops 404/410/451, robots/
noindex, already indexed; crawler Googlebot Smartphone; 1 indexed URL = 100 tokens; Google recheck day 7; Yandex day 15; auto refund if still absent; trial 200 tokens. Observable: new last-crawl; later a yes on the index checker. When: only after steps 4–8. A second-wave delay is not a reason to submit the old shell. Success: indexed, or a refunded token and a leftover render bug still visible in curl. Failure: daily submits of the CSR placeholder.
How source, Inspection, robots, and queues compare on one PDP
View Source, live Inspection, robots.txt, a noindex check, and a Pay-per-Result queue measure different layers of the same product URL.
View Source / curl (first HTML)
- What it answers: whether title, price, and description exist before JavaScript
- What it cannot answer: delayed render-queue success
- When to run it: every headless or app-block PDP
- Observable that misleads: the pretty hydrated tab
- Takeaway: if the offer is missing here, you have a JS-only document
URL Inspection rendered HTML
- What it answers: what Googlebot Smartphone painted on a live test
- What it cannot answer: that the next crawl will wait long enough to render
- When to run it: after SSR work and robots edits
- Observable that misleads: “page is available” while rendered HTML lacks the description
- Takeaway: this is wave two; treat gaps as render failures
robots.txt
- What it answers: whether Googlebot may fetch the PDP and the CSS/JS needed to render
- What it cannot answer: quality selection after a good render
- When to run it: any custom
robots.txt.liquid - Observable that misleads: “we only blocked AI bots” while a Disallow hit the CDN
- Takeaway: blocked renderer assets produce a thin or broken snapshot
noindex / header check
- What it answers: whether you forbade storage in meta or
X-Robots-Tag - What it cannot answer: empty-shell omission when indexing is allowed
- When to run it: apps, password stores, “hide from search” toggles
- Observable that misleads:
noindexinjected only after JS (Google may skip render whennoindexis in the original HTML) - Takeaway: clear directives first
Google index checker
- What it answers: public index yes/no for that exact URL on a date
- What it cannot answer: the render root cause
- When to run it: before the project and on day 7
- Observable that misleads:
site:counts for the whole shop - Takeaway: log the handle URL, not the domain count
SpeedyIndex Pay-per-Result
- What it answers: a dated Smartphone crawl, day-7 recheck, token refund if still absent
- What it cannot answer: SSR, robots repairs, or 100% indexing
- When to run it: after curl shows the offer and robots allow assets
- Observable that misleads: a refund blamed on the vendor when View Source is still empty
- Takeaway: buy process for a real HTML document
Troubleshooting a Shopify PDP that never enters the index
If View Source lacks the product title, stop tuning snippets. Move data into Liquid or the Hydrogen loader.
If Inspection’s screenshot is blank but Chrome is fine, look at blocked JS/CSS and client routing that never settles. A 200 spinner is still a bad document.
If an app and the theme both print JSON-LD, keep one server-printed Product graph.
If the store is password-gated, Googlebot gets a wall.
If collection templates lazy-load cards with no <a href> in the first HTML, Google may never discover the PDP.
If you already SSR the description and the URL stays out, that can be selection on manufacturer boilerplate. More JavaScript will not fix duplicate SKU copy.
Three illustrative scenarios (not real customers)
Illustrative scenario (not a real customer): A Dawn catalog installs a page-builder tab that replaces the description. Shoppers see 400 words. curl shows none. They print product.description in Liquid again, keep the tab as enhancement, request indexing once, and recheck on day 7.
Illustrative scenario (not a real customer): A Hydrogen storefront fetches title and price in the loader but loads descriptionHtml in useEffect. They add descriptionHtml to the Product fragment and render it on the server inside the accordion. JS only opens and closes the panel.
Illustrative scenario (not a real customer): A consultant Disallows the CDN in robots.txt.liquid. Chrome still works. Inspection screenshot is an unstyled empty column. They restore Shopify’s default allow for assets, keep checkout and cart disallowed, then recrawl.
Illustrative model (labeled): two-wave path for one Shopify PDP
This is a teaching model, not a leak of Google internals.
- robots.txt check — if the product URL is disallowed, there is no fetch. If renderer assets are disallowed, fetch can happen and render cannot.
- First-wave fetch — HTTP body. View Source. Title, price, description should already be here.
- Link extraction —
hrefon real<a>tags. No hash router. - Render queue — Chromium runs when it can. Inspection rendered HTML lives here.
- Index selection — store or omit. Empty shells and duplicate manufacturer text both lose.
- Serving — indexed is still not a rank.
A JavaScript-only product page tries to skip step 2 and gamble on step 4.
FAQ
Does Google index JavaScript Shopify pages at all? Yes, after a render, when JS is allowed and the render succeeds. Google still recommends SSR. Primary offer fields belong in the first HTML.
Is Online Store 2.0 Liquid already server-rendered? For the theme output, yes. App blocks and custom JS can still hide fields.
Why is View Source empty when the live page looks complete? The live page is the hydrated DOM. View Source is the first HTML. CSR storefronts print a shell and fill it later.
Can I rely on URL Inspection instead of View Source? No. Inspection is a live render test. Production crawls can lag. If source already has the offer, you are safer.
What if robots.txt blocks CSS and JS? Google Search will not render JavaScript from blocked files. The snapshot can look thin and stay out of the index.
Does Request indexing force a JS page into Google? No. It asks for a recrawl. Inclusion is not guaranteed. Recrawling a shell repeats the miss.
Should variant URLs all be indexed? Usually no. Canonicalize to the main product URL unless a variant is a true separate document.
How do I tell a noindex miss from a JS miss? Run a noindex/header check. If indexing is allowed and source lacks title, price, or description, you have a JS-only document.
Will SpeedyIndex render my React storefront for me? No. The crawler visits as Googlebot Smartphone. Your server must still return useful first HTML. Unindexed URLs refund; that is not an SSR service.
How long can the second wave take? Google says the render queue may be short or longer. Plan days, not minutes. Do not wait weeks on a PDP whose curl is still empty.
What two-wave product indexing will look like next
Headless Shopify will keep mixing loaders that SSR with widgets that fetch in the browser. Agents that read raw HTML will punish the same empty shells Google already punishes. Print the offer in the first response, allow renderer assets, use crawlable links, recrawl once, log day 7. Process queues will still recheck and refund. They will not become a Chromium farm for Storefront API calls.
The previous note in this series covers the step before this one: Why Google says crawled not indexed after one AI-written category page. Read that first if you landed here on a related long tail.
About SpeedyIndex
SpeedyIndex is a Pay-per-Result process. One indexed URL costs 100 tokens. The company does not sell a 100% indexing outcome.
Google jobs recheck on day 7. Yandex jobs recheck on day 15. URLs still not indexed are refunded automatically as tokens.
Standard and Drip-Feed are the two submit modes. Drip-Feed paces large lists. Neither mode ranks a product.
Optional precheck can drop 404, 410, 451, robots/noindex, and already indexed rows. The crawler presents as Googlebot Smartphone. You do not verify the shop in Search Console to run a check or a submit. A trial starts at 200 tokens.
Run that process after curl shows title, price, and description, after robots allow the JS and CSS the renderer needs, and after noindex is gone.