Key Takeaways
- Googlebot can render JavaScript but uses a "second wave" crawl that happens days to weeks after initial discovery — server-rendered HTML is indexed immediately
- Title, description, and canonical tags rendered only in JavaScript may not be read by social preview crawlers (LinkedIn, Slack, Twitter/X) at all
- SSR (Server-Side Rendering) and SSG (Static Site Generation) are the two reliable fixes — client-side hydration alone is not enough
- Dynamic rendering (serving a pre-rendered version to crawlers) is a deprecated workaround that Google explicitly discourages
- React Router / Vue Router SPAs can implement SSR with Next.js / Nuxt.js without rewriting the entire application
How Googlebot Handles JavaScript SPAs
Google's crawling pipeline has two waves:
First wave — Googlebot fetches the HTML. For an SPA, this is often just <div id="app"></div> and a bundle of JavaScript. No content yet.
Second wave — Googlebot queues the page for JavaScript rendering. A headless Chrome instance renders the page and extracts the content. This second wave happens days to weeks after initial discovery.
This delay means new SPA pages may take weeks to appear in search results, updated content on existing pages may take weeks to be re-indexed, and time-sensitive content (news, events, product launches) in SPAs won't be indexed in time. For comparison, a server-rendered HTML page is indexed in hours or days after first crawl.
The Metadata Problem
SPAs that use React Helmet, vue-meta, or similar client-side metadata libraries set <title>, <meta name="description">, and <link rel="canonical"> via JavaScript after the page loads. This causes three distinct problems.
Googlebot's first-wave crawl reads no metadata — the page looks like it has no title or description. Social preview crawlers (LinkedIn, Slack, WhatsApp, iMessage) do NOT execute JavaScript at all. They read only server-rendered HTML. If your OG tags are set via JavaScript, every share card shows blank or generic content. Other search engines (Bing, DuckDuckGo, Yandex) have less capable JS rendering — they may index the blank shell, not the rendered content.
Server-Side Rendering (SSR)
SSR renders each page on the server and sends complete HTML to the browser. Googlebot and all other crawlers receive the full content immediately — no JavaScript required to see the page.
Next.js (for React) — the standard SSR/SSG solution for React SPAs. Pages using export async function getServerSideProps() render on the server per request. Pages using getStaticProps() render at build time (SSG).
Nuxt.js (for Vue) — same pattern for Vue applications. asyncData() and fetch() hooks run server-side.
SvelteKit (for Svelte) — built-in SSR via load() functions.
Migration effort for existing SPAs is significant but typically done page-by-page rather than all at once. Start with your highest-priority landing pages.
Static Site Generation (SSG)
For content that doesn't change per user (blog posts, marketing pages, documentation), SSG generates HTML at build time. The result is static HTML files served from a CDN — instant indexing, maximum performance.
Best for: marketing sites, blogs, documentation, product pages with infrequent updates. Not suitable for: user dashboards, authenticated content, real-time data.
Next.js SSG: getStaticProps() + getStaticPaths() for dynamic routes. Gatsby: React-based SSG with GraphQL data layer. Astro: framework-agnostic SSG with partial hydration ("islands architecture").
The Metadata Solution for SPAs
Regardless of SSR vs SSG, every route in your SPA needs server-rendered metadata. The canonical, title, description, and Open Graph tags must be in the HTML sent by the server — not injected by JavaScript after the page loads.
With Next.js App Router, use the metadata export:
export const metadata = {
title: 'Product Name — Your Store',
description: '...',
openGraph: { title: '...', images: ['...'] }
}
With Next.js Pages Router, use <Head> from next/head inside getServerSideProps or getStaticProps — these run server-side and produce server-rendered metadata.
Canonical Tags in SPAs
Without explicit server-rendered canonicals, Google may canonicalise your SPA routes incorrectly — especially if the same content is accessible with and without trailing slashes, query parameters create duplicate URLs, or JavaScript navigation creates multiple URL states for the same content.
Every route must have a server-rendered <link rel="canonical"> with the absolute URL. Client-side canonical updates via React Helmet are a fallback at best — Google's second-wave crawler reads them, but the first-wave crawl and all other crawlers don't.
Testing SPA SEO
Three ways to test:
1. GSC URL Inspection → Test Live URL — shows Googlebot's rendered view and any metadata errors.
2. curl your SPA routes — curl https://your-app.com/products/123 — if the response is just <div id="app">, all non-JS crawlers see nothing.
3. ByteWaveNetwork SEO Analyzer — crawls your site using a real HTTP request (no JavaScript execution). If your SPA is not SSR/SSG, the analyzer will show TITLE_MISSING, DESC_MISSING, CANONICAL_MISSING, and similar errors on every route — because those are exactly what non-JS crawlers see. This makes it useful for auditing: running the SEO Analyzer against an SPA tells you exactly which routes have server-rendered metadata and which don't.
The Deprecated Alternative — Dynamic Rendering
Dynamic rendering (serving pre-rendered HTML to crawlers while serving the SPA to users, based on User-Agent) was a documented Google workaround circa 2018–2020. Tools like Rendertron and Prerender.io implement it.
Google has since explicitly discouraged this approach and removed it from their developer documentation. It creates maintenance overhead, a content gap between what crawlers and users see (which Google flags as a cloaking risk), and doesn't solve the social preview problem. Don't implement dynamic rendering for new projects — use SSR or SSG.
| Approach | Googlebot first-wave | Social crawlers | Recommended |
|---|---|---|---|
| Client-side SPA only | Empty shell | Empty shell | No |
| React Helmet / vue-meta | Empty shell (first wave) | Empty shell | No (for SEO) |
| Dynamic rendering | Pre-rendered HTML | Depends on UA detection | No (deprecated) |
| SSR (Next.js, Nuxt) | Full HTML | Full HTML | Yes |
| SSG (Next.js, Gatsby, Astro) | Full HTML | Full HTML | Yes (for static content) |
Migration Path for Existing SPAs
Rewriting an entire SPA to use SSR/SSG is daunting — but you don't have to do it all at once. Prioritise by page type:
1. Homepage and top landing pages first — highest traffic, most link equity, most important to index correctly.
2. Key product/service pages — pages that drive conversions should be server-rendered.
3. Blog and content pages — SSG is ideal here; content doesn't change per user and benefits maximally from immediate indexing.
4. User-specific pages last (or never) — dashboards and authenticated content don't need to rank, so SSR here adds cost with no SEO benefit.
Audit Your SPA's SEO
Free, no signup. The SEO Analyzer crawls your site without executing JavaScript — showing exactly what Googlebot sees in its first-wave crawl: missing titles, missing canonicals, and missing meta descriptions on every route that isn't server-rendered.
Try the SEO Analyzer Free →Affiliate disclosure: Some links on this page may be affiliate links. We only mention tools we've personally used and have an honest opinion about. Affiliate revenue helps keep ByteWaveNetwork's tools free and maintained. We are not paid by any of the tools compared in this article for favorable coverage.