SEO for Single-Page Applications: React, Vue, and Beyond
Single-page applications (SPAs) — sites built primarily with React, Vue, Angular, or similar — have historically struggled with SEO. The fundamental tension: SEO requires HTML content for search engines to crawl and index; SPAs render most content via JavaScript after the initial page load. In 2026, with Google’s JavaScript rendering more capable than ever, this tension is solvable — but only with deliberate rendering strategy.
This guide is the practical playbook for SEO-on-SPA. Rendering options, framework-specific patterns, and the indexation traps that still kill SPA visibility.
The fundamental problem
Traditional websites: server returns full HTML with content. Search engines parse it instantly.
Traditional SPAs: server returns mostly empty HTML shell. JavaScript runs in the browser, fetches data, builds the DOM. Content is constructed in the user’s browser.
For search engines, this creates two challenges:
- Crawl budget: rendering JavaScript is expensive. Googlebot does it, but slower and with capacity limits.
- Two-pass rendering: Google first crawls HTML, then queues JavaScript rendering for later. Initial crawl might miss content; final indexed version arrives days later.
Solving for SEO means making sure search engines see meaningful content in the initial HTML, not just an empty shell.
The four rendering strategies
1. Client-side rendering (CSR)
Pure SPA. Server returns shell HTML; JavaScript builds everything.
SEO impact: poor. Google can render JavaScript but with limitations. Other crawlers (Bing, social media link previews, AI search) often can’t.
Use when: app is gated behind authentication and SEO doesn’t matter.
2. Server-side rendering (SSR)
Server renders the JavaScript on each request, returns fully-rendered HTML to the browser. Browser then “hydrates” the page to add interactivity.
SEO impact: excellent. Full HTML returned on every request.
Use when: dynamic content that changes per user or frequently. Examples: e-commerce product pages with real-time pricing, news sites.
Cost: server CPU cost per page render. Caching helps.
3. Static site generation (SSG)
Server pre-renders all pages at build time, returns static HTML for each route. No per-request rendering.
SEO impact: excellent. Equivalent of plain HTML site.
Use when: content doesn’t change per user, only changes when content updates (blog, marketing site, documentation).
Cost: build time grows with site size. 10,000 pages = several minutes to build.
4. Incremental Static Regeneration (ISR) / Dynamic SSR
Hybrid: most pages static, but specific routes re-render on demand or on a schedule.
SEO impact: excellent if configured right.
Use when: large site where some pages change often but most don’t. Examples: large e-commerce with frequent product updates, news + evergreen content.
Framework-specific patterns
Next.js (React)
Next.js makes SSR, SSG, and ISR seamless. Use:
App Router (current standard):
- Default: server components (SSR-equivalent)
generateStaticParamsfor SSGrevalidateoption for ISRdynamicParamsfor dynamic routes
// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
const posts = await getAllPosts();
return posts.map(post => ({ slug: post.slug }));
}
export const revalidate = 3600; // ISR: revalidate hourly
export default async function BlogPost({ params }: { params: { slug: string } }) {
const post = await getPost(params.slug);
return <article>{/* ... */}</article>;
}
Nuxt (Vue)
Nuxt 3 defaults to SSR. For SSG, use nuxt generate. For dynamic routes:
// pages/blog/[slug].vue
const { data: post } = await useFetch(`/api/posts/${route.params.slug}`);
Nuxt handles the SSR rendering automatically.
Astro
Astro is interesting: static by default, “islands” hydrate selectively. Excellent for marketing sites and blogs where most content is static but some interactive widgets need JavaScript.
---
// pages/blog/[slug].astro
export async function getStaticPaths() {
const posts = await getCollection('posts');
return posts.map(post => ({ params: { slug: post.slug }, props: { post } }));
}
const { post } = Astro.props;
---
<article>{post.content}</article>
<InteractiveWidget client:visible /> <!-- Only this hydrates -->
SvelteKit
SvelteKit defaults to SSR. Configure SSG per route:
// +page.js
export const prerender = true; // SSG for this route
Angular (with Angular Universal)
Angular requires explicit setup for SSR via Angular Universal. More complex than React/Vue but doable.
Pure React (no framework)
Pure React SPA with create-react-app or Vite: you need to add SSR yourself (using libraries like Next.js or build SSR via Express + ReactDOMServer). Or accept CSR limitations.
In 2026: don’t build new pure-CSR React apps for SEO-relevant content. Use Next.js, Remix, or Astro.
The critical SEO checklist for SPAs
1. Server returns meaningful HTML
The most important thing. View source on a page (Ctrl+U in browser). If you see meaningful content, you’re SSR/SSG. If you see an empty shell with <div id="app"></div>, you’re CSR and have SEO problems.
2. URLs change on navigation
SPAs use client-side routing. Each route must produce a unique URL. Use the History API (pushState), not hash-only routing (#section).
Most modern frameworks handle this. Don’t use libraries that fight it.
3. Status codes are correct
404 pages must return HTTP 404 status, not 200. SPA frameworks often return 200 for all routes by default. Configure server-side:
// Next.js: app/not-found.tsx returns 404 status
4. Meta tags are server-rendered
<title>, <meta description>, <link rel="canonical">, Open Graph tags — all must be in the initial HTML, not added by JavaScript after load. Social media scrapers especially can’t run JavaScript.
Use framework-provided head management:
- Next.js:
MetadataAPI - Nuxt:
useHeadcomposable - Astro: standard
<Head>content - SvelteKit:
<svelte:head>
5. Structured data renders server-side
JSON-LD schema in HTML, not added by JavaScript. Same reason as meta tags.
6. Pagination uses URLs
Infinite scroll without URLs = invisible to Googlebot. Provide URL-based pagination (?page=2) as fallback even if UI shows infinite scroll.
7. Sitemap.xml lists all routes
For SPAs with dynamic routes, generate sitemap including all routes you want indexed:
- Next.js:
sitemap.tsroute - Astro:
@astrojs/sitemapintegration - Manual: build a sitemap generator that pulls all routes
8. Robots.txt is accessible
Standard requirement. Most SPA frameworks serve this from the root by default.
9. Image lazy loading is configured correctly
Don’t lazy-load above-the-fold images (Googlebot won’t render them as quickly). Use loading="lazy" only below the fold.
10. Core Web Vitals optimization
SPAs can have CWV challenges:
- Large JavaScript bundles slow LCP
- Layout shifts during hydration
- INP issues from hydration code
Audit with Lighthouse and PageSpeed Insights. Fix per our Core Web Vitals article.
Testing your SPA’s SEO
1. View source — does meaningful content render?
2. Mobile-Friendly Test at search.google.com/test/mobile-friendly — what does Google see?
3. URL Inspection in Search Console — see exactly what Google fetched and rendered.
4. Fetch with curl — what does a non-JS-capable bot see?
curl -A "Twitterbot" https://yoursite.com/page
5. Search Console → Pages → Coverage — are URLs being indexed?
If any of these return empty or wrong content, you have an SSR/SSG issue to fix.
Common SPA SEO mistakes
1. Pure CSR for marketing content. Pages that should rank in search but only render via JavaScript. Migrate to SSR/SSG.
2. Hash-only routing. /#/page instead of /page. Google can’t distinguish.
3. Meta tags added in componentDidMount. Too late — initial HTML already served without them.
4. No 404 status codes. All routes return 200. Confuses Google.
5. Soft 404s from empty templates. SPA rendering empty content for invalid routes. Need explicit 404 handling.
6. JavaScript-only sitemaps. Sitemap.xml that requires JS to generate. Build it server-side.
7. Fighting framework defaults. Trying to disable SSR in Next.js for performance reasons that don’t actually apply. Use the framework’s strengths.
8. Heavy hydration cost killing INP. Too much JS hydrates on every page. Use partial hydration (Astro islands, React Server Components).
A 30-day SPA SEO audit and fix sprint
Days 1-7: Audit current state.
- View source on top 10 pages. SSR or CSR?
- Search Console: which pages are indexed vs. not?
- Run Lighthouse/PageSpeed Insights — performance bottlenecks?
Days 8-15: Pick rendering strategy.
- For marketing/content: SSG or ISR
- For dynamic data: SSR with caching
- Migrate framework configuration
Days 16-22: Implementation.
- Migrate routes to chosen rendering mode
- Verify meta tags, schema, status codes server-side
- Test in staging
Days 23-30: Deploy and validate.
- Production deploy
- Submit URLs to Search Console for re-indexing
- Monitor over 4-6 weeks for indexation improvement
By day 30+, your SPA serves Google what it needs to rank.
Frequently asked questions
Can Google render JavaScript content? Yes, but with a delay (the two-pass rendering). For ranking, SSR/SSG content always outperforms purely-rendered JS content.
Does an SPA hurt rankings even with SSR? Properly-implemented SSR matches static-site rankings. The challenge is implementation discipline, not the technology.
What if my SPA can’t be migrated to SSR? Use dynamic rendering as a stopgap: detect bots, serve them pre-rendered HTML; serve users the SPA. Not ideal long-term but workable.
Are AI search engines worse with SPAs than Google? Yes — ChatGPT, Perplexity, Bing AI generally have weaker JavaScript rendering than Google. Pure CSR is invisible to many AI search systems.
How do I decide between SSG and ISR? SSG if content rarely changes (blogs, marketing pages). ISR if content changes but you don’t need real-time freshness. SSR if data must be real-time.
SPA SEO is solvable in 2026 — the technology and frameworks exist. The frameworks that consistently produce SEO-friendly SPAs (Next.js, Nuxt, Astro, SvelteKit) make rendering strategy a configuration choice rather than a re-architecture project. The brands that struggle with SPA SEO usually struggle because they treated rendering as a build-time decision rather than an SEO-critical strategic choice. Treat it as the latter; the rest is implementation.