Skip to main content
StackraStackra
Verified
Case File

How Stackra Made a React App Visible to AI Without Next.js

8 min readApril 20, 2026Verified against codebase

AI tools like ChatGPT could not actually read our pages, so we were invisible to them. The usual fix is to rebuild your whole site on a different setup, which takes weeks.

We found a much smaller fix instead. Here is the story, the numbers, and how to decide what is right for your own site.

AIndustry
B2B SaaS for small and mid-sized businesses
BStack
Vite + React (SPA), Express, Playwright, TypeScript
COutcome
Same content visible to humans and AI crawlers, no render server in production, two days of focused work.

At a glance

AI crawlers were seeing an empty HTML shell on every marketing page.

3
Files changed
in the core path; a handful more for validation
+30-60s
Build-time cost
captures every marketing route at build
0ms
Per-request render cost
production just serves static files
42
Indexable routes covered
13 marketing + 29 blog articles
Easy
Reversibility
delete the script and one server check
1-2 days
Initial dev time
vs weeks for an SSR framework migration

Three pain points, by site type

Tap the one that sounds like your site to read the full story.

1

AI tools see a blank page when they fetch your site

Affects: Any single-page React, Vue, or Angular app. Vite, Create React App, Gatsby in CSR mode, Nuxt without SSR, plain Webpack SPAs.

If this is you

If someone asks ChatGPT, Claude, or Perplexity about your business, the answer is built from what those tools can read in your raw HTML. If your homepage is a React SPA, that is almost nothing.

What it looks like

Run `curl -s https://yoursite.com | grep 'your headline'` and the headline is missing. The page is just a `<div id="root"></div>` and a script tag. AI crawlers reading that conclude your site has no content.

What we tried first

Nothing for a while. The site looked perfect in a browser, so the problem was invisible until we ran the same crawl an AI tool would.

What worked

A build-time step that visits every public page in a headless browser, waits for content to render, and saves the resulting HTML to disk. Production serves those static files directly. AI crawlers see the same content humans see.

Result

Every marketing page on stackra.app now returns full HTML to direct fetches. Verifiable with `curl.exe -s https://stackra.app/about/ | Select-String 'Stackra'`: you get matches. Try the same command on a site that has not solved this and you get nothing back. No error, just silence. That silence is what Google and every AI crawler sees too. To see what a crawler actually receives, drop the filter: `curl.exe -s https://yoursite.com/`. If the output is a single line with a script tag and a `<div id="root"></div>`, your content is invisible to bots. If you see headings, paragraphs, and real text, you are in good shape. One more thing to check in that output: look for `<meta name="robots" content="noindex,nofollow">`. That tag tells every crawler not to index the page and not to follow its links. It is easy to leave in by accident from a staging environment, and any page carrying it will not appear in search results or AI citations regardless of how good the content is.

2

Pages that load data render empty during the build

Affects: Any prerender setup where the build runs without your production API attached. Common with Astro, Gatsby, custom Vite builds, and headless-CMS sites that fetch at runtime.

If this is you

If your homepage shows live data (pricing, listings, stats, search results), prerendering it the naive way captures the loading spinner instead of the data. Crawlers index 'Loading...' and nothing else.

What it looks like

Our Benchmarks page tried to fetch from `/api/benchmarks/stats` during the build. The build server had no backend attached. Fetch failed. TanStack Query returned an empty state. We captured an 18KB shell with no grade bars, no scores, no industry data.

What we tried first

Two failed attempts. First we tried injecting JSON into the captured HTML after the fact: HTML is not React, so the data sat in a script tag and never made it into the page. Then we tried injecting data through Playwright's `addInitScript` so it was present before the page booted: TanStack Query treated the seeded data as stale and triggered a background refetch that timed out.

What worked

Add a single proxy route to the build-time static server. Any request to `/api/benchmarks/stats` during the build forwards to the live production API. Real data, real render, real captured HTML. We also considered intercepting the request inside the headless browser, or telling the data layer to never refetch. Both work. The proxy was cleanest because it matches what production actually does.

Result

The Benchmarks page now prerenders with full content: grade distributions, pillar averages, and platform breakdowns all present in the raw HTML.

3

New pages silently disappear from your prerender

Affects: Any setup where the route list is in a separate file from the router. Custom prerender scripts, sitemap generators, and any tool that needs to be told what to render.

If this is you

If your team adds new marketing pages over time, you will eventually deploy a page that works perfectly in the browser but is invisible to bots. The build does not warn you. The page just does not exist as far as crawlers are concerned.

What it looks like

We added seven marketing pages over a few months: the GEO page, four platform pages, privacy, and terms. Every one of them rendered fine in a browser. None of them were in the prerender route list. All seven were served as the SPA shell to anything that did not run JavaScript.

What we tried first

We caught this manually by running curl against each page and noticing the response was suspiciously small.

What worked

Centralize the route list in one place, then add a validation step at the end of every build that opens each captured file and checks three things: is it bigger than a minimum size, does it contain an `<h1>`, and does it contain the brand name. If any check fails, the build exits with an error.

Result

The build now catches missing routes and silent renders before they ship. A captured file under 5KB for a marketing page or under 3KB for a blog article fails the build.

About Stackra

Stackra is a website audit for small and mid-sized businesses, with a free plan plus paid Starter and Pro tiers. Our own site is a React single-page app served by Express, around 42 marketing and blog routes in total. We hit this problem on our own site, which is why we have a story to tell about it.

Why we did not switch to Next.js or Remix

Server-side rendering frameworks like Next.js, Remix, and Nuxt solve the same problem by rendering HTML on a Node server for every request. They are the industry default answer for AI-visible websites. We considered them and decided against, for four reasons.

  • Our marketing pages are essentially static. The same HTML works for every visitor. SSR's main value, dynamic per-request HTML, did not apply to anything we needed crawlers to see.
  • Migrating to Next.js or Remix is a real architectural change: routing rewrite, data-fetching refactor, layout migration. Adding a build-time prerender step on top of our existing setup is additive, not architectural.
  • Static HTML on disk is the cheapest, fastest, most cacheable response a server can produce. SSR adds a render server we have to run, monitor, and pay for.
  • The pages on Stackra that genuinely need dynamism, like the report viewer and dashboard, are intentionally not indexable. They never needed SSR for SEO.

What our corpus says about shipping HTML

We made this call on reasoning in April, then measured it afterwards. Across 7,203 US hand-built sites, custom code with no CMS, the same shape as stackra.app, we classified each site's raw HTML before any JavaScript ran and compared that against real-world Core Web Vitals.

Core Web Vitals pass rate by what the raw HTML actually contains
Ships readable HTMLNo readable content, no mount pointEmpty shell, JavaScript required0%20%40%60%80%

Source: Stackra corpus, 7,203 US hand-built sites with Chrome UX Report field data, classified from raw HTML captured in the 2026 Q2 crawl. 86.9% ship readable HTML, 9.0% are empty shells, 4.1% land in the middle bucket.

Two honest limits on that chart. It measures whether HTML arrives, not how it got there, so prerendering and SSR both land in the first bar together. And it is a correlation across sites rather than a controlled test: a client-rendered app is usually a heavy JavaScript app, and that weight plausibly drives both the empty shell and the slow score. What it does establish is that shells are not a rare edge case at 9% of custom-built sites, and that they are the slowest group by a wide margin.

Effort and footprint, side by side

The honest cost comparison between migrating to SSR and the prerender setup we shipped. The prerender numbers are actuals from our codebase. The SSR numbers are a realistic estimate of migrating a Vite + React app of similar surface size.

SSR migration vs Stackra's prerender approach
DimensionSSR (Next.js / Remix)Prerender (what we shipped)
Initial dev timeWeeks1-2 days
Files changedHundreds across routing, layouts, data hooks3 in the core path; a handful more for validation and route centralization
Production infrastructureNode render server (often per region)Static files served by the existing Express layer
Per-request CPU costRender every requestZero. Send a file.
Build-time costStandard bundle build+30-60 seconds for the headless browser pass
Cache strategyEdge cache + revalidation rulesHTML on disk, no special cache layer
Failure modes you'll hitRender server outages, hydration mismatches, render-time memory leaksRoute-list drift, missing API stubs, headless browser quirks
ReversibilityHard. SSR changes data fetching everywhere.Easy. Delete the script and the static.ts check.

Effort estimates assume an existing Vite + React SPA of similar surface size to Stackra (around 42 indexable routes including the blog). Modern edge runtimes have lowered SSR's operational cost; the trade-off is real but smaller than it used to be.

How to know if this fits your site

Match the shape of your site to the rendering model. Use this as a starting point, not an absolute rule.

Platform / site-shape decision matrix
Site shapePrerenderSSRWhy
Marketing site (a few hundred routes)RecommendedOverkillSame HTML works for everyone. SSR's per-request value does not apply.
Blog with steady cadenceRecommendedNot neededDeploy-per-change cadence is fine. Static files are the cheapest response.
Documentation site, frequent updatesWorkableRecommendedHundreds of routes plus deploy-on-merge cadence stresses prerender build times.
E-commerce, hundreds of SKUsWorkableRecommendedEither works. SSR wins if inventory or pricing changes between deploys.
E-commerce, tens of thousands of SKUsTop SKUs onlyRecommendedBuild-time render of every variant is impractical. Hybrid often makes sense.
News, live events, hourly updatesNot viableRecommendedDeploy-per-update is infeasible. Per-request rendering is the only honest answer.
Personalized or auth-aware indexable pagesNot viableRecommendedHTML varies per visitor or per logged-in user. Per-request render needed.
SaaS product behind authIrrelevantIrrelevantNot indexable by design. Pick on operational preference; SEO is not the deciding factor.
Already on Next.js, Remix, or NuxtAvailableOne config flagMigration cost is zero in either direction.

"Recommended" means: this is the default unless you have a specific reason otherwise. "Workable" means: it can work, but the other column is usually less painful at this scale.

When prerendering is the wrong call, starting with ecommerce

Prerendering suits our site because every indexable page shows the same HTML to every visitor. Plenty of sites are not shaped like that, and an online store is the clearest example of the mismatch.

  • Regional pricing and currency. A brand selling into six markets needs different HTML per market, and a build cannot resolve which visitor is asking.
  • Live inventory. A prerendered product page can still say in stock after the last one sold, and serving a stale page is worse than a slower first byte.
  • Catalogue size. Five thousand products across six locales is thirty thousand build-time renders, repeated on every deploy.
  • Shipping and returns that change by destination. Same problem as pricing, and it usually sits right on the product page.

One thing worth clearing up, because it comes up every time this decision is discussed: server-side rendering does not make your pages heavier. The JavaScript bundle is the same either way. A client-rendered page ships an empty shell plus that bundle, a server-rendered page ships the content plus the same bundle, and the difference between them is a few kilobytes of markup. What SSR actually costs is time to first byte, because the page is assembled per request instead of being served from cache. That is the real trade to weigh, and payload size is not.

If you run a store, do not read our decision as a recommendation to prerender. We picked it because our marketing pages are identical for everyone. Yours are not, and per-request rendering is the honest answer for a catalogue that changes between deploys.

What we'd tell another small team

Two lessons that did not make it into the pain points:

  • Pick the rendering model that matches the shape of your site, not the framework with the loudest fan club. Most marketing surfaces do not need SSR.
  • Test like a crawler. The fastest way to find out what AI tools see is to run `curl` against your own pages and look at the raw response. If the headline is not there, the page is invisible.

Frequently asked questions

What is prerendering, and how is it different from server-side rendering (SSR)?
Prerendering runs a page once, at build time, in a headless browser, and saves the resulting HTML to a file that a server can send to anyone who asks. SSR renders the HTML fresh on a server for every single request. Prerendering trades per-visitor freshness for zero per-request cost, which is the right trade for pages whose content does not change between deploys.
What is a single-page application (SPA), and why is it invisible to AI crawlers?
A single-page application ships a mostly empty HTML file with a script tag, then builds the actual page in the visitor's browser using JavaScript. A crawler that reads the raw HTML without running that JavaScript sees just the empty shell, not the headline, text, or links a human sees. AI tools and many bots fetch raw HTML this way, which is why an unprerendered SPA can be functionally invisible to them.
What does a noindex meta tag do, and how does it affect AI visibility?
A `<meta name="robots" content="noindex,nofollow">` tag tells every crawler not to index the page and not to follow its links. A page carrying that tag will not appear in search results or be cited by an AI tool, no matter how good its content is. It is easy to leave in by accident from a staging environment, so it is worth checking for directly in a page's raw HTML.
What is hydration, and why can it break a prerendered page?
Hydration is the step where a client-side framework like React attaches its interactive behavior to server-rendered or prerendered HTML already sitting in the page. If the data a page needs is injected the wrong way, for example after the HTML is already captured, the framework's data layer can treat it as stale and refetch it, sometimes timing out and leaving the page blank for anyone who cannot run JavaScript.
When does a site actually need SSR instead of prerendering?
SSR earns its cost when a page's HTML has to differ per visitor or per request: personalized content, logged-in dashboards, live inventory or pricing, or pages that update more often than a team deploys. A marketing site or blog where the same HTML works for every visitor gets no benefit from rendering it fresh on every request, which is why prerendering fits most marketing surfaces better.
Does server-side rendering make pages heavier than prerendering?
No. The JavaScript bundle is identical either way. A prerendered page and a server-rendered page both ship real HTML plus that same bundle, so the payload difference between them is a few kilobytes of markup. The real difference is where the render happens: prerendering serves a file from cache, while SSR assembles the page per request, which costs time to first byte. Weigh SSR on freshness and per-request variance, not on page weight.
Should an ecommerce store prerender its product pages?
Usually not, at least not all of them. Product pages tend to carry regional pricing, currency, live inventory, and shipping terms that vary by destination, and all of that has to be resolved per request rather than at build time. A store selling into several markets multiplies the problem, because every product has to be built once per locale on every deploy. Prerendering a small set of stable top pages can still make sense as a hybrid.
LB
Luke Beck
Founder, Stackra
Last verified April 20, 2026

From the founder

Real tips. Occasional rants.

Dispatches from a founder driving a car while it's being built. No polish, no content calendar. Just what's actually working.

No spam. Unsubscribe any time.

Verdict

Want to see what bots see on your site?