SEO for React and Next.js sites: what really matters
"React is bad for SEO" is one of the most persistent myths in web development. There is some truth in it, but it applies to one particular way of using React — not to React itself.
If you have a React or Next.js site, or are considering one, there are really two questions. Can a search engine see the page content without running JavaScript? And is the site technically sound, like any other well-built website? The first is a question of architecture, the second a question of a checklist.
Where the myth comes from
A classic React app (a SPA, single-page application) sends almost empty HTML from the server: a single <div> and a reference to a script file. All the content — headings, text, links — only appears once the browser has downloaded and run the JavaScript.
To a person it makes no difference; they see a finished page. To a crawler it does.
Google does render JavaScript, but in a separate step. It reads the HTML first and renders the page later, when resources allow. Usually that works. But if a script throws an error, a data request times out or a file is blocked in robots.txt, Google indexes whatever was in the HTML — which is nothing.
Many others don't render at all. Most other crawlers, the bots of AI services and link previews (Facebook, LinkedIn, Slack, WhatsApp) read only the raw HTML. To them a client-side rendered page is empty: a shared link has no title or image, and an AI search has nothing to quote.
So the problem isn't React. It's where the HTML gets built.
The fix: build the HTML on the server or at build time
Two approaches, both of which hand the crawler finished content straight away:
- Server-side rendering (SSR) — the server assembles the HTML for each request.
- Static generation (SSG) — the HTML is assembled in advance, at build time, and the server simply sends a ready-made file.
Next.js does this by default. In the App Router, components are server components, and a page is generated statically unless it needs request-specific data. Every URL returns real HTML with the content in it; JavaScript only adds interactivity on top (this is called hydration).
Quick test: is the text in the HTML?
Open the page, press Ctrl+U (Cmd+Option+U on a Mac) and search for a sentence from the page. Not through the developer tools — they show the page after JavaScript has already run. The same from the command line:
curl -s https://your-site.com/services/ | grep -c "a sentence visible on the page"
If the result is 0, the content is only created in the browser. The URL Inspection tool in Search Console also shows which HTML Google ended up with after rendering.
Checklist for a React or Next.js site
Server-rendered HTML is the prerequisite, not the whole job. Here is what needs to be in place on every page.
A unique title and meta description per page. The most common SPA mistake: the same title on every URL. In Next.js it is set by the metadata export, or by generateMetadata on dynamic pages, and it reaches the HTML <head> on the server, not later.
Canonical. Every page states which URL is its official address — otherwise variants with query parameters or a trailing slash can compete as duplicates of the same page.
hreflang on a multilingual site. Each language version points to all the others and to x-default, and the references have to be reciprocal. Our own site has five languages with translated URLs, and every page gets its canonical and hreflang tags from one place — maintained by hand, it would not stay correct across five languages.
A generated sitemap.xml and robots.txt. In Next.js these are the files app/sitemap.ts and app/robots.ts. The sitemap is built from the same data as the pages, so a new page cannot be left out. Our site generates its sitemap at build time.
Structured data (JSON-LD). Company, services, articles, FAQ and breadcrumbs in a <script type="application/ld+json"> block. It has to be in the initial HTML, not added later by the client.
Real links. Google follows <a href> links; it doesn't click buttons. The Next.js Link component renders a proper <a> element. Navigation that happens only through onClick and router.push() is invisible to a crawler.
A real 404. A missing page has to return HTTP status 404, not 200 with the text "page not found" — Google calls the latter a soft 404. In Next.js, notFound() takes care of this. With a static export, the host has to serve the 404 page with the correct status.
One trailing-slash rule. /services and /services/ are two different URLs. Pick one (trailingSlash in Next.js), 301-redirect the other and make sure internal links use the same form. If a new site is replacing an old one, see also how to redesign a website without losing SEO.
Images with dimensions and alt text. width and height reserve the space in advance so content doesn't jump around (CLS). next/image requires them anyway. In a static export, Next.js's automatic image optimisation is switched off — images need to be compressed to a sensible size beforehand.
Client components only where there is interaction. 'use client' sends a component to the browser along with everything it imports. A menu, a form and a filter need it; a block of text and the footer don't.
Speed: pitfalls specific to React
The general picture of Core Web Vitals is covered in website speed: how to measure it and improve it. On top of that, a React site has three pitfalls of its own.
JavaScript size and hydration
Even a server-rendered page downloads JavaScript and hydrates itself. Until that is done, buttons don't respond — which hurts INP, especially on cheaper phones. Every client component and every library (carousel, animation library, date library) adds to the bundle. A heavy widget near the bottom of the page is worth loading only when it's needed (next/dynamic).
Don't hide the heading behind JavaScript
A common pattern: content "fades in", so it starts at opacity: 0 until a script gives it permission to appear. Do that to the main heading and LCP — Google's main loading-speed metric — waits for the JavaScript to download and hydrate. If the script doesn't load, the heading stays invisible.
We learned this on our own site. The home page heading is the LCP element, and it now uses a pure CSS animation that starts from the first paint rather than a JavaScript-driven reveal. Scroll effects further down the page are a separate matter — the heading doesn't depend on them.
Don't send every language to the browser
On a multilingual site it's easy to import the entire translation dictionary into a client component by accident. Then every visitor downloads the text of every language. The better pattern: a server component reads the text and passes the client component only the strings it needs, as props.
Static export: every page is a ready-made file
Next.js can export an entire site as plain HTML (output: 'export'). The result is a folder of files that any web server or CDN can serve.
- Fast — the server computes nothing at the moment of the visit.
- Secure — there is no database, admin panel or server code to attack.
- Cheap to host — ordinary static hosting is enough.
Our own site is a fully static export: Next.js (App Router), React and TypeScript, five languages.
The price for this: every content change means a new build and upload. For a company website and blog that change once a week, that works well. If content changes every minute, users have accounts or you need search against a database, you need server-side rendering or a separate service. A contact form also needs a small server-side endpoint to receive it.
If you are on WordPress now and considering a move, read migrating from WordPress without losing your SEO and the broader comparison WordPress vs a code-based website.
When to get help
If you already have a React site, do three checks: look at the source of three pages, compare the number of indexed pages in Search Console with the actual number of pages, and see whether the titles differ from page to page. If the text is missing from the HTML, tweaking meta tags won't help — the rendering has to be reorganised, often by moving from a SPA to Next.js.
We build websites with Next.js so that everything above is in place from the start. If the site already exists but isn't moving in Google, start with SEO services.
