Single page application SEO: why SPAs fail to rank, how SSR, static generation and prerendering fix it, and how to make your app readable by AI crawlers.

In short
a single page application (SPA) loads one HTML shell and builds every view with JavaScript in the browser. Google can render JavaScript, but rendering is delayed and fragile, and the crawlers behind ChatGPT, Claude and Perplexity do not execute it at all. To rank and get cited, each view of a SPA needs its own URL, a real link with an href, a unique title and description, a correct status code and, above all, its main content present in the HTML the server sends, through server-side rendering, static generation or prerendering.
Want to go further? Ask
Single page application SEO is the set of techniques that make a JavaScript app built with React, Vue, Angular or Svelte as easy to crawl as a classic website. The problem is well documented: Google explains in its JavaScript SEO basics that it processes JavaScript pages in three phases (crawling, rendering, indexing), and every SPA weakness sits somewhere in that pipeline. This guide focuses on SPAs; for JavaScript sites in general, read our guide to JavaScript SEO.
A traditional website returns a complete HTML document for every URL. A SPA returns almost the same document for every URL: an empty <div id="root">, a few meta tags and a large JavaScript bundle. The browser downloads the bundle, calls an API, then draws the page. When the visitor clicks a link, JavaScript swaps the content without reloading.
For a human with a fast phone, the result feels instant. For a crawler, the first response is an empty shell: no heading, no text, no links. Everything depends on whether the bot executes the JavaScript, and when. That single fact explains most SPA SEO problems:
/#/pricing), and the part after the # is never sent to the server, so all views collapse into one URL.<title>, and if the router does not update it, every page shares it.href: buttons and onClick handlers navigate for users but give crawlers nothing to follow.Googlebot first fetches the raw HTML and reads the links and directives it finds there. Pages that return a 200 status are then put in a queue for the Web Rendering Service, a headless Chromium. Google states that a page "may stay on this queue for a few seconds, but it can take longer than that". Once rendered, the resulting HTML is parsed again for content and links, then indexed.
Four details from Google's documentation matter for SPAs:
<a> elements with an href are reliable links. Links injected by JavaScript are fine as long as they follow that format.pushState) instead of # routes. It deprecated its old AJAX crawling scheme (the #! URLs) in October 2015.noindex in the original HTML wins. Google may skip rendering when it sees noindex, so removing it with JavaScript later does not work.The practical consequence: Google can index a client-rendered SPA, but you add a second step, a delay and a failure point to every page. A timeout, a blocked API call or a JavaScript error during rendering leaves the indexed version empty. Check what Google sees with the URL Inspection tool in Search Console, and read our guide on how Google indexing works if pages stay out of the index.
Mostly not. In a study published in December 2024 on the traffic of its network, Vercel and MERJ found that "none of the major AI crawlers currently render JavaScript": GPTBot, OAI-SearchBot and ChatGPT-User (OpenAI), ClaudeBot (Anthropic), PerplexityBot, Meta-ExternalAgent and Bytespider all read the raw HTML only. They do download JavaScript files (about 11.5% of ChatGPT's requests and 23.8% of Claude's) but do not run them. The exceptions are Gemini, which relies on Google's rendering infrastructure, and AppleBot, which uses a browser-based crawler.
For a client-rendered SPA, this means the AI crawlers that feed ChatGPT, Claude and Perplexity see an empty page. Your product can rank on Google and still be invisible in AI answers, because the assistant never read a word of it. The same applies to most link preview bots on social networks and messaging apps, which read the Open Graph tags in the raw HTML. Once the content is in the HTML, also check that your robots.txt allows the AI crawlers you want.
The fix is to send real HTML on the first response. Google calls dynamic rendering (serving a prerendered version to bots only) "a workaround and not a recommended solution" in its dynamic rendering documentation, and points to server-side rendering, static rendering or hydration instead. The definitions below follow the Rendering on the Web reference on web.dev.
| Strategy | How it works | Content in the first HTML | Best for | Limits |
|---|---|---|---|---|
| Client-side rendering (CSR) | The browser builds the page from JavaScript and an API | No | Dashboards and pages behind a login | Delayed indexing on Google, invisible to most AI crawlers |
| Server-side rendering (SSR) | The server renders HTML on each request, then JavaScript hydrates it | Yes | Catalogs, listings, content that changes often or is personalized | Server cost, slower response time if not cached |
| Static generation (SSG) | HTML is generated at build time for each URL and served from a CDN | Yes | Marketing pages, blog, documentation, landing pages | Rebuild needed when content changes (incremental options exist) |
| Prerendering at build time | A headless browser captures the initial state of each route as static HTML | Yes | Existing SPAs with a limited number of public routes | Data frozen at build time, routes must be listed |
| Dynamic rendering | Bots receive a prerendered snapshot, users the SPA | Yes, for listed bots only | Temporary fix for a legacy app | Officially a workaround, maintenance cost, risk of serving bots different content |
Most modern frameworks let you mix strategies by route: static for marketing pages, server-rendered for the catalog, client-rendered for the logged-in app. That hybrid setup gives crawlers full HTML where search traffic happens and keeps the SPA experience where it adds value.
| Library | Framework or feature for HTML on first load |
|---|---|
| React | Next.js (server components, SSR and static generation), React Router in framework mode, Gatsby |
| Vue | Nuxt (universal rendering by default, static generation available) |
| Angular | Angular SSR (server rendering and build-time prerendering) |
| Svelte | SvelteKit (server rendering by default, per-route prerendering) |
| Any of them | Astro (static HTML by default, interactive islands loaded only where needed) |
Sites generated by AI website builders deserve a special mention: many export a client-rendered React app. If you built your site with one, see our notes on Lovable SEO and GEO before assuming Google and ChatGPT can read it.
Use the History API (/pricing, never /#/pricing), one URL per piece of content, and the same URL whether the visitor arrives directly or through in-app navigation. Every URL must load the right content when opened in a new tab, which is the test a crawler performs.
Every navigation element that leads to an indexable page should be an <a href="/path">. Framework link components (Link, NuxtLink, routerLink) output this markup; custom click handlers on div or button elements do not. Good links also carry your site architecture: if a page is not linked, crawlers rarely find it.
Each route needs its own title, meta description, canonical and Open Graph tags, generated on the server when possible. Our page on meta tags lists what to set. Keep a single canonical tag: Google recommends setting it in the HTML, and if JavaScript changes it, the result must not contradict the original.
Missing products and deleted articles must not answer 200 with an error message. With SSR, return a real 404 or 410 from the server. In a pure SPA, Google accepts two fallbacks: redirect with JavaScript to a URL that returns a 404, or add a noindex robots meta tag to the error view.
Headings, body text, prices, FAQ and JSON-LD structured data should be in the server response, not loaded after a click or a scroll. Content behind tabs that are rendered on demand, or behind "load more" buttons, is often missed.
Split the bundle by route, defer what is not needed for the first view and avoid re-rendering the whole page during hydration. Note that Chrome field data (CrUX) attributes the metrics of in-app navigations to the URL the visitor first loaded, so test your key templates individually.
Compare three versions of each key template: the raw HTML (view the source or fetch the page with a command-line tool), Google's rendered HTML (URL Inspection, then "View crawled page") and the page with JavaScript disabled. If the main text only appears in the second version, AI crawlers do not see it. Our free crawlability audit for AI and the indexability checker run these checks on any URL.
| Symptom | Likely cause | Fix |
|---|---|---|
| Search Console shows many pages "Crawled, currently not indexed" | Empty or near-identical HTML before rendering | SSR, SSG or prerendering for public routes |
Every result in a site: search shows the same title | Title not updated per route | Route-level head management on the server |
| Only the home page is indexed | Hash routing or links without href | History API and real <a> links |
| Error pages appear in the index | Soft 404s (status 200 everywhere) | Server 404, or JavaScript redirect to a 404 URL, or noindex |
| Ranks on Google but never cited by ChatGPT or Perplexity | Content only visible after JavaScript execution | Content in the first HTML response |
| Old content shown in Google's rendered view | Cached JavaScript or CSS files | Fingerprinted file names |
For years, client-side rendering was a manageable risk because Google rendered JavaScript eventually. With part of the discovery now happening in ChatGPT, Claude, Gemini and Perplexity, the cost has changed: a page that exists only after JavaScript execution is absent from the sources most assistants can read. Crawlability in the raw HTML has become the first condition of generative engine optimization.
Once the HTML is complete, the rest follows the usual rules: pages that answer one question clearly, with sourced facts and structured data, are the ones assistants quote. Sorank's GEO optimization features help you write and publish that content, and AI mention tracking shows whether ChatGPT, Claude and Gemini cite your brand after the rendering fix.
The key rule of single page application SEO fits in one sentence: whatever you want Google to rank and AI to cite must be in the HTML the server sends. Keep the SPA for the logged-in experience, render public pages on the server or at build time, then test what bots receive before and after each release.
A single page application is not penalized by Google, but its default setup, client-side rendering with one HTML shell, makes indexing slower and less reliable. Most AI crawlers do not execute JavaScript, so they see an empty page. With server-side rendering, static generation or prerendering on public routes, a SPA can rank as well as any classic website.
React itself is neutral: what matters is where the HTML is produced. A React app rendered only in the browser has the usual SPA problems, while the same components rendered on the server with Next.js or React Router in framework mode deliver complete HTML to crawlers. Choose a framework that supports server or static rendering for the pages you want indexed.
Only as a temporary measure. Google describes dynamic rendering as a workaround and not a recommended solution, because maintaining a separate version for bots adds cost and risk. Server-side rendering, static generation or build-time prerendering give the same HTML to every visitor and are the long-term fixes.
THEY TRIED SORANK BEFORE YOU
