---
title: "SPA SEO for Vue: When You Actually Need SSR"
description: "Most Vue apps do not need SSR. Learn when client-side rendering costs you rankings and when it is safe to ship a pure SPA instead."
canonical_url: "https://nuxtseo.com/learn-seo/vue/spa"
last_updated: "2026-07-16"
---

<note>

Looking for a framework-agnostic guide? Check out our [Complete Guide to Single Page Application SEO](/learn-seo/spa-seo) for [React](https://react.dev), Vue, and [Angular](https://angular.dev).

</note>

<key-takeaways>

- Search engines need to execute your JavaScript before they see SPA content, which delays indexing compared to server-rendered HTML.
- Heavy hydration blocks the main thread and hurts Interaction to Next Paint (INP), the Core Web Vital most SPAs struggle with.
- SSR is the right call for public content that needs to rank; a pure SPA is fine for dashboards and other logged-in tools.
- AI crawlers like GPTBot don't execute JavaScript at all, so unrendered SPA content is invisible to them too.

</key-takeaways>

A single page application renders its content with JavaScript after the page loads. Google can execute that JavaScript, but the browser still has to download, parse, and run your bundle before anything appears, and that costs you time on both indexing and Core Web Vitals.

## Why SPAs Struggle

### Indexing waits on rendering

Google processes pages in [three phases: crawling, rendering, and indexing](https://developers.google.com/search/docs/crawling-indexing/javascript/javascript-seo-basics). A server-rendered page gets indexed straight from the initial HTML. A client-rendered Vue app waits for Googlebot to execute its JavaScript before that content counts. Google gives no timing guarantee for the wait, only that a page "may stay on this queue for a few seconds, but it can take longer than that." On a large site, the gap between what you shipped and what's indexed compounds.

### AI crawlers see even less

AI crawlers fetch pages but don't render them. [Vercel's analysis](https://vercel.com/blog/the-rise-of-the-ai-crawler) found GPTBot, ClaudeBot, and PerplexityBot all download JavaScript files without executing them, so a pure SPA is invisible to them unless the initial HTML already has the content. [Gemini](https://gemini.google.com) is the exception: it crawls through Googlebot's rendering pipeline and gets the fully rendered page.

### Hydration blocks interaction

Hydrating a Vue app locks the main thread while it downloads, parses, and runs. A visitor who tries to click during that window gets no response, which shows up as poor Interaction to Next Paint (INP). Google treats Core Web Vitals as a [page experience signal](https://developers.google.com/search/docs/appearance/core-web-vitals) its ranking systems reward, mainly as a tiebreaker between similarly relevant pages, so a bad INP alone won't sink you, but it's a real cost to real users. Aim for INP under 200ms.

### When SPAs Break SEO

**Meta tags render too late**: your `useSeoMeta()` calls run after JavaScript loads, so the initial HTML has no title, description, or Open Graph tags. Social platforms like Twitter/X and [LinkedIn](https://linkedin.com) see nothing, since 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, which reads to Google as a valid page with thin content.

<tip title="Handling 404s in SPAs">

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

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

If a redirect isn't possible, inject a noindex tag from your error state instead:

```ts
// In your 404 component
useHead({
  title: 'Page Not Found',
  meta: [
    { name: 'robots', content: 'noindex' }
  ]
})
```

</tip>

**Client-side routing hides pages**: Google discovers pages by following real `<a href>` links, not JavaScript click handlers or `router.push()` calls. A button that navigates on click is invisible to a crawler; an anchor tag isn't.

## 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](/learn-seo/vue/controlling-crawlers/meta-tags) anyway.

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

**You only share via direct links**: if users share app URLs with 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 and put your effort into app performance and user experience instead.

## 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, and Slack require meta tags in the initial HTML. SPAs fail here without SSR or prerendering.

**Performance is critical**: SSR delivers faster First Contentful Paint and a lower INP, because users interact with real content instead of a loading shell.

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

The decision comes down to whether content needs indexing and how often routes change:

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

## Solutions Overview

### Server-Side Rendering (SSR)

Renders pages on the 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 a Node.js/Edge runtime and a more complex deployment. Once you add SSR, watch for [hydration mismatches](/learn-seo/vue/spa/hydration): a server/client markup mismatch can make Google index a broken version of the page.

### Prerendering (SSG)

Generates static HTML at build time for known routes and 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, and content updates require a rebuild. Full walkthrough: [Prerendering Vue SPAs](/learn-seo/vue/spa/prerendering).

### Dynamic Rendering: skip it

Serves prerendered HTML to crawlers and the full JavaScript app to humans.

**Why not:** Google [calls it](https://developers.google.com/search/docs/crawling-indexing/javascript/dynamic-rendering) "a workaround and not a long-term solution" and recommends SSR or static rendering instead. It isn't cloaking as long as bots and humans get similar content, but it doubles your infrastructure and fixes nothing for real users' INP. Full story: [Dynamic Rendering](/learn-seo/vue/spa/dynamic-rendering).

## What Search Engines See

Test your SPA to understand what crawlers see:

**Chrome DevTools**: disable JavaScript in DevTools settings and reload your page. This is what non-Google crawlers, and social bots, see.

**View Page Source**: right-click, then View Page Source. This is the initial HTML Google receives before JavaScript runs.

**Google Search Console**: the [URL Inspection tool](https://search.google.com/search-console) shows how Google rendered your page. Compare "crawled page" against "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 and AI crawlers struggle with your site.

## Meta Tags in SPAs

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

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

**Speed**: Google uses meta tags from the 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

```vue [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:

```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)

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

```html
<!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

SPAs change pages without reloading the browser, which creates two problems:

**URL fragments are ignored**: a URL like `yoursite.com#/about` looks like `yoursite.com` to search engines. Google ignores the `#/about` part for indexing.

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

The [History API](https://developers.google.com/search/docs/crawling-indexing/javascript/javascript-seo-basics) fixes the URL problem; SSR or prerendering fixes the content problem. Together they mean each route renders its own HTML and Google sees different content per URL.

## URL Structure

Use Vue Router with History mode, not hash mode:

```ts [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 website paths.

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

Even with SSR, generate a sitemap.

List static routes in `public/sitemap.xml`:

```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>
```

For dynamic routes, generate the sitemap from your data source during build or on demand. See the [sitemaps guide](/learn-seo/vue/controlling-crawlers/sitemaps) for details.

## 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): why Google calls it a workaround, not a fix

If you use Nuxt, it handles most of this automatically: SSR by default, automatic sitemap generation, and proper meta tag handling. [Learn more about SSR and SEO in Nuxt →](/learn-seo/nuxt/routes-and-rendering/rendering)
