Index of every page on this site: llms.txt

SPA SEO for Vue: When You Actually Need SSR

Choose client rendering, SSR, or prerendering for a Vue app. Check initial HTML, crawlable routes, metadata, and interaction behavior.
Harlan Wilton12 mins read Published Updated
Looking for a framework-agnostic guide? Check out our Complete Guide to Single Page Application SEO for React, Vue, and Angular.
What you'll learn
  • Search engines need to execute your JavaScript before they see SPA content, if that content is absent from initial HTML. Google can render JavaScript, subject to resource and rendering availability.
  • Heavy mounting or hydration work can delay interactions. Measure INP rather than assuming the rendering mode predicts it.
  • SSR or prerendering can provide public content and metadata in initial HTML. Private app views may suit client rendering.
  • Crawler capabilities differ. Do not assume a provider can read content that exists only after browser JavaScript runs.

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 client-generated content appears. Static content in the response may appear earlier; measure loading and interaction behavior separately.

Why SPAs Struggle

Indexing waits on rendering

Google crawls, renders, and indexes pages. Client-generated content depends on successful JavaScript rendering. SSR and prerendering can put that content in the response itself, but all eligible HTTP 200 pages can still enter Google's rendering queue. Neither route guarantees indexing or its timing.

AI crawlers see even less

AI products use different fetching, search, and user-requested retrieval systems. Their crawler documentation does not establish one shared JavaScript rendering capability. Publish important public content in initial HTML when you need clients that read only that response to access it. Google Search's JavaScript pipeline does not prove how every Gemini or other AI request retrieves content.

Hydration blocks interaction

Client rendering mounts an app. Hydration instead attaches a client app to server-rendered markup. Both can perform main-thread work. Long tasks can delay input handling, but network downloads do not themselves lock the main thread. INP measures responsiveness; its good threshold is 200ms or less. No rendering mode guarantees that result.

When SPAs Break SEO

Metadata depends on JavaScript: tags added by component code appear after the client app runs. Static tags can still exist in the initial document. For previews that read only that response, provide route-specific sharing metadata through SSR, prerendering, or server-provided HTML.

Soft 404s: an app-shell fallback can return HTTP 200 for a missing URL. A browser-rendered "Not Found" view does not change that response status. Configure missing-page handling and check the status over HTTP.

Google recommends two options for missing SPA pages. One option redirects to a URL your server answers with a real 404. Verify that host behavior before using this browser-only error-handler excerpt:

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

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

import { useHead } from '@unhead/vue'

// Error component setup excerpt, after installing the head plugin
useHead({
  title: 'Page Not Found',
  meta: [
    { name: 'robots', content: 'noindex' }
  ]
})

Client-side routing hides pages: Google discovers pages by following real <a href> links, not JavaScript click handlers or router.push() calls. Use anchors with resolvable hrefs for navigation that crawlers must discover.

When SPA Works Fine

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

Your app requires authentication: admin panels, dashboards, internal tools. Enforce access control for private data. Meta robots tags control indexing preferences, not authentication.

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: decide whether a public preview should describe the app without exposing private content.

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. Google can index a successfully rendered SPA. SSR or prerendering reduces dependence on client JavaScript for the content in the response.

Social sharing matters: provide preview metadata in the HTML response for providers that read it without executing the app.

Performance is critical: compare actual response time, paint, JavaScript work, and interactions. Server rendering can expose content earlier but still needs hydration for app interactions.

You want predictable indexing: SSR or prerendering makes generated content available without executing client JavaScript. Crawl access, server errors, content quality, and indexing decisions still matter.

Choose by public content, freshness, request-specific data, deployment limits, and measured interaction costs.

Solutions Overview

Server-Side Rendering (SSR)

Renders page HTML on the server, possibly with caching. Inspect the response for expected content and metadata.

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

Tools: Nuxt (recommended), Quasar SSR, custom Vite SSR.

Trade-offs: requires a Node.js/Edge runtime and a more complex deployment. Once you add SSR, watch for hydration mismatches: mismatches can change content or interaction behavior and need debugging.

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.

Trade-offs: only works for routes you know at build time, and content updates require a rebuild. Full walkthrough: Prerendering Vue SPAs.

Dynamic Rendering: skip it

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

Why not: Google calls it "a workaround and not a long-term solution" and recommends SSR or static rendering instead. Google generally does not treat similar bot and human content as cloaking, but the separate crawler response path adds maintenance and does not improve the user-facing app by itself. Full story: 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 checks how your app behaves without JavaScript. It does not emulate a named crawler.

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 shows how Google rendered your page. Compare "crawled page" against "live page."

HTTP response: fetch each important URL and inspect its status, page content, and head tags. Compare this with rendered HTML rather than treating them as identical checks.

If content appears only after JavaScript runs, access depends on each client's rendering capability. If Google's rendered output is incomplete, inspect errors and blocked resources.

Meta Tags in SPAs

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

Preview clients: initial metadata supports clients that do not execute app JavaScript. Test each provider's actual preview.

Consistency: keep intended metadata consistent between the response and the rendered page.

Reliability: JavaScript execution can be blocked or delayed. Inspect initial HTML to verify those tags are present before rendering.

Wrong Approach

App.vue
<script setup lang="ts">
import { useSeoMeta } from '@unhead/vue'

// Client-rendered component excerpt, after installing the head plugin
useSeoMeta({
  title: 'My SPA Site',
  description: 'This description only exists client-side'
})
</script>

Initial HTML:

<!DOCTYPE html>
<html>
<head>
  <!-- Illustrative app shell without route-specific 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:

<!DOCTYPE html>
<html>
<head>
  <title>My SPA Site</title>
  <meta name="description" content="This description exists immediately">
</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.

Shared app shell: a client-rendered deployment can serve the same shell for several routes. Google needs successful rendering to obtain their client-generated content. Static metadata can still be present in that shell.

The History API 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 distinct path URLs for public pages. These are Vue Router option excerpts, not complete route declarations:

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

// Hash routes are not distinct server request paths
createRouter({
  history: createWebHashHistory(),
  routes: [/* ... */]
})

// History routes use distinct paths; configure the host or SSR router
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 to handle direct URL access. For a client-only app, valid client routes need the app shell. Missing routes need separate handling. SSR and generated static pages require their own routing rules.

Sitemaps for SPAs

A sitemap can help discover canonical public URLs, particularly when link discovery is incomplete.

List canonical public routes in public/sitemap.xml. These are illustrative dates; supply lastmod values only when they reflect a significant page change:

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 for details.

JavaScript SEO Resources

Google's official guidance on JavaScript and SEO:

Nuxt enables SSR by default and supplies routing and metadata APIs. Install and configure sitemap modules separately, then verify the generated output. Learn more about SSR and SEO in Nuxt →

Was this page helpful?