Blog
SEO2026-01-0510 min

SEO for Single Page Applications (SPAs): React, Next.js, and Vue Solutions

SPAs create SEO challenges with rendering, crawling, and indexation. Here's how to solve them for each major JavaScript framework.Step-by-step process with benchmarks, examples, and tracking setup.

You built a beautiful single page application. The transitions are smooth, the interactions feel native, and your users love it. There is just one problem: Google cannot see most of your content. Your React app renders a blank div on initial load. Your Vue app relies on client-side routing that Googlebot cannot follow. Your Angular app ships 2MB of JavaScript before a single word of content appears. And organic traffic, which should be your largest acquisition channel, flatlines at near zero.

This is the SPA SEO problem, and it has been plaguing development teams since the first wave of JavaScript frameworks arrived. The good news: in 2026, the solutions are mature, well-documented, and production-tested. The bad news: most teams still get it wrong because they treat SEO as an afterthought instead of an architectural decision. The rendering strategy you choose at project kickoff determines whether Google can index your content at all, and changing it later means rewriting your entire frontend.

This guide covers every rendering strategy available for SPAs in 2026, the SEO implications of each one, framework-specific solutions for React, Next.js, Vue, Nuxt, Angular, and SvelteKit, and the exact audit process that reveals whether Google can actually see your content right now. No theory. Just the decisions and implementations that determine whether your SPA ranks or disappears.

TL;DR
  • Client-side rendered SPAs lose 60-90% of potential organic traffic because Google's rendering pipeline is delayed, resource-constrained, and sometimes fails entirely.
  • Server-side rendering (SSR) and static site generation (SSG) solve the indexation problem by delivering complete HTML in the initial response, eliminating Google's dependency on JavaScript execution.
  • Next.js, Nuxt, and SvelteKit provide built-in SSR/SSG that makes SPA SEO a configuration choice rather than an engineering project.
  • The URL Inspection tool in Google Search Console is the only reliable way to see what Google actually renders on your pages, not what you assume it renders.
  • Hybrid rendering strategies (SSG for marketing pages, SSR for dynamic content, CSR for authenticated dashboards) give you the best of all worlds without compromising on any dimension.

Why SPAs Break SEO: The Rendering Gap

Traditional multi-page websites serve complete HTML from the server. When Googlebot requests a page, it receives all the content immediately. No JavaScript execution required. No waiting. No rendering pipeline. The content is right there in the HTML response.

Single page applications work differently. The server sends a minimal HTML shell, usually a single div element, along with JavaScript bundles. The browser downloads the JavaScript, executes it, fetches data from APIs, and then renders the content into the DOM. Users see the page after all of this completes. The experience is fast for subsequent navigations because the JavaScript is already loaded and cached, but the initial render requires the full execution pipeline.

Google processes JavaScript-heavy pages in two waves. During wave one, Googlebot crawls the raw HTML and indexes whatever static content it finds. For a typical SPA, that means it indexes an empty page with a loading spinner. During wave two, Google's Web Rendering Service (WRS) executes the JavaScript and indexes the dynamically generated content. The problem is that wave two is delayed. It can take hours, days, or in some cases, it never happens at all.

70%
of SPAs have indexation gaps
content invisible to Google in wave one
2-14 days
typical rendering queue delay
before wave two processes your pages
35%
rendering failure rate
for complex JS-dependent pages

Based on analysis of JavaScript-heavy sites using Google Search Console data and rendering audits, 2025-2026

Google's Web Rendering Service Has Limits

Google's WRS runs a headless Chromium instance to render JavaScript pages. But it has constraints that development teams rarely consider. The WRS has a finite rendering budget. When your SPA has 50,000 pages that all require JavaScript rendering, Google may not have the resources to render them all. Pages at the bottom of the priority queue may never get rendered. The WRS also has timeouts. If your JavaScript takes too long to execute, the WRS gives up and indexes whatever partial content it managed to render. Third-party scripts, API calls that hang, and inefficient JavaScript bundles all contribute to rendering timeouts.

The WRS does not support all browser APIs. Features like IndexedDB, WebSocket connections, and certain Web APIs may not work in the WRS environment. If your content rendering depends on these APIs, that content will never appear in Google's index. The WRS also does not interact with your page. It does not click buttons, scroll down to trigger lazy loading, fill out forms, or dismiss modals. Content that requires user interaction to appear is invisible to Google.

The Lazy Loading Trap
Infinite scroll and lazy-loaded content blocks are invisible to Googlebot. The WRS loads your page but does not scroll. Content that loads when the user scrolls past a threshold, clicks a "Load More" button, or interacts with a tab interface will not be rendered. If you need that content indexed, it must be present in the initial render or accessible through crawlable links.

The Four Rendering Strategies: CSR, SSR, SSG, and ISR

Every web application uses one or more of four rendering strategies. Each has different trade-offs for SEO, performance, and developer experience. Understanding these trade-offs is the foundation of SPA SEO.

Client-Side Rendering (CSR): The SEO Problem Child

CSR is the default for most SPA frameworks. The server sends a minimal HTML file, the browser downloads and executes JavaScript, and the JavaScript renders the page content. This is what happens when you run create-react-app, vue create, or ng new without additional configuration.

For SEO purposes, CSR is the worst option. The initial HTML response contains no content. Google must use its rendering pipeline to see anything, which introduces delays, unreliability, and resource consumption. Meta tags like title, description, canonical, and Open Graph tags are either missing from the initial HTML or set to default values that JavaScript later changes. Google may or may not see the updated values depending on whether the rendering pipeline processes the page successfully.

CSR is acceptable for authenticated dashboards, admin panels, and internal tools where SEO does not matter. It is not acceptable for any page that needs organic search traffic: landing pages, blog posts, product pages, documentation, or marketing content.

Server-Side Rendering (SSR): Complete HTML on Every Request

SSR generates the complete HTML on the server for every request. When Googlebot (or any user) requests a page, the server executes the JavaScript, renders the component tree, and sends back fully formed HTML with all content, meta tags, and structured data already in place. The browser then "hydrates" the HTML by attaching event listeners and making the page interactive.

For SEO, SSR solves the rendering gap entirely. Google receives complete content in wave one. No rendering pipeline needed. No delays. No rendering failures. The trade-off is server cost and response time. Every request requires server-side computation to render the page. For high-traffic pages, this can be expensive and slow. Caching strategies (CDN caching, in-memory caching, stale-while-revalidate patterns) mitigate this, but they add architectural complexity.

SSR is the right choice for pages with dynamic content that changes frequently: e-commerce product pages with real-time pricing and availability, search results pages, user-generated content, and personalized landing pages where the content varies per request.

Static Site Generation (SSG): Pre-Built HTML at Deploy Time

SSG generates HTML pages at build time. Every page is pre-rendered to a static HTML file and served directly from a CDN. No server-side computation on each request. The pages load extremely fast because they are just static files served from the nearest CDN edge location.

For SEO, SSG is ideal. Google receives complete HTML instantly, pages load fast (which improves Core Web Vitals), and the infrastructure is cheap and reliable. The trade-off is that content is only as fresh as your last build. If you have 10,000 product pages and prices change hourly, SSG alone will not keep those pages current without frequent rebuilds.

SSG is the right choice for content that does not change between deploys: blog posts, documentation, marketing landing pages, about pages, pricing pages, and any content that is the same for every visitor. Most SaaS marketing sites should be statically generated.

Incremental Static Regeneration (ISR): The Best of Both Worlds

ISR, pioneered by Next.js, combines the performance benefits of SSG with the freshness of SSR. Pages are statically generated at build time, but they can be regenerated in the background when a user requests a stale page. You define a revalidation interval (for example, 60 seconds), and after that interval expires, the next request triggers a background regeneration while immediately serving the stale version.

For SEO, ISR delivers the same benefits as SSG with the added ability to keep content fresh without full rebuilds. This is particularly powerful for large sites with dynamic content. An e-commerce site with 100,000 product pages can statically generate the top 1,000 products at build time and generate the rest on demand, caching each page for future requests.

On-demand revalidation takes ISR further. Instead of waiting for a time-based interval, you can programmatically trigger page regeneration when content changes. Update a product price in your CMS, call a revalidation API endpoint, and the page is regenerated immediately. Google always sees fresh content, users always get fast page loads, and your infrastructure costs stay low.

Choosing the Right Rendering Strategy

1
Does the page need SEO?

If no (authenticated dashboards, admin panels, internal tools), use CSR. If yes, continue to step 2. This single question eliminates CSR from consideration for all public-facing pages.

2
Does content change between requests?

If no (blog posts, docs, marketing pages), use SSG. The content is the same for every visitor and only changes when you deploy. SSG gives you the fastest possible page loads and the simplest infrastructure.

3
Does content change frequently but not per-request?

If yes (product pages, pricing, inventory), use ISR. Content updates on a schedule or on-demand, but every visitor sees the same version at any given moment. ISR gives you freshness without server-side rendering on every request.

4
Does content vary per request?

If yes (personalized content, real-time data, user-specific pages), use SSR. Every request generates fresh HTML on the server. Cache aggressively at the CDN layer to reduce server load.

5
Use hybrid rendering

Most real applications use multiple strategies. Marketing pages use SSG. Product pages use ISR. Search results use SSR. Dashboards use CSR. Choose the right strategy per route, not per application.

React and Next.js: The Complete SEO Setup

React itself is a client-side library with no built-in SEO solution. A plain React app created with create-react-app or Vite renders entirely on the client. For SEO, you need a meta-framework that adds server-side rendering capabilities. Next.js is the dominant choice in the React ecosystem, used by companies from startups to enterprises.

Next.js App Router: Server Components by Default

Next.js App Router (the default in Next.js 14+) uses React Server Components by default. Every component is rendered on the server unless you explicitly mark it with "use client". This means your pages are server-rendered out of the box. Googlebot receives complete HTML without needing to execute JavaScript.

For SEO-critical metadata, Next.js provides the generateMetadata function that runs on the server and generates title tags, meta descriptions, Open Graph tags, and canonical URLs. These are included in the initial HTML response, so Google sees them immediately. No client-side JavaScript required.

Static generation is the default rendering strategy in the App Router. Pages without dynamic data are automatically statically generated at build time. Pages that use dynamic functions like cookies(), headers(), or searchParams are automatically server-rendered on each request. You do not need to explicitly choose SSG or SSR in most cases. Next.js infers the correct strategy from your code.

Essential SEO Configuration for Next.js

Beyond the defaults, several configurations are critical for SPA SEO in Next.js. First, implement generateStaticParams for dynamic routes. This tells Next.js which pages to statically generate at build time. Without it, dynamic route pages are only generated on demand, which means Googlebot may encounter a cold start delay on the first request.

Second, configure your sitemap.ts file to programmatically generate an XML sitemap. Next.js supports dynamic sitemap generation using the MetadataRoute.Sitemap type. Include all indexable pages with their last modified dates and change frequencies. Submit this sitemap to Google Search Console.

Third, set up your robots.ts file. This generates a robots.txt that allows crawling of all public pages while blocking authenticated routes, API endpoints, and internal pages. Include a reference to your sitemap URL.

Fourth, implement canonical URLs on every page. Next.js makes this straightforward through the alternates.canonical field in your metadata configuration. Canonical tags prevent duplicate content issues from trailing slashes, query parameters, and URL variations.

Fifth, add structured data using JSON-LD. Create a reusable component that outputs a <script type="application/ld+json"> tag with your Schema.org markup. Because Next.js Server Components render on the server, the structured data is present in the initial HTML response. Google does not need to execute JavaScript to find it.

The Client Component SEO Pitfall
When you mark a component with "use client" in Next.js, it still renders on the server during the initial request. The "use client" directive means the component also runs on the client for hydration and interactivity. However, any content that depends on client-only APIs (window, localStorage, browser-only hooks) will not be available during server rendering. Use conditional checks or dynamic imports with ssr: false for truly client-only components, and keep SEO-critical content in Server Components.

Vue and Nuxt: Server-Side Rendering for Vue Applications

Vue.js has the same CSR problem as React. A standard Vue application created with vue createor Vite renders entirely on the client. The solution is Nuxt, Vue's meta-framework that provides SSR, SSG, and hybrid rendering out of the box.

Nuxt 3: Universal Rendering by Default

Nuxt 3 uses "universal rendering" by default, which means pages are server-rendered on the first request and then hydrated on the client for subsequent navigations. This gives you the SEO benefits of SSR with the smooth navigation experience of an SPA. Googlebot receives complete HTML, and users get fast, app-like interactions after the initial load.

Nuxt provides the useHead composable for managing meta tags, the useSeoMeta composable for SEO-specific metadata, and the useSchemaOrg module for structured data. All of these render on the server and are included in the initial HTML response.

For static sites, Nuxt supports full static generation with nuxt generate. This pre-renders every page to static HTML at build time. For hybrid approaches, Nuxt 3 allows you to configure rendering strategy per route using route rules. Marketing pages can be statically generated while dynamic pages use SSR, all within the same application.

Nuxt SEO Module

The Nuxt SEO module (@nuxtjs/seo) bundles several SEO tools into a single package: automatic sitemap generation, robots.txt configuration, Open Graph image generation, schema.org structured data, and link checker. Installing this module handles most of the SEO infrastructure that you would otherwise need to build manually. It automatically generates canonical URLs, handles trailing slash normalization, and creates social media preview metadata.

For Vue applications that cannot migrate to Nuxt, the alternatives are less elegant. Vue Server Renderer (@vue/server-renderer) provides the low-level SSR capability, but you need to build the routing, data fetching, and meta tag management yourself. Vite SSR is another option, providing SSR capabilities through Vite's build pipeline. Both require significantly more setup than Nuxt.

Angular Universal and SvelteKit

Angular: Server-Side Rendering with Angular Universal

Angular applications use Angular Universal for server-side rendering. Since Angular 17, SSR is a first-class feature integrated into the Angular CLI. Running ng new --ssr creates a project with SSR configured out of the box. For existing projects, ng add @angular/ssr adds SSR capabilities.

Angular's SSR implementation uses Express.js on the server to render Angular components to HTML. The Meta and Title services manage meta tags that are rendered server-side. For structured data, create a service that injects JSON-LD scripts into the document head during server rendering.

Angular also supports pre-rendering (SSG) through the prerender builder. Define routes in your angular.json configuration, and Angular will generate static HTML files at build time. This is ideal for marketing pages, blog posts, and documentation within an Angular application.

One common Angular-specific SEO issue is the heavy bundle size. Angular applications tend to ship larger JavaScript bundles than React or Vue equivalents, which affects Core Web Vitals scores. Angular 17+ introduced the @defer syntax for lazy loading components, which significantly reduces initial bundle size. Use this aggressively for below-the-fold content and non-critical UI components.

SvelteKit: SSR That Just Works

SvelteKit is Svelte's meta-framework, and it handles SSR with less ceremony than any other option. Pages are server-rendered by default. The +page.server.ts file handles data loading on the server, and the +page.svelte file renders the component with that data. Meta tags are managed through the svelte:head element. Static generation is configured per route using the prerender option.

SvelteKit's advantage for SEO is its tiny runtime. Svelte compiles components to vanilla JavaScript with no framework overhead, producing significantly smaller bundles than React, Vue, or Angular. This translates directly to better Core Web Vitals scores, faster Time to Interactive, and better INP scores because there is less JavaScript competing for the main thread.

Monitor your SPA indexation automatically

OSCOM connects to Google Search Console and tracks which of your SPA pages are indexed, which are stuck in the rendering queue, and which have rendering failures. Get alerts before traffic drops.

Connect Search Console

Pre-Rendering Services: The Migration Bridge

If you have an existing CSR SPA that you cannot migrate to a meta-framework (due to time, budget, or technical constraints), pre-rendering services provide a pragmatic middle ground. These services detect search engine crawlers, render your JavaScript pages, and serve the pre-rendered HTML to the crawler while regular users continue to get the normal SPA experience.

How Pre-Rendering Works

Pre-rendering services sit between your web server and search engine crawlers. When a request comes in, the service checks the user agent. If it is Googlebot (or another search engine crawler), the request is routed to a headless browser that renders the page, waits for JavaScript execution to complete, and serves the resulting HTML. Regular users bypass this process entirely and receive the normal SPA.

The major pre-rendering services in 2026 include Prerender.io, Rendertron (Google's open-source option), and Puppeteer-based custom solutions. Prerender.io is the most turnkey option, offering plugins for major web servers (Nginx, Apache, Node.js) and CDN platforms (Cloudflare, Fastly). Rendertron requires more setup but is free and open-source.

Google officially considers pre-rendering (also called "dynamic rendering") an acceptable practice. Their documentation states that serving different content to crawlers is not cloaking as long as the content is substantively the same. The pre-rendered version should be functionally identical to what users see after JavaScript execution.

Pre-Rendering Is Not a Long-Term Solution
Dynamic rendering adds complexity, cost, and potential points of failure. The pre-rendering service must stay in sync with your application. If your SPA changes and the pre-rendering service caches stale content, Google may index outdated information. Google has also stated that they plan to eventually deprecate their recommendation of dynamic rendering as the WRS improves. Use pre-rendering as a bridge while planning a proper SSR/SSG migration.

Client-Side Routing and Crawlability

SPAs use client-side routing to navigate between pages without full page reloads. The JavaScript router intercepts link clicks, updates the URL using the History API, and swaps the page content. This creates smooth, instant navigation for users but introduces crawlability challenges for search engines.

Hash Routing vs. History Mode

Older SPAs use hash-based routing where URLs look like yoursite.com/#/about or yoursite.com/#/products/123. Google generally ignores everything after the hash, which means hash-routed pages are effectively invisible to search engines. All of your content appears to exist on a single URL.

History mode routing uses clean URLs like yoursite.com/about and yoursite.com/products/123. These are proper URLs that Google can crawl and index independently. Every modern SPA framework supports history mode routing, and it should be your default. If you are still using hash routing, migrating to history mode is step one of your SPA SEO effort.

History mode requires server configuration to work correctly. When a user navigates directly to yoursite.com/products/123(or Googlebot crawls that URL), the server needs to return your SPA's index.html file rather than a 404. This is typically configured as a catch-all or fallback route in your web server (Nginx, Apache) or hosting platform (Vercel, Netlify, Cloudflare Pages). Without this configuration, direct URL access and search engine crawling will fail.

Internal Links Must Be Crawlable

Googlebot discovers pages by following links. For links to be crawlable, they must be standard HTML anchor elements with href attributes. JavaScript-based navigation using onClick handlers, router.push(), or window.location is not reliably followed by Googlebot. Even though the WRS can execute JavaScript, link discovery primarily happens during the HTML parsing phase (wave one), not during rendering (wave two).

In React/Next.js, always use the Link component (which renders a real anchor tag) instead of programmatic navigation for content links. In Vue/Nuxt, use NuxtLink or RouterLink. In Angular, use routerLink. In SvelteKit, use standard <a> tags. All of these render as real HTML links that Googlebot can follow.

Auditing Your SPA for SEO Issues

The audit process for SPA SEO is different from traditional site audits because you need to verify what Google actually renders, not just what the raw HTML contains. Here is the step-by-step process.

SPA SEO Audit Process

1
Test with JavaScript disabled

Open Chrome DevTools, press Cmd+Shift+P (or Ctrl+Shift+P), type 'Disable JavaScript', and reload your key pages. Whatever content is missing is what Google sees during its first crawl wave. If your page is blank, you have a critical SEO problem.

2
Use URL Inspection in GSC

For every key page template, use Google Search Console's URL Inspection tool. Compare the 'HTML' tab (raw server response) with the 'Rendered Page' screenshot. If critical content, meta tags, or internal links only appear in the rendered version, your SEO depends on Google's rendering pipeline succeeding.

3
Check the initial HTML response

Use curl or 'View Page Source' (not DevTools Elements panel, which shows the live DOM) to see the raw HTML your server returns. Verify that title tags, meta descriptions, canonical tags, Open Graph tags, and structured data are present. Check that primary content headings and body text are in the HTML, not injected by JavaScript.

4
Crawl with JavaScript rendering

Run a full site crawl in Screaming Frog with JavaScript rendering enabled. Compare the 'HTML' column with the 'Rendered' column for each page. Identify pages where content, links, or meta tags differ between the raw HTML and the rendered version.

5
Verify internal link crawlability

Check that all internal links use proper anchor tags with href attributes. Search your codebase for onClick-based navigation that should be replaced with link components. Verify that your navigation menu, footer links, and in-content links are all crawlable HTML links.

6
Check the GSC Pages report

In Google Search Console, go to the Pages report and look for 'Discovered - currently not indexed' and 'Crawled - currently not indexed' entries. These often indicate pages where Google's rendering pipeline failed or timed out. Cross-reference with your sitemap to identify patterns.

Performance Optimization for SPA SEO

Core Web Vitals are a ranking signal, and SPAs face unique performance challenges that affect all three metrics. JavaScript bundle size, hydration cost, and client-side rendering overhead all contribute to slower page loads and worse interactivity scores.

Bundle Size Reduction

The average SPA ships between 500KB and 2MB of JavaScript. Every kilobyte adds to download time, parse time, and execution time. Use your framework's bundle analyzer to identify the largest dependencies: @next/bundle-analyzer for Next.js, nuxt-bundle-analyzer for Nuxt, source-map-explorer for Angular. Common offenders include moment.js (replace with date-fns or dayjs), lodash (import individual functions instead of the entire library), and UI component libraries that do not support tree shaking.

Code splitting breaks your application into smaller chunks that load on demand. Every modern framework supports route-based code splitting automatically. The code for your about page does not need to load when the user visits your homepage. Beyond route-based splitting, use dynamic imports (import()) for heavy components that are not needed on initial render: charts, editors, maps, and modals.

Hydration Optimization

Hydration is the process where the client-side JavaScript "takes over" the server-rendered HTML by attaching event listeners and making the page interactive. During hydration, the main thread is busy executing JavaScript, which blocks user interactions and hurts INP scores. The more complex your component tree, the longer hydration takes.

React 18+ supports selective hydration, which prioritizes hydrating components that the user is interacting with. Wrapping non-critical components in Suspense boundaries allows React to defer their hydration until the main thread is free. Next.js leverages this automatically for Server Components, which do not need hydration at all because they have no client-side JavaScript.

Vue 3 supports lazy hydration through the defineAsyncComponent function and Nuxt's built-in lazy hydration directives. Angular supports incremental hydration in Angular 17+ with the @defer syntax, which lazy loads and hydrates components based on triggers like viewport visibility, interaction, or idle time.

Image Optimization in SPAs

Images are typically the largest assets on a page and the primary driver of LCP scores. SPAs often mishandle images because developers use standard <img> tags instead of framework-optimized image components. Next.js provides next/image, which automatically serves images in modern formats (WebP, AVIF), generates responsive sizes, lazy loads below-the-fold images, and provides blur placeholders. Nuxt offers nuxt/image with similar capabilities.

For the LCP image (the hero image or main visual above the fold), always add the priority attribute (Next.js) or loading="eager" (other frameworks) to ensure it loads immediately rather than being lazy loaded. Add explicit width and height attributes to prevent layout shift (CLS). Use the sizes attribute to tell the browser what size the image will be rendered at, so it can download the correct responsive variant.

Insight
A common SPA performance antipattern is loading data in a parent component and passing it down through props to child components that render the actual content. This creates a waterfall: the parent must load, render, and pass data before the children can even start rendering. Instead, use parallel data loading where each component fetches its own data independently. In Next.js, use React Server Components with parallel async data fetching. In Nuxt, use the useAsyncData composable at the component level. This eliminates data waterfalls and reduces time to meaningful content.

Structured Data and Meta Tags in SPAs

Meta tags and structured data must be present in the initial HTML response for reliable indexation. If they are injected by client-side JavaScript, Google may or may not see them depending on whether the rendering pipeline processes your page. This is a risk that is easy to eliminate.

Framework-Specific Meta Tag Management

Every framework has a server-side meta tag solution. In Next.js, use the generateMetadata function or the Metadata export in your page files. In Nuxt, use useSeoMeta or useHead in your setup function. In Angular, use the Meta and Title services. In SvelteKit, use svelte:head. All of these render meta tags on the server.

Do not use client-side libraries like react-helmet or vue-metafor SEO-critical meta tags. These libraries manage meta tags on the client side, which means the initial HTML response will contain default or empty meta tags. Google may index these defaults instead of the dynamically injected values. Use your framework's built-in server-side metadata solution.

Structured Data in JavaScript Frameworks

JSON-LD structured data should be rendered server-side, just like meta tags. In Next.js, create a component that renders a <script type="application/ld+json"> element with your Schema.org data. Because Server Components render on the server, the structured data is present in the initial HTML. In Nuxt, the useSchemaOrg composable from the Nuxt SEO module handles this. In Angular, inject the JSON-LD script into the document head using a service that runs during server rendering.

Validate your structured data implementation by checking the initial HTML response (not the rendered DOM). Use curl -s your-url | grep "application/ld+json"to verify the structured data is present in the raw HTML. Then run the URL through Google's Rich Results Test to confirm the schema is valid and eligible for rich results.

Migrating a CSR SPA to SSR: The Step-by-Step Plan

Migrating from client-side rendering to server-side rendering is one of the highest-impact SEO projects you can undertake. It is also one of the most complex. Here is the practical migration plan that minimizes risk and maximizes impact.

CSR to SSR Migration Plan

1
Audit the current state

Document every page type, component dependency, API call, and third-party integration. Identify components that use browser-only APIs (window, document, localStorage). These will need conditional rendering or refactoring during migration.

2
Choose your meta-framework

React apps migrate to Next.js. Vue apps migrate to Nuxt. Angular apps add Angular Universal. Svelte apps migrate to SvelteKit. Each migration has different complexity levels, but the general approach is the same: incrementally move pages to the new framework.

3
Migrate incrementally by route

Start with the highest-traffic pages that currently have the worst indexation. Migrate one page type at a time (blog posts first, then product pages, then landing pages). Verify each migrated page type in GSC before moving to the next. Do not attempt a big-bang migration.

4
Handle browser-only code

Components that access window, document, localStorage, or other browser APIs need to be wrapped in client-side checks. In Next.js, use 'use client' directives and dynamic imports with ssr: false. In Nuxt, use the <ClientOnly> component. In Angular, use isPlatformBrowser checks.

5
Implement redirects

If URL structures change during migration, set up 301 redirects from old URLs to new URLs. Map every old URL to its new equivalent. Submit the updated sitemap to GSC. Monitor GSC for crawl errors on old URLs.

6
Validate and monitor

After each page type migration, verify in GSC URL Inspection that the page is now server-rendered. Check that meta tags, structured data, and content are present in the initial HTML. Monitor organic traffic for the migrated pages over 4-6 weeks to confirm improvement.

The Quick Win: Pre-Rendering as a Bridge
If migration will take months, implement pre-rendering (Prerender.io or Rendertron) as a temporary fix. This gives you immediate SEO improvements while the full SSR migration proceeds. The pre-rendering service catches search engine crawlers and serves pre-rendered HTML, solving the indexation problem today while you build the proper solution.

Common SPA SEO Mistakes (and How to Fix Them)

After auditing hundreds of SPAs for SEO issues, these are the mistakes that appear most frequently. Each one can cost you significant organic traffic, and most are straightforward to fix once identified.

  • Default meta tags on every page. The SPA sets a default title and description in index.html, and client-side routing updates them. But the initial HTML always has the defaults. Solution: server-render meta tags using your framework's metadata API.
  • Missing canonical tags. SPAs often omit canonical tags entirely or set a single canonical for all pages. Solution: generate unique canonical URLs for every indexable page, server-rendered in the initial HTML.
  • Hash routing. URLs like yoursite.com/#/about are invisible to Google. Solution: switch to history mode routing with proper server configuration.
  • JavaScript-dependent internal links. Navigation that uses onClick handlers or programmatic routing instead of anchor tags. Solution: use your framework's Link component, which renders real HTML anchor elements.
  • No sitemap. SPAs with client-side routing often have no XML sitemap because there is no server-side route registry. Solution: generate a sitemap from your route configuration or CMS content.
  • Soft 404s. SPAs that show a "Not Found" message on a 200 response when a route does not exist. Google may try to render and index these pages. Solution: return a proper 404 status code from the server for non-existent routes.
  • Infinite scroll without pagination. Content below the initial viewport that loads via infinite scroll is invisible to Google. Solution: implement paginated URLs alongside infinite scroll, or use "Load More" buttons that are actual links to paginated routes.
  • Heavy third-party scripts. Chat widgets, analytics bundles, and advertising scripts that block the main thread and degrade INP. Solution: defer third-party scripts, load them after the critical rendering path, and audit their performance impact regularly.

Catch SPA SEO issues before they cost you traffic

OSCOM audits your site for rendering issues, missing meta tags, broken structured data, and indexation gaps. Get a prioritized fix list in minutes, not weeks.

Run your free SEO audit

Testing and Monitoring SPA SEO

SPA SEO requires ongoing monitoring because any code change can break server rendering, alter meta tags, or introduce JavaScript errors that prevent Google from seeing your content. Build these checks into your development workflow.

Pre-Deployment Testing

Before every deployment, run automated tests that verify server-rendered HTML contains the expected meta tags, structured data, and content. In a CI/CD pipeline, this can be as simple as making HTTP requests to your staging server and asserting on the HTML response body. Check that title tags are unique per page, that canonical URLs are correct, that Open Graph images resolve, and that JSON-LD structured data is valid.

Use a headless browser test (Playwright or Puppeteer) alongside the HTML assertion tests. The headless browser test verifies that the page renders correctly after JavaScript execution, catching hydration errors, JavaScript crashes, and visual regressions that could affect the rendered output Google sees.

Post-Deployment Monitoring

After deployment, monitor Google Search Console for indexation changes. Set up alerts for drops in indexed page count, increases in "Crawled - currently not indexed" pages, and Core Web Vitals regressions. These signals indicate that a deployment may have broken server rendering or degraded performance.

Schedule a weekly crawl of your site using Screaming Frog with JavaScript rendering enabled. Compare the results week-over-week to catch regressions in meta tags, structured data, internal linking, and rendered content. Automate this with Screaming Frog's API or a custom crawler script that stores results in a database for trend analysis.

Monitor Core Web Vitals using the web-vitalsJavaScript library, which reports real user metrics to your analytics platform. Track LCP, INP, and CLS at the page level, not just the site level. A single page template with poor performance can drag down your entire site's CWV assessment in Google's CrUX data.

Key Takeaways

  • 1Client-side rendering is not viable for pages that need organic search traffic. Server-side rendering or static generation must be used for all public-facing, indexable content.
  • 2Next.js (React), Nuxt (Vue), Angular Universal, and SvelteKit all provide production-ready SSR solutions. The choice depends on your existing tech stack, not SEO considerations, because all four solve the rendering problem equally well.
  • 3Hybrid rendering (SSG for static content, SSR for dynamic content, CSR for authenticated sections) is the optimal architecture for most applications.
  • 4Pre-rendering services are a valid temporary solution while migrating to proper SSR, but they add complexity and should not be your long-term strategy.
  • 5History mode routing, crawlable internal links, server-rendered meta tags, and proper HTTP status codes are non-negotiable SPA SEO requirements.
  • 6Test server-rendered HTML in CI/CD pipelines and monitor Google Search Console post-deployment to catch regressions before they impact traffic.
  • 7Core Web Vitals optimization for SPAs requires reducing JavaScript bundle size, optimizing hydration, and using framework-provided image optimization components.

Your SPA Can Rank. You Just Need the Right Architecture.

The narrative that SPAs are bad for SEO is outdated. SPAs with proper server-side rendering rank just as well as traditional multi-page websites. In many cases, they rank better because the performance optimizations built into frameworks like Next.js and Nuxt deliver superior Core Web Vitals scores compared to traditional server-rendered sites weighed down by WordPress plugins and legacy JavaScript.

The key is making the rendering strategy decision early and correctly. If you are starting a new project, choose a meta-framework (Next.js, Nuxt, SvelteKit) that provides SSR and SSG out of the box. If you have an existing CSR SPA, plan the migration incrementally, starting with the highest-impact pages. And if migration is months away, implement pre-rendering today so you are not losing organic traffic while the engineering work proceeds.

The SPA SEO problem is solved. It has been for years. The question is whether your team has implemented the solution.

Get tactical SEO and engineering playbooks every week

Framework-specific guides for SPA SEO, performance optimization, rendering strategies, and technical SEO. Built for developers and technical marketers. Unsubscribe anytime.

Find where you're losing traffic and what to fix first

Oscom SEO scores every keyword across 6 dimensions and shows you the highest-value opportunities you're missing right now.