SEO for SPAs starts with an uncomfortable fact: a single-page application ships an almost empty document. The initial response is a div and a bundle; everything a user sees exists only after the browser executes JavaScript. Users don't notice. Crawlers do. Search engine optimization was built for a web of plain HTML pages, and an SPA asks the search engine to do extra work — fetch, render, then read — before it can see a single word.
Google can do that work. It renders JavaScript at scale, and a well-built SPA can rank exactly as well as a static site. But "can" is doing heavy lifting in that sentence. Rendering is deferred, budgets are finite, mistakes are silent, and other crawlers — some AI assistants, social preview bots, smaller search engines — read far less JavaScript than Google does. This guide is the whole single page application SEO playbook: how crawling actually works for JS apps, the routing rules, the rendering choices, and the tests that tell you the truth. If you want a quick outside read on any site first, the free SEO audit takes a minute.
How Googlebot Handles JavaScript
Google processes a JavaScript app in two waves. First it fetches your page and reads whatever rendered HTML comes back — for a pure client-side app, that's nearly nothing. Then, later, the page enters a render queue where a headless Chromium executes JavaScript, builds the DOM, and hands the result back to indexing. Only after that second wave is your content truly crawled and indexed.
Three consequences follow. Delay: the render queue can add hours or days, which matters for fresh content. Fragility: if your javascript code errors under the crawler, times out, or depends on a blocked request, Google indexes the empty shell without telling you. And coverage: links that only exist after render get discovered late, so a purely client-side app crawls slowly from the inside out. None of this is fatal — all of it is why the rest of this page exists.
Routing: URLs Are the Contract
An SPA can swap views without changing anything a crawler understands, and that's the first thing to fix. Every indexable view needs a real, unique URL, powered by the History API — clean paths like /pricing and /guides/onboarding, never hash fragments. Hash-based routes (#/pricing) are invisible to indexing; the History API gives you crawlable paths with no page reload.
Then make each route behave like a page. Unique title and meta description set on navigation, a canonical tag, and correct status codes: a route that doesn't exist must return a real 404, not a friendly component on a 200. Soft-404s are the classic SPA leak — thousands of junk URLs indexed because the app smiles at every path. And every internal link should be a genuine anchor tag with an href, not a click handler on a div. Crawlers follow links; they don't click buttons.
Rendering Strategies: Pick Where the HTML Comes From
Every SPA answers one question, knowingly or not: when a crawler asks for a URL, who builds the HTML? There are four honest answers.
Client-side rendering (CSR). The browser builds everything. Simplest to ship, weakest for search: empty initial HTML, render-queue delays, and total dependence on Google's JS execution. Acceptable for logged-in app screens; risky for anything you want ranked.
Server-side rendering (SSR). The server sends fully rendered HTML for every request, and the app hydrates on top. Crawlers get real content instantly; users get faster first paint. Frameworks like Next and Nuxt made this the default answer for content that matters. The cost is server complexity and cache discipline.
Static generation. Pages are pre-built at deploy time — the most SEO friendly option there is, because every crawler on earth reads plain HTML. Perfect for marketing pages, docs, and blog posts; wrong for content that changes per request.
Prerendering. A service renders snapshots of your pages and serves them to crawlers while users get the normal app. It's a respectable retrofit for an existing SPA you can't rebuild — with the ongoing chore of keeping snapshots fresh and consistent with what users see.
The pragmatic pattern for most teams: static or SSR for every public, indexable route; CSR for the application behind login. Mixed rendering isn't a compromise — it's the architecture matching the job.
The On-Page Layer Still Applies
Once crawlers can see your pages, the normal rules return. Structured data in JSON-LD on every key template — product, article, FAQ, organization — so machines read meaning, not just markup. One H1 per view, headings in order, images with alt text. Load time discipline: split the bundle, lazy-load below the fold, and watch hydration cost, because a fast-to-paint app that's slow to respond still fails Core Web Vitals. User experience metrics are ranking inputs now, and SPAs live or die on them.
And keep an eye on the crawlers that aren't Google. Social preview bots, many AI assistants, and smaller engines read the initial HTML only. If your open-graph tags, titles, and core content exist solely after hydration, you're invisible in exactly the places links get shared. Server-rendered meta is cheap insurance.
Testing: Trust the Rendered HTML, Not the Demo
SPA SEO fails silently, so verification is the discipline. Google Search Console's URL Inspection tool is the ground truth: it shows the rendered HTML Google actually got, the resources it couldn't load, and whether the page is indexed. Run it on every template, not just the homepage. Pair it with a crawl of your site in JS-rendering mode and again with JavaScript off — the difference between those two crawls is your risk surface.
Then wire measurement to reality. A stock Google Analytics install counts one pageview per session on an SPA, because nothing reloads; configure route-change tracking so every view fires and your data means something. Watch Search Console's coverage report monthly for soft-404 spikes and rendering errors — they're the smoke alarms of a JS site.
Framework Notes: Where Your Stack Lands
Google is good at rendering JavaScript, but your framework decides how much rendering JavaScript demands — and how easily you can hand crawlers finished HTML instead.
React. Plain client-rendered React is the classic risk case. Next.js exists largely to fix that: per-route SSR, static generation, and sane defaults for titles and meta. If a React app needs rankings, it usually needs Next or an equivalent.
Vue. Same story, different names — Nuxt gives Vue apps server rendering and static builds with little ceremony. The Vue router uses History API paths by default, which removes one classic mistake for free.
Angular. Angular Universal handles the server side; the discipline is keeping transfer state clean so hydration doesn't double-fetch and drag load time.
Svelte and friends. SvelteKit, Astro, and the newer meta-frameworks were born HTML-first. Astro in particular ships plain pages by default and hydrates islands only where needed — the architecture this whole guide keeps recommending, productized.
The pattern across all of them: the framework is rarely the problem. Shipping the framework's client-only mode for public pages is.
Retrofitting an Existing SPA: The Triage Order
If the app is already live and the traffic never came, fix things in this order. First, routing hygiene: History API paths everywhere, real 404s, canonical tags, anchor-tag links. It's the cheapest work with the widest blast radius. Second, per-route titles and meta — an afternoon of work that fixes how every page looks in results and previews.
Third, get real HTML into the first response for the pages that earn money: prerender them if you can't rebuild, SSR them if you can, or carve the marketing pages out to static hosting entirely. Fourth, verify: URL Inspection on every template, a JS-off crawl, analytics wired to route changes. Then give the index a few weeks and read the coverage report before touching anything else.
Teams that run this sequence usually find most of their loss sat in steps one and three — and that the "SEO problem" was an engineering backlog wearing a marketing costume.
The Strategic Question Nobody Asks Early Enough
The best seo strategy for many SPAs is scope reduction: does the marketing site need to be inside the app at all? Splitting the public site — home, pricing, blog posts, docs — into static pages, and keeping the SPA for the product itself, removes the whole problem class for the pages that earn traffic. Teams that decide this at architecture time spend nothing on it; teams that decide it after launch call it a migration. If you're a designer or an agency making that call for clients, the broader build checklist lives in SEO for web designers — and it pairs with everything above.
