Most advice on vue seo jumps straight to meta tags and sitemap plugins, and that’s exactly why so many Vue apps still get crawled like an empty shell. The uncomfortable truth is that Vue SEO is a rendering problem before it’s a plugin problem. By default, a Vue single-page app ships a near-empty <div id="app"> and builds every heading, paragraph and meta tag in the browser with JavaScript. Whether a crawler ever sees that content — and how reliably — is decided by where you render, not by which head library you install. Get the rendering model right and the rest is configuration. Get it wrong and no amount of structured data will save you.
Vue’s SEO Problem Starts With Rendering, Not Plugins
A stock Vue app built with Vite or the Vue CLI is client-side rendered (CSR). The server sends a skeleton HTML document plus a JavaScript bundle; the bundle then constructs the real page in the visitor’s browser. To a human on a fast connection this is invisible. To a crawler it means the meaningful HTML doesn’t exist until JavaScript runs — and not every consumer of your page runs JavaScript.
This is the single fact that reframes seo for vue: your content has two possible existences, one in the shipped HTML and one that only appears after hydration. Google can usually reach the second; many other important clients only ever see the first. Any strategy for a Vue app that doesn’t decide, deliberately, which content lives in the initial HTML is leaving indexing to chance.
Why Client-Side Rendering Hurts Vue SEO
Googlebot does render JavaScript — it uses an evergreen Chromium engine — but it does so in a deferred second wave, after the initial HTML crawl, and on a queue with no guaranteed timing. For a small, fast site that’s often fine. For a large or frequently changing one, it means new and updated content can sit unindexed for longer than a server-rendered equivalent, because rendering is a resource Google rations.
The bigger problem is everything that isn’t Googlebot. Social scrapers that build link previews (the ones behind Open Graph and Twitter cards), and the fast-growing set of AI crawlers that feed answer engines, frequently do not execute JavaScript at all. If your title, description and Open Graph tags are injected client-side, those clients see the placeholder in your skeleton HTML — a generic title and no description. That’s how a Vue app can rank acceptably in Google yet produce blank previews everywhere a link is shared and go uncited by AI systems. Solving vue js seo properly means getting the critical HTML — headings, body copy, and head tags — into the response the server sends, before any JavaScript runs.
CSR vs SSR vs SSG vs Hybrid: The Decision That Determines Everything
Four rendering models are available to a Vue project, and choosing between them is the highest-leverage decision in the whole exercise:
- CSR (client-side): the default SPA. Cheapest to host, worst for crawlability. Fine for logged-in app screens that shouldn’t rank anyway.
- SSR (server-side): the server renders full HTML per request, then hydrates. Best for content that changes constantly or is personalized — the crawler gets complete HTML immediately.
- SSG (static generation): pages are pre-rendered to HTML at build time and served as static files. Fastest and most robust for content that changes on a deploy cadence — blogs, docs, marketing pages.
- Hybrid: different rules per route — static for the blog, SSR for a live dashboard, on-demand regeneration for product pages. This is where most real sites end up.
A usable decision rule: if a page needs to rank and its content changes less often than users request it, pre-render it (SSG or cached SSR). If it must reflect per-request or per-user data and still needs to rank, use SSR. Only leave a page CSR when it genuinely doesn’t need organic visibility. Almost every Vue SEO failure I see traces back to skipping this decision and shipping the default SPA for pages that were always meant to be found in search.
How to Do SEO for a Plain Vue SPA (No Nuxt)
You don’t have to adopt a meta-framework to fix an existing Vue SPA, but your options narrow. The pragmatic path is static pre-rendering: a build step that boots a headless browser, visits each route, and saves the fully rendered HTML. Tools like vite-ssg (for Vite-based Vue apps) generate real HTML for a known list of routes at build time, giving crawlers complete markup without rewriting the app as SSR. It works well when your indexable URLs are enumerable and don’t change per request.
Two configuration details matter regardless of pre-rendering. First, use Vue Router in history mode (createWebHistory), not hash mode — URLs with a # fragment are a poor foundation for indexing, and history mode needs your host to serve index.html for unknown paths. Second, if you cannot pre-render, at minimum inject accurate head tags with Unhead (@unhead/vue, the successor to @vueuse/head and the old vue-meta) so the client-rendered path is correct — but understand this still leaves non-JS crawlers seeing the skeleton. Pre-rendering is what actually closes that gap.
Why Nuxt Is the Default Answer for Vue SEO
For most content-driven Vue projects, nuxt seo is simply easier than assembling the pieces yourself. Nuxt is the meta-framework for Vue, and it renders universally (SSR) out of the box on its Nitro server engine, so every page arrives as complete HTML. Its real power for SEO is granular control via routeRules in nuxt.config.ts: you can mark the blog as prerender: true, put ISR-style caching on product pages, keep an account area ssr: false, and run a live section as full SSR — all in one app. Running nuxt generate pre-renders the whole site to static files when you want SSG.
That per-route flexibility is why the hybrid model is realistic in Nuxt rather than aspirational. You match the rendering strategy to each page’s search intent and freshness without maintaining separate builds — the core of good Nuxt SEO.
Managing Meta Tags: useHead and useSeoMeta
Nuxt exposes two composables that handle the head correctly for both SSR and hydration. useHead() sets arbitrary tags — title, link, script, base. useSeoMeta() is the one to reach for most of the time: it gives fully typed helpers for every meta property (title, description, ogTitle, ogImage, twitterCard and the rest) so you can’t misspell a property name. The important part for SEO is that both run on the server, so the tags are present in the initial HTML — the exact thing CSR gets wrong.
For pages built from data, fetch first and derive the head from it. Pull the record with useAsyncData or useFetch, then feed computed values into useSeoMeta so each article or product renders its own title, description and canonical on the server. That single pattern — data-driven, server-rendered head tags — resolves the majority of duplicate-title and missing-description issues that plague Vue apps.
The Nuxt SEO Module: Sitemap, Robots, Schema and OG Images
The @nuxtjs/seo package bundles the infrastructure most sites hand-roll badly. Point it at your site URL and it wires up a dynamic /sitemap.xml (with multi-sitemap support), a managed /robots.txt with per-route indexing control, Schema.org helpers for rich results, dynamic Open Graph image generation, canonical-URL and Open Graph utilities, and a link checker that catches broken internal links before they leak crawl budget. It even generates llms.txt and Markdown endpoints aimed at AI crawlers.
Structured data is worth calling out. Use the Schema.org helpers to describe articles, products, breadcrumbs and organization details in the server-rendered HTML — this is what makes a page eligible for rich results and gives answer engines clean, machine-readable facts. Because it’s emitted server-side, it’s visible to the non-JS crawlers that client-injected JSON-LD would miss.
The Soft-404 and Redirect Traps That Quietly Kill Rankings
A subtle failure mode: SPAs return HTTP 200 for every URL, including ones that don’t exist, because routing happens in the browser. Google reads that as a “soft 404” — a not-found page reporting success — and it erodes trust in your URL space. In Nuxt, throw a real not-found with createError({ statusCode: 404 }) (or set the status with setResponseStatus) so the server sends a genuine 404. Likewise, handle permanent moves as server 301 redirects via routeRules, not client-side JavaScript redirects that crawlers treat as soft, unreliable signals.
Two more quiet killers: unmanaged query-parameter URLs that spawn duplicate content (control them with canonicals and robots rules), and a missing or default siteUrl that leaves canonicals pointing at the wrong host. These aren’t exotic — they’re the specific technical issues a real crawler-based audit surfaces on Vue and Nuxt sites again and again.
Core Web Vitals: Hydration Is the Vue-Specific Cost
Server rendering solves visibility but introduces a performance tax unique to SPAs: hydration. After the server-rendered HTML paints, the browser downloads the JavaScript bundle and re-attaches interactivity across the whole page, which can delay interaction readiness and hurt Interaction to Next Paint. Keep the bundle lean, code-split by route, and use <NuxtLink> for smart prefetching. Where a page is mostly static, delayed or lazy hydration (and Nuxt’s server components / islands) let you ship far less JavaScript, protecting Largest Contentful Paint. Core Web Vitals are a ranking factor and a real user-experience signal, so treat hydration cost as part of your vue seo budget, not an afterthought.
Fitting an SEO Workflow Around a Vue or Nuxt Build
Rendering gets your Vue app crawlable; it doesn’t tell you what to build or whether it’s ranking. That’s a separate layer, and it’s where SEO Rocket fits — a platform-agnostic SEO system, not a page builder, that sits alongside whatever stack you ship. Its real-crawler site audit renders pages the way a search engine would and flags exactly the failures above: content that only appears after JavaScript, soft 404s, missing canonicals, orphaned routes. Its AI keyword research runs on real Ahrefs data so you’re targeting queries with genuine demand, and the validation-gated AI writer drafts the article or landing content — enforcing a length floor, title and meta limits, and a repair loop — so you can produce indexable pages at the pace a component-driven site makes possible.
From there, rank tracking and AI-visibility tracking on a client dashboard tell you whether the rendering work paid off in positions and citations, not just in a green Lighthouse score. It’s built on a playbook proven across 1,000,000+ ranking pages, and it runs around $50/month with a free tier — the point being to close the loop between “the page renders correctly” and “the page actually ranks,” which technical work alone never confirms.
Frequently Asked Questions
Can a Vue single-page app rank in Google without SSR?
Yes, a Vue SPA can rank — Googlebot renders JavaScript and will eventually see client-rendered content. But rendering is deferred and rationed, so indexing is slower and less reliable at scale, and non-Google clients (social preview scrapers, AI crawlers) often see only the empty skeleton. For anything that needs dependable organic visibility, pre-render (SSG) or server-render (SSR) instead of relying on client rendering.
Do I need Nuxt to make a Vue app rank?
Not strictly. A plain Vue SPA can be pre-rendered with a tool like vite-ssg, which gives crawlers real HTML for a known set of routes. But Nuxt bundles SSR, per-route rendering rules, server-side head management and the @nuxtjs/seo infrastructure (sitemap, robots, schema, OG images) with far less assembly — which is why it’s the default recommendation for content-driven Vue projects.
Why do my Vue page’s meta tags and link previews come up blank when shared?
Because the tags are being injected client-side. Social and AI scrapers frequently don’t run JavaScript, so they read the placeholder title and empty description in your shipped HTML. The fix is to render head tags on the server — with Nuxt’s useSeoMeta, or via pre-rendering in a plain SPA — so the correct title, description and Open Graph image exist in the initial response.