Part 1 of this series covered why React apps are invisible to AI crawlers and how static prerendering fixes it. This part covers what happens when prerendering itself breaks. Specifically, when a page that depends on an API call renders empty because no API is available during the build.
The problem: no backend during prerendering
The Benchmarks page at Stackra loads its data from /api/benchmarks/stats, an Express endpoint that queries aggregated stats from the database. During prerendering, Playwright visits the page through a local static file server. That server serves the built frontend files but has no Express backend attached. When React fetches /api/benchmarks/stats, the static server has no route for it, so it returns the index.html fallback. TanStack Query receives HTML instead of JSON, treats it as an error, and the page renders in its empty/loading state. That empty state is what gets captured and saved.
-
TanStack Query ↗
Data-fetching library for React that handles caching, background refetches, and loading states.
-
Stackra Benchmarks page →
The page that was rendering empty during prerendering — now fixed with the proxy approach.
Attempt 1: Inject data into the captured HTML (failed)
The first instinct was straightforward: fetch the benchmark data separately during the build, then inject it as a JSON blob into the already-captured HTML string before saving it to disk. This fails for a fundamental reason. HTML is not a React app. By the time you have a captured HTML string, the React rendering cycle is over. Injecting JSON into a static string doesn't re-run React, doesn't populate the DOM, and doesn't update any state. The grade bars and pillar scores remained absent from the output. The page was still empty.
Attempt 2: Inject data before rendering via window variable (failed)
The second approach was more sophisticated. Playwright has an addInitScript API that runs code in the browser context before the page loads. The idea: before navigating to /benchmarks, inject the benchmark JSON into a window variable. React boots, TanStack Query reads the window variable as its initialData, and the page renders with real data before Playwright's networkidle capture fires.
This almost worked. TanStack Query killed it.
TanStack Query treats initialData as immediately stale unless you set initialDataUpdatedAt to a recent timestamp. The moment the page loaded, a background refetch kicked off to /api/benchmarks/stats, which in the prerender environment still had no backend to answer it. Playwright's networkidle wait sometimes caught the page mid-refetch in a loading state, sometimes after it had re-rendered with the window data. The result was inconsistent captured HTML, and on the deploy that mattered, it captured the loading state.
-
TanStack Query: initialData docs ↗
How TanStack Query treats seed data and when it triggers background refetches to replace it.
-
Playwright addInitScript ↗
API for injecting code into the browser context before a page loads — the approach we tried and abandoned.
What actually worked: proxy the API in the static server
The real fix was to stop working around the missing API and just make the API available. A single proxy route added to the prerender's local Express static server: any request to /api/benchmarks/stats gets forwarded to the live production API, which returns real JSON. The page's fetch resolves successfully. TanStack Query gets a proper response. React renders normally. Playwright captures a fully-rendered page with real data.
- Add an Express route to the prerender static server before the catch-all static file handler
- The route forwards the request to the live production URL using a standard fetch call
- Return the proxied JSON response to the page
- TanStack Query gets a successful response with no fallbacks, no timing issues, and no tricks
-
Express.js ↗
Node.js web framework used to add the API proxy route in the prerender static server.
The code (simplified)
In the prerender script's startStaticServer() function, add this before app.use(express.static(...)):
app.get('/api/benchmarks/stats', async (_req, res) => { const r = await fetch('https://yoursite.com/api/benchmarks/stats'); res.json(await r.json()); });
The broader lesson
Prerendering works when the page works. If your page depends on an API call, that API call needs to succeed during prerendering. The simplest way to guarantee that is to make the API available rather than engineer around it. For any data-driven page that needs prerendering, add a proxy route in your prerender server for each API endpoint the page calls. It's a few lines of code and it eliminates an entire class of prerender failures.