DEV Community

Cover image for Why Google Can't See Your React App
Mitu Das
Mitu Das

Posted on • Originally published at ccbd.dev

Why Google Can't See Your React App

I spent way too long debugging why Google couldn't see my React app. It ranked for nothing, not even my own brand name. The fix, once I found it, was 4 lines of code.

If you've shipped a React app and then watched site:yourdomain.com return zero results in Google, this one's for you. Here's what was actually happening under the hood, and three ways to fix it, including the one I ended up shipping.

Why Google "Can't See" Your React App in the First Place

Here's the counterintuitive part: Googlebot does execute JavaScript. It has for years. So why do so many React apps still get skipped?

The problem isn't that Google can't run JS, it's that Googlebot renders your page in two waves. Wave one grabs the raw HTML. Wave two, which can happen hours or days later, actually executes your JS bundle and renders the final DOM. If your app is slow to hydrate, throws a console error, or depends on client-side data fetching that never resolves in the crawl budget given, wave two either times out or never happens.

Check what Google actually sees with this command in your terminal:

curl -s https://yourapp.com | grep -A 5 "<title>"
Enter fullscreen mode Exit fullscreen mode

If that returns something like:

<title>React App</title>
<div id="root"></div>
Enter fullscreen mode Exit fullscreen mode

...that's your problem, right there. That's the entire page content as far as the first crawl wave is concerned. No title, no description, no content, just an empty div waiting for JavaScript.

Fix #1: Server-Side Rendering (The "Correct" Fix)

The most robust fix is rendering your HTML on the server before it ever reaches the browser. If you're using Next.js, you get this for free with getServerSideProps or React Server Components. But if you're on plain Create React App or Vite, you're not getting SSR without real architectural changes.

A lighter-weight option is prerendering with react-snap, which crawls your built app with a headless browser and bakes the resulting HTML into static files:

npm install --save-dev react-snap
Enter fullscreen mode Exit fullscreen mode
// package.json
{
  "scripts": {
    "postbuild": "react-snap"
  },
  "reactSnap": {
    "source": "build"
  }
}
Enter fullscreen mode Exit fullscreen mode
// src/index.js
import { hydrateRoot, createRoot } from "react-dom/client";
import App from "./App";

const rootElement = document.getElementById("root");
if (rootElement.hasChildNodes()) {
  hydrateRoot(rootElement, <App />);
} else {
  createRoot(rootElement).render(<App />);
}
Enter fullscreen mode Exit fullscreen mode

Result: run npm run build, and your build/index.html now contains fully rendered markup instead of an empty div. curl your local build folder and you'll see real content this time.

Fix #2: Meta Tags That Actually Update Per Route

Even with SSR sorted, a lot of React SEO problems come from every route sharing the same <title> and <meta description> because they're hardcoded in public/index.html. Google (and every social preview card) needs unique tags per page.

react-helmet-async handles this cleanly:

npm install react-helmet-async
Enter fullscreen mode Exit fullscreen mode
// App.js
import { HelmetProvider, Helmet } from "react-helmet-async";

function ProductPage({ product }) {
  return (
    <HelmetProvider>
      <Helmet>
        <title>{product.name} | YourStore</title>
        <meta name="description" content={product.shortDescription} />
        <link rel="canonical" href={`https://yourstore.com/products/${product.slug}`} />
      </Helmet>
      <h1>{product.name}</h1>
    </HelmetProvider>
  );
}
Enter fullscreen mode Exit fullscreen mode

Result: view-source on each route now shows a distinct title and description instead of one generic tag copy-pasted across your whole site.

Fix #3: Automating Meta Injection at Build Time

Manually wiring Helmet into every component works, but it doesn't scale past a few dozen pages, and it's easy to forget on a new route. This is the part of my stack where I stopped hand-rolling things and reached for a small npm package called @power-seo, which reads a simple route-to-metadata map and injects the right tags into each prerendered page during the build step:

npm install @power-seo
Enter fullscreen mode Exit fullscreen mode
// seo.config.js
module.exports = {
  routes: {
    "/": { title: "YourApp, Home", description: "Build faster with YourApp." },
    "/pricing": { title: "Pricing | YourApp", description: "Simple, transparent pricing." },
  },
};
Enter fullscreen mode Exit fullscreen mode
// package.json
{
  "scripts": {
    "postbuild": "power-seo inject --config seo.config.js --dir build"
  }
}
Enter fullscreen mode Exit fullscreen mode

Result: one config file instead of scattered Helmet calls, and every static page in build/ comes out with correct, unique tags, verifiable with the same curl command from earlier.

What I Learned

  • "Google runs JS" is technically true and practically misleading. The delay between crawl waves is where most indexing problems hide.
  • curl your own site before you touch any SEO tool. It tells you exactly what a crawler sees, no guessing required.
  • Prerendering and SSR aren't optional for content-heavy React apps. If you want organic traffic, someone (a server, a build step, or a service) needs to render real HTML before JS runs.
  • Automate meta tags early. Manual Helmet calls work for 5 pages; they silently fail for 50.

If you want to try this approach, here's the repo: https://ccbd.dev/blog/react-app-not-indexing-google-the-real-reason-and-how-i-fixed-it

Let's Talk

Have you hit this "Google can't see my React app" wall? What ended up fixing it for you, SSR, prerendering, something else entirely? Drop your setup in the comments, I'm curious how many different ways people have solved this.

Top comments (0)