SEO for SPAs: Single Page Application SEO Guide · Nuxt SEO

[NuxtSEO](https://nuxtseo.com/ "Home")

- [Modules](https://nuxtseo.com/docs/nuxt-seo/getting-started/introduction)
- [Tools](https://nuxtseo.com/tools)
- [Pro](https://nuxtseo.com/pro)
- [Learn SEO](https://nuxtseo.com/learn-seo/nuxt) [Releases](https://nuxtseo.com/releases)

[1.4K](https://github.com/harlan-zw/nuxt-seo)

[Nuxt SEO on GitHub](https://github.com/harlan-zw/nuxt-seo)

Learn SEO

Master search optimization

Nuxt

 Vue

[SEO Checklist](https://nuxtseo.com/learn-seo/checklist) [Pre-Launch Warmup](https://nuxtseo.com/learn-seo/pre-launch-warmup) [Backlinks & Authority](https://nuxtseo.com/learn-seo/backlinks)

[Mastering Meta](https://nuxtseo.com/learn-seo/vue/mastering-meta)

- [Titles](https://nuxtseo.com/learn-seo/vue/mastering-meta/titles)
- [Meta Description](https://nuxtseo.com/learn-seo/vue/mastering-meta/descriptions)
- [Social Sharing](https://nuxtseo.com/learn-seo/vue/mastering-meta/social-sharing)
- [Schema.org](https://nuxtseo.com/learn-seo/vue/mastering-meta/schema-org)
- [Migrating vue-meta](https://nuxtseo.com/learn-seo/vue/mastering-meta/migrating-vue-meta)
- [Rich Results](https://nuxtseo.com/learn-seo/vue/mastering-meta/rich-results)
- [Image Alt Text](https://nuxtseo.com/learn-seo/vue/mastering-meta/alt-text)

[ Controlling Crawlers](https://nuxtseo.com/learn-seo/vue/controlling-crawlers)

- [Robots.txt](https://nuxtseo.com/learn-seo/vue/controlling-crawlers/robots-txt)
- [Sitemaps](https://nuxtseo.com/learn-seo/vue/controlling-crawlers/sitemaps)
- [Robot Meta Tag](https://nuxtseo.com/learn-seo/vue/controlling-crawlers/meta-tags)
- [Canonical Link Tag](https://nuxtseo.com/learn-seo/vue/controlling-crawlers/canonical-urls)
- [HTTP Redirects](https://nuxtseo.com/learn-seo/vue/controlling-crawlers/redirects)
- [Duplicate Content](https://nuxtseo.com/learn-seo/vue/controlling-crawlers/duplicate-content)
- [llms.txt](https://nuxtseo.com/learn-seo/vue/controlling-crawlers/llms-txt)

[ SPA SEO](https://nuxtseo.com/learn-seo/vue/spa)

- [Prerendering](https://nuxtseo.com/learn-seo/vue/spa/prerendering)
- [Dynamic Rendering](https://nuxtseo.com/learn-seo/vue/spa/dynamic-rendering)
- [Hydration & SEO](https://nuxtseo.com/learn-seo/vue/spa/hydration)

[ Routes & Rendering](https://nuxtseo.com/learn-seo/vue/routes-and-rendering)

- [URL Structure](https://nuxtseo.com/learn-seo/vue/routes-and-rendering/url-structure)
- [Pagination](https://nuxtseo.com/learn-seo/vue/routes-and-rendering/pagination)
- [Trailing Slashes](https://nuxtseo.com/learn-seo/vue/routes-and-rendering/trailing-slashes)
- [Query Parameters](https://nuxtseo.com/learn-seo/vue/routes-and-rendering/query-parameters)
- [Hreflang & i18n](https://nuxtseo.com/learn-seo/vue/routes-and-rendering/i18n)
- [404 Pages](https://nuxtseo.com/learn-seo/vue/routes-and-rendering/404-pages)
- [Dynamic Routes](https://nuxtseo.com/learn-seo/vue/routes-and-rendering/dynamic-routes)
- [Internal Linking](https://nuxtseo.com/learn-seo/vue/routes-and-rendering/internal-linking)
- [Rendering Modes](https://nuxtseo.com/learn-seo/vue/routes-and-rendering/rendering)
- [Programmatic SEO](https://nuxtseo.com/learn-seo/vue/routes-and-rendering/programmatic-seo)
- [Security](https://nuxtseo.com/learn-seo/vue/routes-and-rendering/security)

[ SSR Frameworks](https://nuxtseo.com/learn-seo/vue/ssr-frameworks)

- [Nuxt vs Quasar](https://nuxtseo.com/learn-seo/vue/ssr-frameworks/nuxt-vs-quasar)
- [Custom Vite SSR](https://nuxtseo.com/learn-seo/vue/ssr-frameworks/vite-ssr)
- [VitePress SEO](https://nuxtseo.com/learn-seo/vue/ssr-frameworks/vitepress)

[ Launch & Listen](https://nuxtseo.com/learn-seo/vue/launch-and-listen)

- [Getting Indexed](https://nuxtseo.com/learn-seo/vue/launch-and-listen/going-live)
- [Google Search Console](https://nuxtseo.com/learn-seo/vue/launch-and-listen/search-console)
- [Core Web Vitals](https://nuxtseo.com/learn-seo/vue/launch-and-listen/core-web-vitals)
- [Indexing Issues](https://nuxtseo.com/learn-seo/vue/launch-and-listen/indexing-issues)
- [SEO Monitoring](https://nuxtseo.com/learn-seo/vue/launch-and-listen/seo-monitoring)
- [Site Migration](https://nuxtseo.com/learn-seo/vue/launch-and-listen/site-migration)
- [IndexNow](https://nuxtseo.com/learn-seo/vue/launch-and-listen/indexnow)
- [Debugging](https://nuxtseo.com/learn-seo/vue/launch-and-listen/debugging)
- [AI Search Optimization](https://nuxtseo.com/learn-seo/vue/launch-and-listen/ai-optimized-content)

1. [Learn SEO for Vue](https://nuxtseo.com/learn-seo)
2.
3. [SPA SEO](https://nuxtseo.com/learn-seo/vue/spa)

# SEO for SPAs: Single Page Application SEO Guide

SEO for SPAs explained: why single page applications struggle with search engines and how to fix it. Learn when you need SSR, when prerendering works, and when client-side rendering is fine.

[![Harlan Wilton](https://avatars.githubusercontent.com/u/5326365?v=4)Harlan Wilton](https://x.com/harlan-zw)12 mins read Updated Mar 23, 2026

What you'll learn

- SPAs require JavaScript execution to show content, consuming crawl budget and delaying indexing
- **INP (Interaction to Next Paint)** suffers in SPAs due to heavy hydration and main-thread work
- SSR is preferred for public content, while SPAs excel for dashboards and authenticated apps

Single page applications render content with JavaScript after the page loads. While Google has become efficient at executing JavaScript, the basic physics of Client-Side Rendering (CSR) create challenges for **Crawl Budget** and **Core Web Vitals**.

## [The Core Problem: Cost & Timing](#the-core-problem-cost-timing)

**Traditional HTML sites**: Server sends complete HTML. Crawler sees content immediately. INP is low because the browser main thread is free.

**Vue SPAs**: Server sends minimal HTML. JavaScript downloads, parses, and executes.

1. **Crawl Budget**: Googlebot uses significantly more resources to render a JS page than a static HTML page. For large sites, this means fewer pages get crawled.
2. **Indexing Delay**: While the "rendering queue" is faster in 2026 (often minutes), it still exists.
3. **INP Impact**: Hydrating a large Vue app blocks the main thread. If a user tries to interact while the app is mounting, the page feels unresponsive, hurting your Core Web Vitals.

**Interaction to Next Paint (INP)** is a stable Core Web Vital. In a pure SPA, the browser must download and execute a massive JS bundle before the page becomes interactive.Even if your "Time to Interactive" looks okay, heavy hydration can block the main thread for hundreds of milliseconds. Google takes this responsiveness into account for rankings. **SSR + Partial Hydration** is the modern solution to keep INP low.

### [When SPAs Break SEO](#when-spas-break-seo)

**Meta tags render too late**: Your `useSeoMeta()` calls run after JavaScript loads. Initial HTML has no title, description, or Open Graph tags. Social platforms (Twitter/X, [LinkedIn](https://linkedin.com)) often see nothing because they don't execute JS.

**Soft 404s**: In a SPA, every route returns `200 OK` from the server. If a product doesn't exist, you show a "Not Found" component, but the HTTP status is still 200. This confuses Google.

Since you can't change the server's HTTP status code from client-side JS, use **noindex** to signal a 404 to Google:

```
// In your 404 component
useHead({
  title: 'Page Not Found',
  meta: [
    { name: 'robots', content: 'noindex' } // Tell Google to drop this URL
  ]
})
```

**Client-side routing hides pages**: While Google handles History API well, complex client-side routing logic can sometimes obscure links until user interaction occurs.

## [When SPA Works Fine](#when-spa-works-fine)

Not every Vue app needs server-side rendering. SPAs work when:

**Your app requires authentication**: Admin panels, dashboards, internal tools. Block these from indexing with [meta robots tags](https://nuxtseo.com/learn-seo/vue/controlling-crawlers/meta-tags) anyway.

**You don't need search traffic**: Apps used through direct links or bookmarks. No need to rank in Google.

**You only share via direct links**: If users share app URLs to logged-in colleagues, Open Graph previews don't matter.

**Content is user-generated and private**: Chat apps, project management tools, personal data views.

For these cases, skip the SSR complexity. Focus on app performance and user experience instead.

## [When You Need SSR](#when-you-need-ssr)

Server-side rendering matters when:

**Public content needs indexing**: Marketing sites, blogs, documentation, product pages, landing pages. If Google should index it, you need SSR.

**Social sharing matters**: Link previews on Twitter, LinkedIn, Slack require meta tags in initial HTML. SPAs fail here without SSR or prerendering.

**Performance (INP & LCP) is critical**: SSR delivers faster First Contentful Paint because users see content before JavaScript loads, improving both user experience and search rankings.

**You want predictable indexing**: SSR guarantees search engines see your content without waiting for JavaScript execution or risking rendering failures.

## [Decision Tree](#decision-tree)

![SPA Decision Tree](https://nuxtseo.com/images/learn-seo/vue/spa-decision-tree.svg)

## [Solutions Overview](#solutions-overview)

### [Server-Side Rendering (SSR)](#server-side-rendering-ssr)

Renders pages on server for every request. Guarantees search engines and users get complete HTML immediately.

**Best for:** Dynamic content, frequent updates, personalized pages, anything requiring authentication combined with public pages.

**Tools:** [Nuxt](https://nuxt.com) (recommended), [Quasar SSR](https://quasar.dev/quasar-cli-vite/developing-ssr/introduction), custom Vite SSR.

**Trade-offs:** Requires Node.js/Edge runtime, more complex deployment.

### [Prerendering (SSG)](#prerendering-ssg)

Generates static HTML at build time for known routes. Serves static files to crawlers and users.

**Best for:** Marketing sites, blogs, documentation. content that doesn't change per request.

**Tools:** [vite-ssg](https://github.com/antfu/vite-ssg).

**Trade-offs:** Only works for routes you know at build time. Content updates require rebuilding.

### [Dynamic Rendering (Legacy/Deprecated)](#dynamic-rendering-legacydeprecated)

Serves prerendered HTML to crawlers, JavaScript app to users. **Avoid this approach in 2026.**

**Why:** Google explicitly advises against it. It's complex, prone to "cloaking" penalties, and doesn't solve INP issues for real users. Use SSR instead.

## [What Search Engines See](#what-search-engines-see)

Test your SPA to understand what crawlers see:

**Chrome DevTools**: Disable JavaScript in DevTools settings. Reload your page. This is what non-Google crawlers (and social bots) see.

**View Page Source**: Right-click → View Page Source. This is the initial HTML Google receives before JavaScript runs.

**Google Search Console**: [URL Inspection tool](https://search.google.com/search-console) shows how Google rendered your page. Compare "crawled page" vs "live page."

**Mobile-Friendly Test**: [Google's testing tool](https://search.google.com/test/mobile-friendly) renders JavaScript and shows the result.

If your content doesn't appear in these tests without JavaScript, search engines struggle with your site.

## [Meta Tags in SPAs](#meta-tags-in-spas)

Even with Google's JavaScript rendering, meta tags need to be in initial HTML for:

**Social platforms**: Twitter, Facebook, LinkedIn, Slack don't run JavaScript. They only see initial HTML.

**Speed**: Google uses meta tags from initial HTML when available, even if they change during rendering.

**Reliability**: [JavaScript execution can be blocked or delayed](https://developers.google.com/search/docs/crawling-indexing/javascript/javascript-seo-basics). Initial HTML guarantees tags are present.

### [Wrong Approach](#wrong-approach)

App.vue

```
<script setup lang="ts">
// ❌ Meta tags only exist after JavaScript runs
useSeoMeta({
  title: 'My SPA Site',
  description: 'This description only exists client-side'
})
</script>
```

Initial HTML:

```
<!DOCTYPE html>
<html>
<head>
  <!-- Empty! No meta tags. -->
</head>
<body>
  <div id="app"></div>
  <script src="/app.js"></script>
</body>
</html>
```

### [Right Approach (SSR)](#right-approach-ssr)

With server-side rendering, `useSeoMeta()` runs on the server:

```
<!DOCTYPE html>
<html>
<head>
  <title>My SPA Site</title>
  <meta name="description" content="This description exists immediately">
  <meta property="og:title" content="My SPA Site">
</head>
<body>
  <div id="app"><!-- Server-rendered content --></div>
  <script src="/app.js"></script>
</body>
</html>
```

## [Client-Side Routing](#client-side-routing)

SPAs change pages without reloading the browser. This creates problems:

**URL fragments ignored**: URLs like `yoursite.com#/about` look like `yoursite.com` to search engines. The `#/about` is a browser-only detail. Google ignores it for indexing.

**History API works**: Modern routers (Vue Router) use [History API](https://developers.google.com/search/docs/crawling-indexing/javascript/javascript-seo-basics) with real URLs (`yoursite.com/about`). This works for SEO if you have SSR or prerendering.

**Without SSR**: Every route returns the same empty HTML. Google discovers routes from links and sitemaps, but sees the same empty content for all URLs.

**With SSR**: Each route renders its own HTML. Google sees different content per URL. This works correctly.

## [URL Structure](#url-structure)

Use Vue Router with History mode, not hash mode:

router.ts

```
import { createRouter, createWebHashHistory, createWebHistory } from 'vue-router'

// ❌ Hash mode - SEO doesn't work
createRouter({
  history: createWebHashHistory(),
  routes: [/* ... */]
})

// ✅ History mode - works with SSR/prerendering
createRouter({
  history: createWebHistory(),
  routes: [/* ... */]
})
```

Hash mode URLs (`#/about`) can't be distinguished by servers. History mode URLs (`/about`) work like traditional websites.

Note: History mode requires [server configuration](https://router.vuejs.org/guide/essentials/history-mode.html#html5-mode) to handle direct URL access. All routes must serve your index.html.

## [Sitemaps for SPAs](#sitemaps-for-spas)

Even with SSR, generate a sitemap:

**Static routes**: List all routes in `public/sitemap.xml`:

public/sitemap.xml

```
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <url>
    <loc>https://yoursite.com/</loc>
    <lastmod>2026-01-29</lastmod>
  </url>
  <url>
    <loc>https://yoursite.com/about</loc>
    <lastmod>2026-01-29</lastmod>
  </url>
  <url>
    <loc>https://yoursite.com/pricing</loc>
    <lastmod>2026-01-29</lastmod>
  </url>
</urlset>
```

**Dynamic routes**: Generate sitemap from your data source during build or on demand. See [sitemaps guide](https://nuxtseo.com/learn-seo/vue/controlling-crawlers/sitemaps) for details.

## [JavaScript SEO Resources](#javascript-seo-resources)

Google's official guidance on JavaScript and SEO:

- [JavaScript SEO Basics](https://developers.google.com/search/docs/crawling-indexing/javascript/javascript-seo-basics): How Google processes JavaScript
- [Fix JavaScript SEO Problems](https://developers.google.com/search/docs/crawling-indexing/javascript/fix-search-javascript): Common issues and solutions
- [Dynamic Rendering](https://developers.google.com/search/docs/crawling-indexing/javascript/dynamic-rendering): Deprecated workaround

## [Using Nuxt?](#using-nuxt)

If you use Nuxt, it handles most of this automatically. Nuxt provides SSR by default, automatic sitemap generation, and proper meta tag handling.

[Learn more about SSR and SEO in Nuxt →](https://nuxtseo.com/learn-seo/nuxt/routes-and-rendering/rendering)

[The 2026 SEO Checklist for Nuxt & Vue Pre-launch setup, post-launch verification, and ongoing monitoring. Interactive checklist with links to every guide.](https://nuxtseo.com/learn-seo/checklist) [Haven't launched yet? Start with the Pre-Launch Warmup](https://nuxtseo.com/learn-seo/pre-launch-warmup)

---

[llms.txt Help AI assistants understand your Vue documentation with the llms.txt standard. Learn the file format, implementation, and when it matters.](https://nuxtseo.com/learn-seo/vue/controlling-crawlers/llms-txt) [Prerendering Build-time and on-demand prerendering for client-side Vue apps. How to get SPA performance with SSR indexing.](https://nuxtseo.com/learn-seo/vue/spa/prerendering)

On this page

- [The Core Problem: Cost & Timing](#the-core-problem-cost-timing)
- [When SPA Works Fine](#when-spa-works-fine)
- [When You Need SSR](#when-you-need-ssr)
- [Decision Tree](#decision-tree)
- [Solutions Overview](#solutions-overview)
- [What Search Engines See](#what-search-engines-see)
- [Meta Tags in SPAs](#meta-tags-in-spas)
- [Client-Side Routing](#client-side-routing)
- [URL Structure](#url-structure)
- [Sitemaps for SPAs](#sitemaps-for-spas)
- [JavaScript SEO Resources](#javascript-seo-resources)
- [Using Nuxt?](#using-nuxt)

[GitHub](https://github.com/harlan-zw/nuxt-seo) [ Discord](https://discord.com/invite/275MBUBvgP)

### [NuxtSEO](https://nuxtseo.com/ "Home")

- [Getting Started](https://nuxtseo.com/docs/nuxt-seo/getting-started/introduction)
- [MCP](https://nuxtseo.com/docs/nuxt-seo/guides/mcp)

Modules

- [Robots](https://nuxtseo.com/docs/robots/getting-started/introduction)
- [Sitemap](https://nuxtseo.com/docs/sitemap/getting-started/introduction)
- [OG Image](https://nuxtseo.com/docs/og-image/getting-started/introduction)
- [Schema.org](https://nuxtseo.com/docs/schema-org/getting-started/introduction)
- [Link Checker](https://nuxtseo.com/docs/link-checker/getting-started/introduction)
- [SEO Utils](https://nuxtseo.com/docs/seo-utils/getting-started/introduction)
- [Site Config](https://nuxtseo.com/docs/site-config/getting-started/introduction)
- [Skew Protection](https://nuxtseo.com/docs/skew-protection/getting-started/introduction)
- [AI Ready](https://nuxtseo.com/docs/ai-ready/getting-started/introduction)

### [NuxtSEO Pro](https://nuxtseo.com/pro "Home")

- [Getting Started](https://nuxtseo.com/pro)
- [Dashboard](https://nuxtseo.com/pro/dashboard)
- [Pro MCP](https://nuxtseo.com/docs/nuxt-seo-pro/mcp/installation)

### [Learn SEO](https://nuxtseo.com/learn-seo "Learn SEO")

Nuxt

- [Mastering Meta](https://nuxtseo.com/learn-seo/nuxt/mastering-meta)
- [Controlling Crawlers](https://nuxtseo.com/learn-seo/nuxt/controlling-crawlers)
- [Launch & Listen](https://nuxtseo.com/learn-seo/nuxt/launch-and-listen)
- [Routes & Rendering](https://nuxtseo.com/learn-seo/nuxt/routes-and-rendering)
- [Staying Secure](https://nuxtseo.com/learn-seo/nuxt/routes-and-rendering/security)

Vue

- [Vue SEO Guide](https://nuxtseo.com/learn-seo/vue)
- [Mastering Meta](https://nuxtseo.com/learn-seo/vue/mastering-meta)
- [Controlling Crawlers](https://nuxtseo.com/learn-seo/vue/controlling-crawlers)
- [SPA SEO](https://nuxtseo.com/learn-seo/vue/spa)
- [SSR Frameworks](https://nuxtseo.com/learn-seo/vue/ssr-frameworks)
- [SEO Checklist](https://nuxtseo.com/learn-seo/checklist)
- [Pre-Launch Warmup](https://nuxtseo.com/learn-seo/pre-launch-warmup)
- [Backlinks & Authority](https://nuxtseo.com/learn-seo/backlinks)

### [Tools](https://nuxtseo.com/tools "SEO Tools")

- [Social Share Debugger](https://nuxtseo.com/tools/social-share-debugger)
- [Robots.txt Generator](https://nuxtseo.com/tools/robots-txt-generator)
- [Meta Tag Checker](https://nuxtseo.com/tools/meta-tag-checker)
- [HTML to Markdown](https://nuxtseo.com/tools/html-to-markdown)
- [XML Sitemap Validator](https://nuxtseo.com/tools/xml-sitemap-validator)
- [Schema.org Validator](https://nuxtseo.com/tools/schema-validator)
- [Keyword Research Pro](https://nuxtseo.com/tools/keyword-research)
- [SERP Analyzer Pro](https://nuxtseo.com/tools/serp-analyzer)
- [Domain Rankings Pro](https://nuxtseo.com/tools/domain-rankings)

Copyright © 2023-2026 Harlan Wilton - [MIT License](https://github.com/harlan-zw/nuxt-seo/blob/main/license) · [mdream](https://mdream.dev)