Most advice on spa seo starts and ends with “Google can render JavaScript now, so you’re fine.” That sentence is technically true and practically misleading. Google can render your single-page app — but rendering happens on a queue, costs the crawler real compute, and is skipped entirely by half the bots that matter. The result is a familiar failure mode: your app works perfectly in a browser, ranks for nothing, and shows up in Search Console as “crawled — currently not indexed.” The problem is almost never that Google can’t read JavaScript. It’s that your architecture asks Google to do expensive work before it will show a single word of your content, and Google frequently declines.
Why a Single-Page App Is Hard for Search Engines
A traditional website ships finished HTML — headings, body copy, links, and meta tags already in the markup. A search engine reads it in one pass and it’s done. A single page app inverts this. The server returns a near-empty shell: a root div, a couple of script tags, and almost nothing else. The actual content is assembled in the user’s browser after a JavaScript bundle downloads, parses, and executes. This is fantastic for app-like interactivity and terrible for anything that reads the raw response — which is exactly what the first pass of every crawler does.
That gap between “what the server sends” and “what a human eventually sees” is the entire discipline of spa seo. Close it and your app ranks like any other site; leave it open and you’re betting your organic traffic on a rendering step that is optional from the crawler’s point of view.
Client-Side Rendering Is the Root Cause
The default mode for React, Vue, Angular, and Svelte apps is client-side rendering (CSR): the browser does all the work. View the page source of a CSR app — not the DevTools Elements panel, which shows the rendered DOM, but the actual “View Source” HTML — and you’ll typically see no headings, no paragraphs, no article text. That is exactly what a crawler sees on its first fetch: if your content only exists after hydration, your page is blank in the crawler’s initial snapshot.
This is the most important thing to internalize about javascript app seo: the Elements inspector lies to you — it shows the DOM after JavaScript has run. The right diagnostic is the raw HTML response and Google’s rendered-HTML view in the URL Inspection tool. If your key content and links aren’t in that response, you’re relying entirely on the render pass to save you.
The Two-Wave Indexing Model
Google processes JavaScript sites in two waves, and the split explains most SPA indexing pain. In the first wave, Googlebot fetches the raw HTML, extracts whatever links and content are present, and indexes that immediately. The page then enters a render queue. In the second wave — which can follow seconds later or, historically, days later — a headless, evergreen Chromium instance executes the JavaScript, produces the full DOM, and sends that back for indexing.
Two consequences follow. First, any content or link that only appears after JavaScript runs is invisible during the entire first wave, so it’s discovered late at best. Second, the render queue is a shared, budget-constrained resource; Google renders what it deems worth the compute, and for a low-authority new domain that budget is thin. “Google can render JavaScript” is true. “Google will promptly render your JavaScript, every route, every time” is not a promise you should build a business on.
Not Every Crawler Runs JavaScript at All
Even if you accept Google’s render queue, Google is not the only bot that matters. Bing renders JavaScript far less reliably, and many regional search engines don’t execute it at all. Critically, the crawlers behind link previews and AI answers are mostly non-rendering: when someone shares your URL on social or messaging apps, the unfurling bot reads your raw HTML Open Graph tags and nothing more. The crawlers feeding large language models and AI Overviews are similarly conservative about executing scripts.
So a pure client-side app can be simultaneously “indexed by Google” and invisible to Bing, to social previews, and to AI citation surfaces — and in 2026 that last category is not a rounding error. If your single page app seo strategy is “Googlebot renders it,” you’ve optimized for one crawler and abandoned the rest. The fix — putting real content in the initial HTML — serves all of them at once.
The Rendering Strategies, Ranked by Reliability
There are four practical ways to close the gap between the shell and the content, and they are not equal.
- Server-side rendering (SSR): the server returns fully-formed HTML for each request, then the client hydrates it into an interactive app. Every crawler gets complete content on the first fetch. Frameworks like Next.js, Nuxt, Angular Universal, and SvelteKit exist largely to make this practical. It’s the gold standard for content that must rank.
- Static site generation (SSG): pages are pre-rendered to HTML at build time and served from a CDN. Fastest possible delivery and perfectly crawlable — ideal for content that doesn’t change per request, like marketing pages, docs, and blog posts. Add incremental regeneration (ISR) to refresh static pages on a schedule without a full rebuild.
- Prerendering for bots: a service detects crawler user-agents and serves them a cached HTML snapshot while humans get the CSR app. It works, but it’s a bolt-on — snapshots go stale, and Google explicitly frames this “dynamic rendering” as a workaround, not a durable solution.
- Pure client-side rendering: acceptable only for content that genuinely doesn’t need to rank — a logged-in dashboard or an internal tool behind auth. For anything you want in search results, it’s the weakest option.
The decision rule is simple: if a route needs to rank, its real content must be in the server response — SSR or SSG for public pages, CSR only behind a login.
Routing: Make Every View a Real URL
SPAs navigate in the browser without full page reloads, and there are two ways to do it. The wrong way for SEO is hash-based routing, where routes look like example.com/#/products. Search engines treat everything after the # as a fragment of a single URL, so all your “pages” collapse into one indexable address. The right way is the History API (pushState), which produces clean, distinct paths like example.com/products that crawlers treat as separate, indexable URLs.
The second routing rule matters just as much: navigation must use real anchor tags. A crawler discovers pages by finding <a href="/products"> in the HTML. If your “links” are <div onClick={navigate}> handlers with no href, the crawler sees no link and never queues the page. This is one of the most common and most invisible causes of missing spa indexing — the site navigates beautifully for humans and is a dead end for bots.
Per-Route Metadata, Canonicals, and Status Codes
Because a SPA loads one shell, it’s easy to end up with one title tag and one meta description for the entire application. Every route needs its own. In Next.js this is the Metadata API; in other stacks it’s a head-management library (React Helmet, Vue Meta, and equivalents) that updates the document head per route. But with client-side rendering, that head update also happens after JavaScript runs — so non-rendering crawlers still see the default. This is another reason server-side rendering wins: it puts the correct title, description, canonical, and Open Graph tags into the initial HTML for every URL.
Two more things break quietly. Set a self-referencing canonical per route so parameter and tracking variants consolidate correctly. And watch status codes: a SPA that renders a “not found” component while the server still returns 200 OK creates a soft 404 — missing to users, but reporting success to Google, which wastes crawl budget and pollutes your index. A missing route must return a real 404, which generally requires server-side handling rather than a client-rendered error screen.
Core Web Vitals and the JavaScript Bundle Tax
SPAs are heavy. A large JavaScript bundle must download, parse, and execute before anything is interactive, which directly hurts Largest Contentful Paint and Interaction to Next Paint — two of the Core Web Vitals Google uses as ranking signals. Client-side rendering makes it worse because the content itself waits on that bundle. Server-side rendering and static generation help twice over: they paint real content immediately (better LCP) and let you defer non-critical JavaScript.
The practical levers are code-splitting so each route ships only the JavaScript it needs, lazy-loading below-the-fold components, trimming third-party scripts, and streaming server-rendered HTML. The modern frameworks support all of it, but it means treating performance as an architecture decision, not a post-launch cleanup.
How to Diagnose SPA Indexing Problems
When a single-page app isn’t ranking, work the stack in order:
- View Source, not Inspect. If your headings, body copy, and internal links aren’t in the raw HTML, that’s your problem — everything downstream is a symptom.
- Use Google’s URL Inspection tool. It shows crawled and rendered HTML separately; content that appears only in the rendered version depends on the render queue.
- Check “crawled — currently not indexed” in the Page Indexing report. A cluster on JS-dependent routes is the signature of a rendering or link-discovery gap.
- Confirm links are real anchors and routes return correct status codes, including genuine 404s for missing pages.
- Run a crawler that respects JavaScript — the gap between its raw and rendered crawl shows how much you’re leaning on rendering.
This layer is easy to skip because the app “looks fine.” SEO Rocket’s site audit runs a real crawler against your live pages and surfaces exactly these issues — thin rendered HTML, broken canonicals, soft 404s, missing metadata — so you catch the rendering gap before it costs you a quarter of indexation. It’s a platform-agnostic SEO layer, not a page builder: it won’t rewrite your React app, but it will tell you which routes Google is failing to see.
A Practical SPA SEO Checklist
- Render public, rankable routes on the server (SSR) or at build time (SSG); reserve pure CSR for content behind authentication.
- Use History-API routing with clean paths; never hash routes for indexable pages.
- Make every internal navigation a real
<a href>so crawlers can discover it. - Set a unique title, meta description, and canonical per route — in the server-rendered HTML, not only after hydration.
- Return correct HTTP status codes; a missing page is a real 404, not a 200 shell.
- Maintain an XML sitemap listing every canonical URL and submit it in Search Console.
- Budget the JavaScript: code-split, lazy-load, protect your Core Web Vitals.
- Verify with View Source and URL Inspection, then monitor which routes get indexed and where they rank.
The content side matters as much as the plumbing: once your app is crawlable, you still have to publish pages that beat the current results across dozens of routes. SEO Rocket runs keyword research on real Ahrefs data, competitor gap analysis, a validation-gated AI writer that enforces length and metadata rules before a draft ships, plus rank and AI-visibility tracking on a client dashboard, for roughly $50/month with a free tier — the same playbook proven across 1,000,000+ ranking pages, applied to the routes your SPA now lets Google see.
Frequently Asked Questions
Can Google index a client-side rendered single-page app?
Yes, but conditionally. Googlebot renders JavaScript via an evergreen headless Chromium, so a CSR app can be indexed. The catch is the render queue: rendering is deferred and compute-budgeted, so low-authority sites and deeply nested routes are often rendered late or not at all. Server-side rendering removes the dependency and gets every route indexed reliably.
Is server-side rendering always necessary for SPA SEO?
Not for everything. Content behind a login — dashboards, internal tools — never needs to rank, so CSR is fine there. But any public route you want in search results should have its real content in the initial HTML, which means SSR or static generation. The rule is per-route: rankable pages get server-rendered content.
Why does my SPA rank in Google but disappear in link previews and AI answers?
Because most non-Google crawlers don’t execute JavaScript. Social unfurlers, messaging previews, and many AI citation crawlers read only your raw HTML. If your content and Open Graph tags exist only after hydration, those bots see the empty shell. Putting content in the server response fixes Google’s render-queue risk and these blind spots at the same time.