If you're an engineering leader at a dev-tools company, an AI infrastructure platform, or an API-first SaaS, there's a good chance your marketing site is a React SPA — Vite, Create React App, or something similar — that your team built five-to-eight years ago, shipped, and never had a reason to revisit. Conversions weren't bad. The site loaded. Your engineers respected it because they wrote it. There was no fire.
Quietly, though, your marketing pages may not be in Google's index. Or they may be in the index in a degraded form that's worse than not being indexed at all. Either way, the searches that should bring buyers to your pricing page or your developer-experience post are going to your competitors instead — competitors who probably migrated to Next.js the same year you decided not to.
I want to walk through one site in detail to show what this actually looks like. Not to embarrass the team that built it — they built a real product and a real business — but because the failure mode is so widespread, and the specific shape of the failure so different from what most engineers expect, that the only way to make it concrete is to look at one site, in 2026, with current tools.
The site is , and Martin Splitt from Google has been clear since at least 2019 that there is no longer a separate "first wave / second wave" indexer. But "Google can render JavaScript" and "Google reliably indexes your SPA exactly the way you'd expect" are very different statements. In practice, JavaScript-heavy sites introduce uncertainty around crawl timing, DOM capture timing, metadata extraction, and indexing consistency. The initial HTML response provides little meaningful body content for the crawler to extract before rendering. What it does have — title, meta description, canonical URL — comes only from the <head>. Everything else is contingent on rendering completing cleanly, in the right order, within the budget Google decides to grant your site.
That budget is not unlimited and not uniform. In practice, higher-authority domains appear to receive more consistent and timely rendering than smaller domains competing for crawl and rendering resources. For a 35-person AI-infra company competing with established players, you're on the smaller side of that allocation curve.
So when beam.cloud's homepage advertises "Run sandboxes, inference, and training with ultrafast boot times, instant autoscaling, and a developer experience that just works" — that string appears nowhere in the raw HTML response. It appears only inside the React component tree that renders after JavaScript executes. Whether Google captures that string, and when, depends on a pipeline you don't observe and can't control.
2. Half the routes return a "404" title in raw HTML — while returning HTTP 200
This is the finding I wasn't expecting, and it's probably the most damaging single problem on the site.
Six of the thirteen routes — /careers, /contact, /docs, /help, /privacy, /terms — return the title 404 • Beam in their raw HTML. Not Careers • Beam. Not Privacy Policy • Beam. Just 404 • Beam.
What's happening is that beam.cloud uses react-router to handle routing client-side. When you request https://www.beam.cloud/privacy, the server returns the same 11-kilobyte SPA shell it returns for every route, with HTTP status 200 OK. The React app then loads, looks at the URL, decides there's no matching route, and renders the <NotFound /> component. That component sets document.title to 404 • Beam.
This is a textbook soft 404. The HTTP status says the page exists. The rendered page says it doesn't. treats site-wide duplicate meta descriptions as a quality signal: they tell Google your pages aren't differentiated, and Google responds by reducing the weight given to the description in snippet generation. In practice, that often means Google generates a snippet from whatever's in the body of the page — which on beam.cloud is empty — so it falls back to showing just the URL and title.
If you search site:beam.cloud today, you can see the result of this. Every snippet shows the same meta description, regardless of the page being indexed. The user clicking through has no way to tell what each page is actually about.
4. Zero structured data anywhere
There is no JSON-LD on any page of beam.cloud. Zero. No Organization schema. No WebSite. No Product for the platform. No Article schema on the blog posts. No FAQPage on /help. No BreadcrumbList.
Structured data is how a modern SEO site tells Google "this is a software product, here are its features, here's its pricing, this article is by this author, this page is a step-by-step guide." It's how rich snippets get generated. It's how AI search engines (ChatGPT browsing, Perplexity, Claude) extract entities from your page to surface in answers.
Beam has 35+ blog posts at beam.cloud/blog/* covering topics like "CUDA vs Tensor Cores," "Fine-tuning Llama 3," "Faster Whisper" — exactly the kind of high-intent technical content a buyer in the AI-infra space would be searching for. None of those posts have Article schema. None of them tell Google who wrote them, when they were published, or what topic cluster they belong to. They're competing with structured-data-enriched competitor blog posts and losing.
5. Seven of thirteen routes are missing from the sitemap
Beam has a sitemap.xml. It lists fifty-three URLs. But seven of the thirteen route templates that the SPA renders are not in that sitemap — including all six of the soft-404 routes above, plus /customers/[slug] (which has at least three real case-study pages).
This means Google has no way to discover the case-study pages except by crawling internal links — and the internal links live inside a JavaScript-rendered DOM, which the initial crawl doesn't see. So customers/geospy-case-study, customers/hooktheory-case-study, and customers/magellan-ai-case-study are much harder for Google to consistently discover and index. These are the most credibility-rich pages on the entire site — real customer stories with real results.
6. Lighthouse measurements were unstable and consistently poor
I'll mention this last because it's the least surprising finding and the one most likely to be misread.
The most telling part of the Lighthouse data is what's missing from it. On five of the thirteen surfaces — /help, /pricing, /privacy, /terms, and one of the docs templates — Lighthouse exhausted its measurement budget without producing metrics at all. When a tool that exists specifically to measure page performance gives up before producing a number, that is itself a finding. It tells you the page is so slow to reach an interactive state that synthetic measurement doesn't terminate cleanly within reasonable bounds.
On the surfaces where Lighthouse did complete, the results were poor and unstable. LCP exceeded the . What hasn't gone away — and what the simplified version of the claim glosses over — is that rendering JavaScript-heavy sites introduces uncertainty at several points:
Rendering is rationed by domain authority and crawl budget. Google does not have infinite Chromium instances. Higher-authority domains get rendered quickly and frequently. Lower-authority domains get rendered slowly, partially, or with stale results. If you're a smaller engineering-led company competing with established players, you're on the smaller side of that allocation.
DOM capture is timing-dependent. The renderer captures the DOM at some point during page load. If your content loads asynchronously after that point — fetched from an API, hydrated late, lazy-rendered — it may not be captured in the indexed version.
What gets captured is whatever exists at render time, regardless of correctness. This is the trap that catches beam.cloud's soft-404 pages. By the time the renderer captures the DOM,
react-routerhas already setdocument.title = "404 • Beam". Rendering ran successfully. What it captured was wrong.Metadata extraction happens against the rendered DOM, not your component tree. If your title, canonical URL, or structured data are injected late in the JavaScript lifecycle, what Google extracts can be different from what your developers expect to see when they view the page.
The "Google can read JavaScript" claim is true the way "I can lift 200 pounds" is true if I tell you I can do it sometimes, in the right conditions, with my back warm.
What you do about it
You move the public-facing routes — and only the public-facing routes — onto server-side rendering. Next.js is the dominant choice in the React ecosystem because it's specifically designed for this hybrid: the public marketing surfaces SSR for SEO, the authenticated application keeps running as the SPA you already have.
The migration doesn't have to be all-or-nothing. The hybrid pattern — Next.js in front, the existing SPA continuing to serve authenticated routes — is the cheapest path through. You keep your component tree. You keep your routing logic for the app. Your engineers don't rewrite product logic. They don't touch the parts of the codebase that actually deliver value to logged-in users. The marketing layer — and only the marketing layer — moves onto a renderer that gives Google what it actually needs: real HTML, with real content, on the first request.
What that looks like in practice, mapped against the six findings above: every public route returns real <h1> and body copy in raw HTML before any JavaScript executes. Non-existent routes return HTTP 404 with a server-rendered 404 page — soft 404s become impossible. Each surface has its own per-route title and meta description, generated at build or request time from the underlying content. JSON-LD is injected at render time, deterministically, on every page where it applies. The sitemap is generated from the same route definitions that produce the pages, so it cannot drift out of sync. And the surfaces that previously timed out under Lighthouse measurement reach interactive state in well under a second on a cold load. None of this depends on Google's render queue cooperating; the HTML is correct on the first byte.
That's the work I do. If your site looks anything like beam.cloud's, I run the same diagnostic against your specific site, produce a PDF report identifying every surface that needs migration, and quote a fixed-fee project to migrate the public surfaces while leaving your existing app untouched.
The diagnostic is free. Most teams I talk to are surprised by what shows up — particularly the soft-404 finding, which almost no one has seen before. If you'd like to know what your site looks like to Google in 2026, the service page at richardwrobinson.com/react-spa-seo-migration explains how the engagement works and has a 15-minute call booking right at the top.
The diagnostic tool used to generate the findings above is private, but the underlying methodology — headless Chromium with JavaScript disabled, real-Chromium Lighthouse runs, sitemap and robots.txt validation — is reproducible. If you want to verify any of the beam.cloud findings yourself, view-source on the URL and look at the body. It will take about thirty seconds.
SOCIAL SHARE CARD GENERATOR