A React or Vue app can look perfect in the browser and still sit invisible in Google's index for weeks — here's why, and what actually fixes it.
A client comes to us with a beautifully built React app — fast interactions, clean UI, everything a modern web product should be — and a Search Console report showing half the pages stuck on "Discovered, currently not indexed." Nothing is technically broken. The site just isn't built the way search engines need it to be built. This is the most common SEO failure mode we see in single-page applications, and it has almost nothing to do with keywords or content quality. It's architecture.
How Googlebot actually handles JavaScript
Understanding the failure starts with understanding the process, because most SPA SEO mistakes come from a mental model that's a few years out of date. Googlebot doesn't render JavaScript in the same pass it crawls a URL. It happens in two stages: first, Googlebot fetches the raw HTML and queues the URL; second, a separate rendering service (running a headless version of Chromium) executes the JavaScript and captures the resulting DOM. That second step happens on a delay — sometimes seconds, sometimes days, depending on crawl budget and site size — and it costs Google significantly more compute per page than parsing static HTML does.
For a small site this delay is often invisible. For a large SPA with thousands of routes, it means two things in practice. First, indexing is slower — new pages can sit in a rendering queue for days before their content is actually evaluated. Second, if anything in the render step fails silently (a blocked script, a timeout, a client-side redirect that never fires because an API call hangs), Google may index the page based on an empty or partial shell rather than the real content. We've seen product pages indexed with nothing but a loading spinner as the visible text, simply because the render queue and the API response never lined up in Google's simulated browser.
Bing, and most other crawlers besides Google, do a much shakier job of executing JavaScript at all. If a meaningful share of your traffic ambition includes Bing (still relevant for some B2B and enterprise buyers) or any AI-answer crawler that doesn't run a full browser, client-side-only rendering is a bigger liability than it is for Google alone.
The specific mistakes that break SPAs
Routing that never touches the server. Client-side routers using the History API can create URLs like /products/blue-sneakers that look like real pages but return a 200 status with the exact same generic HTML shell no matter what's requested — because the actual routing decision happens after JavaScript loads. If a crawler fetches that URL before rendering completes, or if rendering fails, every route on the site looks identical. The older hash-based routing pattern (/#/products/blue-sneakers) is worse: everything after the # is invisible to the server and historically was not even treated as a separate URL for indexing purposes.
Missing or duplicated per-route metadata. In a traditional multi-page site, every page ships with its own <title> and meta description in the initial HTML. In a naive SPA, the title tag is set once in index.html and then manipulated via JavaScript as the user navigates. If that manipulation depends on a component mounting correctly, and the render pass captures the DOM before that happens, every indexed page can inherit the same title and description — which reads to Google as duplicate content across the entire site.
Content behind interaction. Tabs, accordions, "load more" buttons, and infinite scroll are common UX patterns in SPAs, and they're common ways to accidentally hide content from search entirely. Googlebot does not click buttons, scroll, or fill in forms during rendering. If your product specifications are only visible after a user clicks a "Details" tab that swaps in new DOM content, that content functionally doesn't exist for SEO purposes unless it's present in the initial render or reachable through a crawlable link.
Internal linking that depends on JavaScript execution. Search engines discover most pages by following links, not by reading a sitemap alone. If your navigation, category pages, or "related items" modules render <div> elements with onClick handlers instead of real <a href> tags, there is no link for a crawler to follow independent of full JavaScript execution — which slows discovery of new and deep pages considerably.
Client-side redirects instead of server ones. Using router.push() or similar to redirect a logged-out user, an old URL, or a canonical variant means the redirect only happens after JavaScript runs. Crawlers may index the pre-redirect URL and content instead of following the intended destination, especially under render-budget pressure.
Choosing a rendering strategy
The fix isn't "add more meta tags." It's choosing an architecture where content and metadata exist in the HTML response itself, before any JavaScript runs. There are a few established approaches, and the right one depends on how often content changes.
- Server-side rendering (SSR) generates the full HTML on each request, on the server. Every visit — bot or human — gets complete content and metadata immediately. This is the safest default for content-heavy or frequently updated sites, at the cost of server compute per request.
- Static site generation (SSG) builds every page to flat HTML at build time. It's extremely fast and cheap to serve, and ideal for content that doesn't change per-visitor — marketing pages, blogs, documentation — but it means a content change requires a rebuild and redeploy.
- Incremental static regeneration / hybrid rendering blends the two: pages are built statically but can be regenerated on a schedule or on-demand, which suits large catalogs where full rebuilds are impractical but full SSR overhead isn't justified either.
- Client-side rendering (CSR) with pre-rendering for bots used to be a common workaround — serving a pre-rendered snapshot to known crawler user agents while serving the SPA bundle to real users. This is riskier today: it can drift out of sync with the live app, and if implemented sloppily it resembles cloaking, which search engines explicitly penalize. We treat it as a last resort for legacy apps that can't be re-architected, not a first choice.
For most marketing sites, content platforms, and e-commerce catalogs we build, SSR or SSG through a framework like Next.js, Nuxt, or Astro is the default starting point, precisely because it removes the rendering-timing risk instead of trying to manage it after the fact. The routing decision and the SEO decision are the same decision — it's much cheaper to make once at architecture time than to retrofit after launch.
What still needs manual attention even with SSR
Rendering strategy solves the visibility problem; it doesn't automatically solve metadata quality. Each route still needs a unique, accurate title tag and meta description generated from real page data — not a template that quietly falls back to a generic default when a field is empty. Canonical tags need to point to the correct URL per route, especially where filter and sort parameters can generate near-duplicate URLs (/shoes?color=blue&sort=price versus /shoes). Structured data (JSON-LD) for products, articles, or FAQs should be rendered server-side too, since Google's ability to read structured data injected purely client-side is less reliable than for regular content. That markup is what unlocks star ratings, FAQ dropdowns and other rich results in listings, so losing it to a rendering gap is a real cost.
A clean, server-rendered XML sitemap listing canonical URLs remains valuable even for a well-architected SPA — it gives crawlers a direct map instead of relying entirely on link discovery, which matters most for large sites or ones with thin internal linking between sections.
Core Web Vitals and JavaScript weight
Rendering strategy and performance are related but separate problems, and SPAs tend to struggle at both simultaneously because a large JavaScript bundle is often the root cause of poor Largest Contentful Paint and Interaction to Next Paint scores. A page can be perfectly server-rendered and still perform badly if the client has to download, parse, and execute several hundred kilobytes of JavaScript before the page becomes interactive — a pattern search engines increasingly weigh through Core Web Vitals as a ranking input.
The practical fixes overlap with good frontend engineering generally: code-splitting so route-level bundles only load what that route needs, deferring non-critical third-party scripts (chat widgets, analytics, marketing pixels) until after the main content is interactive, and being deliberate about client-side hydration so the browser isn't re-executing work the server already did to produce the visible page.
Hydration mismatches: a subtler failure mode
Even a properly server-rendered app can undermine itself during hydration — the step where client-side JavaScript takes over the static HTML the server sent and attaches interactivity to it. If the client-rendered output doesn't match what the server sent (a common cause is content that depends on localStorage, a user's timezone, or a viewport check that isn't available during server rendering), React and similar frameworks will discard and replace the mismatched DOM. When that happens on content that matters for SEO — a product description, a price, a heading — the version a crawler captured during the initial render can differ from what actually renders after hydration completes, and inconsistency between the two is exactly the kind of signal that erodes trust in a page's content over time even when neither version is individually wrong. The practical fix is keeping anything content-bearing deterministic between server and client: pull user-specific personalization in as a secondary client-side enhancement layered on top of stable, indexable base content, rather than branching the core content itself on client-only state.
Framework defaults worth knowing
The framework choice does most of the heavy lifting here, and it's worth knowing what each one assumes by default rather than discovering it after launch. Next.js's App Router renders server-first by default and exposes a dedicated metadata API specifically so title tags, descriptions, and Open Graph data are generated per route on the server rather than patched in client-side — which removes an entire category of the per-route metadata problem described above, provided it's actually used route by route rather than left on a single root default. Nuxt follows a similar server-first model with its own head-management composables. SvelteKit defaults to server-side rendering unless explicitly disabled per route. Angular's story has historically been weaker here — a pure client-rendered Angular SPA has the same fundamental exposure described throughout this piece, and Angular Universal (its SSR add-on) is an opt-in step, not a default, which is why a surprising number of older enterprise Angular sites still carry this exact problem years after launch. None of this means the framework alone guarantees good SEO — it means the framework determines how much deliberate work is required versus how much comes free, and that's worth weighing before the routing and rendering architecture is locked in, not after.
An audit checklist before you trust an SPA's SEO
Before assuming a JavaScript site is search-ready, we run through a short list with clients:
- Fetch the raw HTML response for key pages (before JS executes) and confirm title, meta description, and primary content are present.
- Use Search Console's URL Inspection tool to view the "rendered HTML" Google actually captured, not just what's in the browser.
- Check that primary navigation and internal links use real
<a href>elements, not click handlers on non-link elements. - Confirm there's no reliance on hash-based routing for indexable content.
- Verify canonical tags resolve correctly across filtered, sorted, and paginated URL variants.
- Run Lighthouse or PageSpeed Insights specifically on mobile, since JavaScript execution cost hits low-power devices hardest.
- Confirm structured data validates when read from the rendered DOM, not just the source template.
None of this is exotic. It's the same discipline any solid web build should have — the difference is that with an SPA, skipping it doesn't just mean a slightly worse page, it can mean the page effectively doesn't exist to search engines at all, no matter how good the content behind it is.
Frequently Asked Questions
Does Google actually render JavaScript before indexing a page?
Yes, but not in a single pass. Googlebot first fetches the raw HTML and queues the URL, then a separate rendering service running headless Chromium executes the JavaScript later and captures the resulting DOM. That second step can happen seconds or days after the initial crawl depending on crawl budget and site size, and it costs Google far more compute than parsing static HTML. If anything fails silently during that render — a blocked script, a timeout, an API call that never resolves — Google may index an empty or partial shell instead of the real content, which is why pages can sit as "Discovered, currently not indexed" for weeks.
What is the "two-wave indexing" problem with JavaScript sites?
It refers to the gap between Googlebot's initial HTML crawl and the later rendering pass that actually executes JavaScript. In the first wave, Google sees only what's in the raw server response. In the second wave, a headless browser renders the page and evaluates the real content — but this wave is delayed and not guaranteed to succeed. For small sites the delay is often invisible, but for large single-page applications with thousands of routes, it means slower indexing and a real risk that Google evaluates a loading spinner or generic shell instead of the finished page.
Should I use server-side rendering, static generation, or client-side rendering for SEO?
It depends on how often your content changes. Server-side rendering (SSR) generates full HTML per request, so every visitor — bot or human — gets complete content immediately, at the cost of server compute. Static site generation (SSG) builds flat HTML at build time, which is fast and cheap but requires a rebuild to reflect content changes, making it ideal for blogs, docs, and marketing pages. Incremental static regeneration blends both, rebuilding pages on a schedule or on-demand, which suits large catalogs. Pure client-side rendering with bot-only pre-rendering is the riskiest option and is generally treated as a last resort for legacy apps.
Does using React automatically hurt my SEO?
Not by itself. React is a rendering library, not a rendering strategy, so the SEO outcome depends on how it's deployed. A React app rendered entirely client-side, with no server-rendered HTML and no pre-rendering, does carry real SEO risk because content and metadata only exist after JavaScript executes. A React app rendered through a framework like Next.js using server-side rendering or static generation removes most of that risk because content and metadata already exist in the initial HTML response before any JavaScript runs. The framework and configuration matter more than the library choice.
Does Next.js need extra work to be SEO-friendly?
Next.js's App Router renders server-first by default and includes a dedicated metadata API for generating titles, descriptions, and Open Graph data per route on the server, which removes an entire category of duplicate-metadata problems common in naive SPAs. That said, it isn't automatic — the metadata API has to actually be used route by route rather than left pointing at a single root default. The rendering strategy solves visibility; unique, accurate per-route metadata and correct canonical tags still require deliberate implementation.
Is Angular bad for SEO?
Angular's default story here is weaker than some competitors. A pure client-rendered Angular single-page application carries the same fundamental exposure as any client-only SPA: content and metadata don't exist in the initial HTML until JavaScript executes. Angular Universal, Angular's server-side rendering add-on, is an opt-in step rather than a default, which is why a surprising number of older enterprise Angular sites still carry rendering-related SEO problems years after launch. Adding Angular Universal, or otherwise ensuring content ships in the initial HTML, addresses this directly.
Do Nuxt and SvelteKit handle SEO better out of the box than plain client-side apps?
Generally yes. Nuxt follows a server-first model similar to Next.js, with its own head-management composables for setting per-route metadata on the server. SvelteKit defaults to server-side rendering unless a route explicitly disables it. Both remove the rendering-timing risk that plagues purely client-rendered apps by default, rather than requiring it to be bolted on later. The framework determines how much deliberate SEO work is required versus how much comes free — it doesn't guarantee good SEO on its own, but it lowers the baseline risk considerably.
Why is hash-based routing (the # in URLs) bad for SEO?
Everything after a # in a URL is historically invisible to the server and was not treated as a separate URL for indexing purposes. A hash-routed URL like /#/products/blue-sneakers looks like a distinct page to a user, but the server has no way to know which "page" was requested, so it can't return route-specific content or metadata. This is a step worse than client-side History API routing, which at least produces distinct-looking URLs, even though those can still return an identical generic HTML shell if rendering fails or hasn't completed.
Why do all my SPA pages have the same title tag in Google's index?
This typically happens because the title tag is set once in index.html and then manipulated via JavaScript as the user navigates between routes. If that manipulation depends on a component mounting correctly, and Google's rendering pass captures the DOM before that happens, every indexed page can inherit the same generic title and meta description. To Google, that reads as duplicate content across the entire site. The fix is generating unique, accurate titles and descriptions server-side per route rather than patching them in client-side after the fact.
Can Google index content that's hidden behind tabs or accordions?
Only if that content is present in the initial render or otherwise reachable through a crawlable link — Googlebot does not click buttons, scroll, or fill in forms during rendering. Tabs, accordions, "load more" buttons, and infinite scroll are common UX patterns that can accidentally hide content from search entirely. If product specifications only appear after a user clicks a "Details" tab that swaps in new DOM content, that content functionally doesn't exist for SEO purposes unless it's already in the page's initial HTML.
Why does redirecting with router.push() cause SEO problems?
Client-side redirects using router.push() or similar only fire after JavaScript executes, unlike a server-side redirect which happens immediately as part of the HTTP response. This means crawlers can index the pre-redirect URL and its content instead of following through to the intended destination, especially under render-budget pressure where the JavaScript execution step gets delayed or skipped. Redirects for logged-out users, old URLs, or canonical variants should happen at the server level whenever the destination matters for SEO.
Why don't my internal links get discovered by Google in my SPA?
Search engines discover most pages by following links, not by reading a sitemap alone. If navigation, category pages, or "related items" modules render <div> elements with onClick handlers instead of real <a href> tags, there is no link for a crawler to follow independent of full JavaScript execution completing successfully. That slows discovery of new and deep pages considerably. The fix is straightforward: use real anchor tags with actual href attributes for anything that functions as internal navigation, even if a client-side router intercepts the click for users.
How do Core Web Vitals relate to JavaScript-heavy SPAs?
Rendering strategy and performance are related but separate problems, and SPAs tend to struggle at both simultaneously because a large JavaScript bundle is often the root cause of poor Largest Contentful Paint and Interaction to Next Paint scores. A page can be perfectly server-rendered and still perform badly if the client has to download, parse, and execute hundreds of kilobytes of JavaScript before becoming interactive — and Core Web Vitals is a signal search engines weigh as a ranking input. Fixing rendering strategy doesn't automatically fix bundle-size performance problems.
How do I reduce JavaScript bundle size to improve Core Web Vitals?
The practical fixes overlap with good frontend engineering generally. Code-split so route-level bundles only load what that specific route needs, rather than shipping the entire app's JavaScript on every page. Defer non-critical third-party scripts — chat widgets, analytics, marketing pixels — until after the main content is interactive. Be deliberate about client-side hydration so the browser isn't re-executing work the server already did to produce the visible page. These changes target Largest Contentful Paint and Interaction to Next Paint directly, which affect both user experience and Core Web Vitals ranking signals.
What is a hydration mismatch and why does it matter for SEO?
Hydration is the step where client-side JavaScript takes over the static HTML the server sent and attaches interactivity to it. If the client-rendered output doesn't match what the server sent — often because content depends on localStorage, a user's timezone, or a viewport check unavailable during server rendering — React and similar frameworks discard and replace the mismatched DOM. When that happens to SEO-relevant content like a product description, price, or heading, the version a crawler captured during initial render can differ from what renders after hydration, and that inconsistency is the kind of signal that erodes trust in a page's content over time.
How do I prevent hydration mismatches from affecting SEO-critical content?
Keep anything content-bearing deterministic between server and client. Pull user-specific personalization in as a secondary client-side enhancement layered on top of stable, indexable base content, rather than branching the core content itself on client-only state like localStorage, timezone, or viewport checks. If the server and client would ever compute a different value for the same content-bearing element, that element is a hydration-mismatch risk, and it's exactly the content a crawler is most likely to capture inconsistently between the initial render and the post-hydration state.
How do I check whether Google can actually see my SPA's content?
Use Search Console's URL Inspection tool to view the "rendered HTML" Google actually captured for a given page, not just what appears in your own browser. It's also worth fetching the raw HTML response directly, before any JavaScript executes, to confirm the title, meta description, and primary content are present in that initial response rather than relying entirely on client-side rendering to add them. Comparing what's in the raw response, what Google's rendered HTML shows, and what your browser displays will surface most rendering-related gaps.
Should structured data (JSON-LD) for an SPA be rendered client-side or server-side?
Structured data for products, articles, or FAQs should be rendered server-side, since Google's ability to read structured data injected purely client-side is less reliable than for regular content rendered the same way. That markup is what unlocks star ratings, FAQ dropdowns, and other rich results in search listings, so losing it to a rendering-timing gap is a real cost, not a cosmetic one. When auditing, confirm structured data validates when read from the rendered DOM in Search Console, not just from the source template.
Can I fix an existing SPA's SEO without a full rewrite?
In many cases, yes — the rendering strategy is what needs to change, not necessarily the entire application. Introducing server-side rendering or static generation through a framework capable of hydrating an existing component structure, adding server-rendered per-route metadata and canonical tags, converting click-handler navigation to real anchor tags, and moving client-side redirects to the server level can address most of the issues described here incrementally. Full client-side rendering with bot-specific pre-rendering is a legitimate stopgap for legacy apps that genuinely can't be re-architected, though it carries cloaking risk if implemented sloppily.
How long does it take to see indexing improve after fixing SPA rendering issues?
The post doesn't give a fixed timeline, and it depends on crawl budget, site size, and how Google's rendering queue is currently treating the site — the same rendering delay that causes the original problem also affects how quickly a fix gets picked up. What speeds things up is making the underlying signal easy for Google to re-evaluate: a clean, server-rendered XML sitemap listing canonical URLs, verifiable through the URL Inspection tool, and confirming via that tool that newly fixed pages now render with complete content rather than an empty shell.



