Case studies

How a React or Vue Shop Gets Full Product Pages to Googlebot with RenderCache

Picture a small furniture shop built as a single-page app. The storefront is React, the catalogue comes from a JSON API, and every product URL returns the same 2 KB of HTML: a title tag, a script bundle and an empty <div id="root">. In a browser it looks fine. The name, the price, the photos and the stock level all arrive a moment later from the API.

Googlebot does run JavaScript. It just doesn’t do it straight away. The first fetch gets the raw HTML, and the page goes into a render queue that can take a while to come back round. Until it does, Google has a product page with no product on it. Bing, DuckDuckBot and Applebot each handle JavaScript their own way, and the shop owner can’t control any of them. When the catalogue changes weekly, a slow or partial render costs real listings.

This is the setup RenderCache is built for. It goes in front of the shop as a reverse proxy, or behind the shop’s nginx with only crawler requests routed to it. Shoppers keep getting the normal app. Crawler requests get the finished HTML.

A product page served to Googlebot with name, price and stock in the HTML

What the setup looks like

The shop already has a sitemap.xml listing every product and category page. RenderCache reads it, opens each page in headless Chrome against the shop’s origin, and saves the rendered document to disk. On a shop like this the useful setting is ready_selector. Point it at something that only exists once the product has loaded, say [data-price], and Chrome waits for that element instead of guessing with a fixed delay. If the product data comes from an API on another host, that host goes in render_allow_origins, because the renderer only talks to hosts you list.

Once a product page is warm, Googlebot gets it with X-RenderCache: HIT. The name, the price, the description and the JSON-LD product markup are all in the HTML on the first fetch. Scripts are stripped from the snapshot. Structured data is kept.

The parts of the shop that should never be shared stay out of it. The cart and checkout send cookies, and any request carrying a cookie or an authorization header goes straight to the origin. So does anything under bypass_paths, which defaults to /api and can take /graphql or a headless CMS path too. A page that answers with a Set-Cookie, no-store or a 401 is refused as a snapshot, and an older copy is withdrawn.

Prices change, so the cache has to follow

A shop is not a blog. Prices move, products sell out, campaigns end. A few settings do most of the work here.

snapshot_ttl and warm_interval decide how old a saved page can get before it’s refreshed. A shop that reprices daily might set both to a few hours. When an expired copy is requested, it’s served as STALE while a refresh runs behind it, so Googlebot never waits on Chrome.

The purge API handles the changes that can’t wait. The shop’s admin, or a deploy hook, sends the product URL to the protected /purge endpoint on the admin listener after a price edit. RenderCache queues a fresh render and keeps serving the old copy until the new one is ready. When a product is gone for good and the app returns a 404 or 410, that status is stored too, and crawlers get the removal rather than a stale page.

Campaign links stop multiplying pages. A product shared with ?utm_source=newsletter or a gclid maps to the same snapshot as the plain URL, because those parameters are in ignore_query_params by default.

Checking it worked

The admin page on 127.0.0.1:9090 shows coverage per sitemap page: cached, missing, stale, failing. Compare HTML puts the origin’s raw response next to the snapshot and highlights the words that only exist after rendering. On an empty React shell that’s nearly every word on the page.

Search Console’s URL Inspection tool uses Google-InspectionTool, which is in the default crawler list since alpha 2. So the test Google gives you goes through RenderCache as well, and the response receipts show it arriving.

One honest note on the method. Google calls this dynamic rendering a workaround, and server-side rendering is the longer-term answer for a shop that can afford the rewrite. RenderCache is for the shop that can’t do that this quarter, or doesn’t want a second rendering path in its codebase. Crawlers get the same content shoppers see, rendered from the same app. The quickstart runs the whole setup locally against an example app before it goes near the real shop.