Skip to content
ExponaBLOG
VUE.JS & NUXT · DAY 093 / 100

SEO checklist for Vue SPAs: a practical Vue & Nuxt guide

When you cannot use SSR.

Expona Admin7 min read

Building search visibility when server-side rendering isn't an option

Single-page applications built with Vue have become the standard for interactive user interfaces, but they come with a well-known trade-off: search engines struggle to index JavaScript-rendered content. If you're running a Vue SPA without server-side rendering, you're likely leaving performance and discoverability on the table. This guide walks through the SEO checklist for Vue SPAs: a practical Vue & Nuxt guide that bridges the gap between dynamic interfaces and search engine visibility, giving you concrete strategies that work in production.

We've encountered this problem across numerous projects—from e-commerce platforms that needed category pages indexed to SaaS dashboards with public-facing content. The reality is that not every team can (or should) move to full SSR immediately, but that doesn't mean accepting poor SEO. Let's cover the practical approaches we've found most effective.

Prerender key pages: static HTML for critical content

Prerendering generates static HTML files at build time for routes you know matter for search. Unlike SSR, which renders on every request, prerendering is a one-time process that transforms your SPA routes into static assets. This means search engines see fully rendered HTML without JavaScript execution delays.

The right candidate pages for prerendering are those with high search intent and stable content: landing pages, product category pages, pricing pages, blog post archives, and any public-facing documentation. If the content changes frequently based on user session or real-time data, prerendering isn't the fit—but for structural content, it's powerful.

We typically use @prerenderer/prerenderer with puppeteer for Vue projects. Here's a practical setup:

typescript
// prerender-routes.js
module.exports = [
  '/',
  '/about',
  '/pricing',
  '/contact',
  '/products/category-1',
  '/products/category-2',
  '/blog',
  '/blog/getting-started-with-vue'
];

// vue.config.js (or vite.config.js)
import PrerenderSPAPlugin from 'prerender-spa-plugin';
import path from 'path';

export default {
  configureWebpack: {
    plugins: [
      new PrerenderSPAPlugin({
        staticDir: path.join(__dirname, 'dist'),
        routes: require('./prerender-routes.js'),
        renderer: new PrerenderSPAPlugin.PuppeteerRenderer({
          renderAfterDocumentEvent: 'spa-render-complete'
        })
      })
    ]
  }
};

// main.js
app.mount('#app');
document.dispatchEvent(new Event('spa-render-complete'));

This approach generates static files in your dist folder. When a search engine bot hits /pricing, it gets pre-rendered HTML with full content, metadata, and links—no JavaScript required. The bot doesn't wait for Vue to hydrate; it sees the complete page immediately.

For dynamic routes (like product pages), you need to generate them programmatically. Build a list of all product IDs at compile time and pass them to the prerenderer.

Unique titles and meta tags with @unhead/vue

Each page needs a unique, descriptive title and meta description. Generic titles across your SPA kill your click-through rate in search results. @unhead/vue (formerly VueUse Head) is the modern standard for managing head tags in Vue 3 applications, and it works seamlessly with prerendered content.

Install and configure it:

typescript
// main.js
import { createHead } from '@unhead/vue';
import { defineNuxtConfig } from 'nuxt';

const app = createApp(App);
const head = createHead();
app.use(head);
app.mount('#app');

In your component, use the composable to set page-specific metadata:

typescript
// components/ProductCategoryPage.vue
<script setup lang="ts">
import { useHead } from '@unhead/vue';

const route = useRoute();
const category = ref('Electronics');

useHead({
  title: `${category.value} | Shop — Premium Products`,
  meta: [
    {
      name: 'description',
      content: `Browse our curated selection of ${category.value}. Fast shipping, 30-day returns, expert support.`
    },
    {
      property: 'og:title',
      content: `${category.value} | Shop`
    },
    {
      property: 'og:description',
      content: `Discover top-quality ${category.value} products.`
    },
    {
      property: 'og:type',
      content: 'product.group'
    },
    {
      name: 'robots',
      content: 'index, follow'
    }
  ]
});
</script>

This sets titles, descriptions, Open Graph tags for social sharing, and robot directives per page. Search engines now see unique, relevant metadata for each route. When combined with prerendering, the title and description tags end up in your static HTML—no dynamic lookup needed.

For product pages with dynamic IDs, build a component that fetches metadata from your API and applies it before the page renders. Use a route guard or lifecycle hook to ensure the head is set before the prerenderer captures the page.

Sitemap and robots.txt: guide search engines

A sitemap tells search engines which pages to crawl and how frequently they change. robots.txt sets crawl rules. Together, they dramatically improve indexation speed and ensure crawlers don't waste time on low-value pages.

For a Vue SPA with prerendered pages, generate a static sitemap at build time:

typescript
// build-sitemap.js
import { writeFileSync } from 'fs';
import { resolve } from 'path';

const BASE_URL = 'https://yourdomain.com';
const ROUTES = [
  { path: '/', changefreq: 'weekly', priority: 1.0 },
  { path: '/about', changefreq: 'monthly', priority: 0.7 },
  { path: '/pricing', changefreq: 'monthly', priority: 0.9 },
  { path: '/blog', changefreq: 'daily', priority: 0.8 },
  { path: '/blog/getting-started', changefreq: 'never', priority: 0.6 },
  { path: '/contact', changefreq: 'yearly', priority: 0.5 }
];

const generateSitemap = () => {
  const urls = ROUTES.map(route => {
    const lastmod = new Date().toISOString().split('T')[0];
    return `
    <url>
      <loc>${BASE_URL}${route.path}</loc>
      <lastmod>${lastmod}</lastmod>
      <changefreq>${route.changefreq}</changefreq>
      <priority>${route.priority}</priority>
    </url>`;
  }).join('');

  const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  ${urls}
</urlset>`;

  writeFileSync(resolve(__dirname, 'dist/sitemap.xml'), sitemap);
};

generateSitemap();
console.log('✓ Sitemap generated');

Add this to your build script in package.json:

typescript
"build": "vite build && node build-sitemap.js"

Then create robots.txt in your public folder:

typescript
User-agent: *
Allow: /
Disallow: /admin
Disallow: /private

Sitemap: https://yourdomain.com/sitemap.xml

Search engines now have explicit directions: here's every page we want indexed, here's where to find the complete list. This is especially useful for SPAs because crawlers can optimize their budget—they won't waste time on hash-based navigation or client-side redirects.

Common mistakes to avoid

  • Prerendering every route indiscriminately. If you have 50,000 product pages, prerendering becomes a build bottleneck. Prerender only your most important pages (top categories, homepage, docs) and let dynamic pages use other SEO tactics.
  • Forgetting to update titles after client-side navigation. @unhead/vue works, but only if you call it in every component. If you have a route-based layout that doesn't update the head, search engines won't see unique titles for each page.
  • Ignoring Core Web Vitals. Good SEO isn't just markup. If your SPA takes 4 seconds to become interactive, search engines rank it lower regardless of meta tags. Profile your Vue app with Lighthouse and fix slow routes.
  • No canonical tags. If your SPA can be accessed via multiple URLs (e.g., with and without trailing slash), add a canonical tag pointing to your preferred version.
  • Prerendering before your API is ready. If your prerenderer tries to fetch data from an API that's down, your build fails. Mock or retry gracefully.

Consider moving to Nuxt for SEO-heavy projects

If SEO is core to your business (SaaS with public documentation, e-commerce, content-heavy sites), Nuxt is worth the migration. Nuxt is built on Vue but adds server-side rendering, static generation, and SEO best practices out of the box.

Nuxt's nuxt.config.ts lets you define head metadata globally and per-route, with automatic sitemap generation via @nuxtjs/sitemap. You get SSR for free, which means search engines see fully rendered pages instantly—no prerendering dance required.

Here's a minimal Nuxt setup for comparison:

typescript
// nuxt.config.ts
export default defineNuxtConfig({
  app: {
    head: {
      title: 'My SaaS',
      meta: [
        { name: 'description', content: 'Best SaaS for teams' }
      ],
      link: [
        { rel: 'icon', href: '/favicon.ico' }
      ]
    }
  },
  modules: ['@nuxtjs/sitemap'],
  sitemap: {
    urls: async () => {
      return [
        '/',
        '/pricing',
        '/docs'
      ];
    }
  }
});

// pages/pricing.vue
<script setup lang="ts">
definePageMeta({
  title: 'Pricing Plans | My SaaS',
  description: 'Choose a plan that scales with your team.'
});
</script>

Nuxt handles prerendering, SSR, and sitemap generation without manual orchestration. If you're starting a new project or maintaining an existing Vue SPA where SEO is blocking growth, the investment to migrate is usually worth it.

Key takeaways

  • Prerender your key pages (homepage, docs, category pages) at build time to serve static HTML to search engines.
  • Use @unhead/vue to set unique titles and meta descriptions per route; this metadata appears in both prerendered HTML and client-side navigation.
  • Generate a sitemap and robots.txt during build; upload them to your public folder so crawlers find them immediately.
  • Monitor Core Web Vitals and fix rendering performance—fast SPAs rank higher than slow ones, even with perfect metadata.
  • For content-heavy or e-commerce projects, strongly consider migrating to Nuxt; the SEO infrastructure is native and eliminates fragile prerendering pipelines.
  • Test your setup with Google Search Console and Lighthouse; these tools reveal indexation issues and ranking opportunities you can't see from your dev machine.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles