ssrchecker
HomeCheckFrameworksReact

Is your React app server-rendered, or a client-only SPA?

Short answer
React on its own does not render on the server. Create React App and a default Vite setup both ship an empty HTML shell that JavaScript fills in, so crawlers see nothing on the first pass. Server rendering requires a framework such as Next.js or React Router in framework mode, or a prerendering layer.

This is the opposite of the Next.js situation, where server rendering is the default and the question is which route broke it. With plain React the question is whether you have server rendering at all — and for most setups the answer is no.

Check your React app
https://

The empty shell signature

A client-only React app has a recognisable fingerprint in the raw HTML. Roughly:

<div id="root"></div> <script type="module" src="/assets/index-a1b2c3.js"></script>

That is the entire page as far as a crawler is concerned. The checker will report a content ratio near zero, zero or very few links, and typically a missing or placeholder title. The rendered screenshot will show your full site, which is what makes this so counterintuitive to diagnose by eye — everything looks fine in a browser.

Worth knowing: id="root" is a convention, not a requirement. Some setups use id="app" or a custom value. The signature is the emptiness, not the specific id.

CRA, Vite and React Router behave differently

Three common starting points, three different situations — though two of them end up in the same place.

Create React AppClient only
No server rendering, and no supported path to it. react-scripts produces a static shell. CRA is also no longer actively maintained, so if you're starting a migration this is the strongest argument for doing it properly rather than patching.
Vite + ReactClient only
Identical crawler outcome to CRA — an empty shell — despite the much better developer experience. Vite does support SSR, but you have to build that layer yourself; the default template does not include it.
React Router (framework mode)Can be SSR
Modern React Router can server-render, and when configured that way it produces complete HTML. But it also has an SPA mode that behaves exactly like CRA. Which one you're running is not obvious from the dependency list — check the config.

Why deep links fail worst

This is the React-specific failure that does the most damage, and it has no real equivalent on a server-rendered framework.

A client-only React app usually deploys with a catch-all rewrite: every path returns the same index.html, and React Router decides what to show once JavaScript boots. That is what makes deep links work for users at all.

For a crawler, it means every URL on your site returns byte-identical HTML. Your homepage, your product pages, your articles — all the same empty shell with the same title. From the first pass, there is no way to distinguish them and nothing to suggest any of them is worth rendering.

How to spot it
Run the checker on your homepage and on a deep product or article URL. If both return the same word count, the same link count and the same title, you have the catch-all shell. Users get a working site; crawlers get one page repeated.
Two ssrchecker results side by side showing a React homepage and a deep product URL returning identical empty HTML
Same shell, different URLs. The crawler has nothing to tell them apart.

react-helmet cannot fix this

A very common misunderstanding, and an expensive one because it feels like a fix.

react-helmet and react-helmet-async set the document title and meta tags from within your components. That happens when React runs — which is after the crawler has already captured the initial HTML. The tags are correct in the browser and absent in the raw response.

The checker reports title, description and canonical separately from body content precisely so you can see this. A page can have perfect metadata in the rendered DOM and none at all in the raw HTML.

Helmet is not useless — it does the right thing once the page is rendered, and in a server-rendered React setup it can emit tags into the server output. But in a client-only app it cannot put anything in the first response, because nothing of yours runs before that response is sent.

Your setup and the verdict to expect

Find your configuration. A result that differs from the expectation is worth investigating.

SetupExpectWhy
Create React App (react-scripts build)CSRNo server rendering. The build produces a static shell filled in by JavaScript.
Vite + React (default template)CSRSame shell model as CRA. Faster build, identical crawler result.
React Router in framework mode with SSR onSSRRenders on the server per request. Complete HTML.
React Router in SPA modeCSRClient-only routing. Every URL returns the same empty shell.
Hand-rolled Express + renderToPipeableStreamSSRGenuine server rendering, provided the data resolves before streaming.
CRA or Vite behind a prerendering serviceSSRCrawlers receive a cached rendered snapshot. Works, but adds drift risk.
Any setup with content fetched in useEffectCSR or HYBRIDThe shell may render, but the content arrives only after mount.
Static export of a React site with all data inlinedSSRIf the data is baked into the HTML at build time, crawlers get everything.

Your options, in order of durability

01
Move to a server-rendering framework
Next.js, Remix or React Router in framework mode. Your components mostly carry over — the work is in routing and data fetching. Most durable answer, and the one to choose if organic traffic matters to the business.
02
Add prerendering in front of the app
A service or self-hosted layer serves cached rendered HTML to crawlers. Fast to deploy and requires no code changes, but adds a dependency, an ongoing cost, and a second version of your site that can silently drift out of sync.
03
Static-generate the pages that need to rank
Marketing pages, articles and documentation often don't need to be part of the SPA at all. Moving just those to static HTML fixes the SEO-relevant portion without touching the application.
04
Accept it, deliberately
For an app behind a login, rendering doesn't matter — those pages shouldn't be indexed anyway. The mistake is accepting it by default on pages that do need to rank.

The fourth option is a real one and gets dismissed too quickly. Not every React app needs to rank. The problem is arriving at that conclusion by accident rather than by decision.

React rendering FAQ

No. React on its own is a client-side library. Create React App and the default Vite React template both produce a static HTML shell that JavaScript fills in after loading. Server rendering requires a framework such as Next.js or React Router in framework mode, or a custom server using React's server rendering APIs.