---
title: "SPA SEO: How to Get Single Page Applications Indexed"
description: "Check what crawlers can read in your single page application. Compare initial and rendered HTML, then fix crawlable URLs and missing content."
canonical_url: "https://nuxtseo.com/learn-seo/spa-seo"
last_updated: "2026-10-08"
---

::key-takeaways
- Google can render JavaScript. Check whether your important content appears in the initial and rendered HTML.
- SSR and prerendering make content available without client-side rendering. They do not guarantee indexing or good performance.
- Use distinct URLs, crawlable links, and correct status codes for public pages.
::

A single page application ships one HTML shell and builds every page in the browser with JavaScript. Client navigation can avoid a full document reload. A client-rendered page can send an HTML shell without its main content. Inspect the response instead of assuming every crawler sees the same result.

This guide is framework-agnostic: the same fixes apply whether you build with [React](https://react.dev), Vue, [Angular](https://angular.dev), [Svelte](https://svelte.dev), or vanilla JS. Working with Vue specifically? The [Vue SPA guide](/learn-seo/vue/spa) covers the same ground with Vue tooling.

## Why SPAs Struggle in Search

### Google renders JavaScript, eventually

Google processes pages in [three phases: crawling, rendering, and indexing](https://developers.google.com/search/docs/crawling-indexing/javascript/javascript-seo-basics). Google can use content from server-rendered HTML. Client-rendered content depends on successful rendering. Neither path guarantees indexing.

Google publishes no timing guarantee for that queue, only that a page "may stay on this queue for a few seconds, but it can take longer than that". Check the indexed and rendered versions when updates are missing from Search.

[Reported internal observations](https://www.seroundtable.com/google-crawling-indexing-serving-data-42225.html) distinguish rendering execution from waiting in its queue. Content can render quickly once scheduled while earlier processing still adds delay. The [Google Search timeline explanation](/learn-seo/google-search-timelines) separates those stages without promising an indexing deadline.

### Check Each Crawler's Rendering Support

Rendering support varies by client. Server-rendered HTML makes content available without depending on that support.

[OpenAI documents separate bots](https://developers.openai.com/api/docs/bots) for training, Search, and user-requested fetching. Configure access for the task you intend. Those roles do not establish a universal JavaScript limitation.

For Google Search's AI features, use Google's [current AI optimization guidance](https://developers.google.com/search/docs/fundamentals/ai-optimization-guide). Google does not require llms.txt or special AI markup.

### Hydration hurts Core Web Vitals

JavaScript execution and hydration can create long main-thread tasks. Those tasks can delay interactions and affect Interaction to Next Paint (INP). Measure your application rather than assuming every SPA has poor responsiveness. Google [recommends good Core Web Vitals](https://developers.google.com/search/docs/appearance/core-web-vitals) for page experience. Good scores do not guarantee rankings. Aim for INP under 200ms.

### Soft 404s

A traditional server returns a `404 Not Found` status for a missing page. A catch-all SPA fallback can return the `index.html` shell with `200 OK` for missing URLs and lets JavaScript decide to show a "not found" view. To Google [that looks like a valid page with thin content](https://developers.google.com/search/docs/crawling-indexing/javascript/fix-search-javascript), and Google may classify the response as a soft 404. Fixes are in the [requirements section](#technical-requirements-handle-404s-properly) below.

## Diagnose Your SPA

Start with these three checks:

1. **View source.** Right-click your deployed page, "View Page Source". If your text isn't in the raw HTML, you depend on JavaScript rendering for that content
2. **URL Inspection in Search Console.** Run "Test Live URL" and compare the crawled HTML with the rendered HTML. Content that only appears in the rendered version depends on the render queue
3. **Disable JavaScript.** Turn off JS in DevTools and reload. This checks which content your page exposes without JavaScript

## When SPA SEO Doesn't Matter

Not every app needs to rank. Skip most of this guide if:

- **Everything is behind a login.** A bank, a CRM, an admin panel. Crawlers can't log in and there's nothing public to index
- **The URL is the product.** The Figma editor (`figma.com/design/...`) doesn't need SEO; the marketing site (`figma.com`) does
- **It's an internal tool.** Dashboards and employee portals

Use authentication for private pages. Noindex can exclude crawlable public pages from search, but it does not restrict access. Focus this guide on your public pages.

## Rendering Strategies

SSR and static rendering expose content before browser JavaScript runs. Choose based on the page's data and deployment requirements.

### Server-Side Rendering (SSR)

The server renders each page per request and sends complete HTML. Best for dynamic content that changes per request or per user: e-commerce, news, search results. Hosting location, caching, and backend latency affect time to first byte. Measure the deployed route before choosing an edge deployment.

### Static Rendering: SSG, Prerendering, and ISR

Static Site Generation (SSG) renders every page once at build time and outputs static files. Best for blogs, docs, and marketing sites where content changes at deploy time. The constraint is build time: rendering 100,000 pages per deploy stops being fun.

Incremental Static Regeneration (ISR) is a hybrid of SSR and SSG: the framework generates pages statically, then re-renders them in the background when they go stale. Consider it for large catalogs when a full rebuild is too slow and content still changes.

### Dynamic Rendering: don't

Serving prerendered HTML to bots and the SPA to humans was a common workaround. Google now [calls it exactly that](https://developers.google.com/search/docs/crawling-indexing/javascript/dynamic-rendering): "a workaround and not a long-term solution", with SSR or static rendering recommended instead. It isn't treated as cloaking as long as bots and humans get similar content, but it doubles your infrastructure and fixes nothing for real users. See [dynamic rendering](/learn-seo/vue/spa/dynamic-rendering) for the full story.

## Technical Requirements

These apply regardless of rendering strategy.

### Use Real, Crawlable URLs

Google generally does not support URL fragments for distinct content pages. Avoid hash-routed content URLs such as `example.com/#/about`. Use the History API so every page gets a clean, individually indexable path (`example.com/about`). Check your router's history mode and configure the server to handle direct requests to those paths.

Crawlers also discover pages by following `<a href>`{lang="html"} links. They don't click buttons or run `router.push()`{lang="ts"}:

```html
<!-- ✅ Crawler can follow this -->
<a href="/pricing">View Pricing</a>

<!-- ❌ Crawler ignores this -->
<button onclick="goToPricing()">View Pricing</button>
```

### Ship Metadata and Canonicals in the Initial Response

Each page needs its own `<title>`{lang="html"}, meta description, and canonical URL, present in the HTML the server sends. Client-side updates arrive too late for bots that don't render. Verify with the [meta tag checker](/tools/meta-tag-checker).

SPAs also accumulate URL variants (`?ref=twitter`, `?utm_source=...`). A canonical tag gives Google a preferred URL for duplicate or very similar content. Google can select another URL. More in the [canonical URLs guide](/learn-seo/vue/controlling-crawlers/canonical-urls).

### Handle 404s properly

Google [recommends two options](https://developers.google.com/search/docs/crawling-indexing/javascript/fix-search-javascript) for missing pages in an SPA. Prefer redirecting to a URL your server answers with a real 404:

```ts
window.location.href = '/not-found'
```

Or, if a redirect isn't possible, inject a noindex tag from your error state:

```ts
if (pageNotFound) {
  const meta = document.createElement('meta')
  meta.name = 'robots'
  meta.content = 'noindex'
  document.head.appendChild(meta)
}
```

## Framework Notes

- **Vue / Nuxt**: Nuxt server-renders by default. Configure prerendering or cache rules per route; ISR availability depends on the deployment provider. The Nuxt SEO module bundles sitemap, robots, [Schema.org](http://Schema.org), and OG image generation. Start with the [Nuxt SEO guide](/learn-seo/nuxt). Check the current Vue release notes before adopting experimental rendering features
- **React / Next.js**: In the [Next.js App Router](https://nextjs.org/docs/app/getting-started/server-and-client-components), Server Component code stays on the server. Client Components add browser JavaScript. Use the built-in `Metadata` API for titles and Open Graph tags
- **Svelte / SvelteKit**: [SvelteKit](https://svelte.dev/docs/kit) server-renders by default; load data in `+page.server.js` so it's in the initial HTML
- **Angular**: SSR is first-class via [`@angular/ssr`](https://angular.dev/guide/ssr) (`ng add @angular/ssr`), which replaced the old Angular Universal package. Use the `Meta` and `Title` services so tags land in the server response

## Visibility in AI Answers

Keep readable public content and useful navigation as the common foundation. [Google Search](https://developers.google.com/search/docs/fundamentals/ai-optimization-guide) does not need special AI files, chunking, or schema markup for its generative AI features.

Semantic HTML helps people and clients identify your main content. Use structured data for supported content types and eligible search features. Neither change guarantees a citation.

See [AI Search Optimization](/learn-seo/vue/launch-and-listen/ai-optimized-content) for the provider-specific limits.

## Tools

1. [Google Search Console](https://search.google.com/search-console): indexing status, Core Web Vitals field data, and the URL Inspection tool for crawled-vs-rendered comparison
2. [PageSpeed Insights](https://pagespeed.web.dev/): available field Core Web Vitals and lab diagnostics
3. [Screaming Frog](https://www.screamingfrog.co.uk/): desktop crawler with JS rendering (paid tier) to audit rendered content at scale
4. [Meta tag checker](/tools/meta-tag-checker): confirm titles and descriptions are server-rendered

## Checklist

::checklist{#spa-seo}
- Public pages expose content through server rendering, prerendering, or a verified client-rendering path
- Raw HTML contains your content ("View Source" test passes)
- URLs use the History API, no `#` routing
- All internal navigation uses `<a href>`{lang="html"} links
- Titles, descriptions, and canonicals ship in the initial response
- Missing pages return a real 404 or inject noindex
::

For framework-specific walkthroughs: [Nuxt SEO guide](/learn-seo/nuxt) · [Vue SPA SEO](/learn-seo/vue/spa)

## Sitemap

See the full [sitemap](/sitemap.md) for all pages.
