Guide Vue SEO

Vue 3 defaults to rendu côté client, so le contenu isn't in the raw HTML. Ce is how to faire Vue apps crawlable and indexable — router mode, @unhead/vue, prerendering with vite-ssg, SSR/SSG with Nuxt, hydration, and Core Web Vitals.

Première publication : 26 juin 2026 · Dernière mise à jour : 3 août 2026 · Advanced
Langues

Vue 3 defaults to rendu côté client, so a plain Vite + Vue app ships an vide HTML shell and builds lune page in le navigateur. Google peut render it — but rendering is delayed and async données peut be missed, and Bing and social scrapers souvent can't run the JS at tout. The fixes: utiliser Vue Router history mode (pas hash mode), manage <head> with @unhead/vue, and put content in the HTML with prerendering (vite-ssg), SSR, or SSG via Nuxt. Dynamic rendering is deprecated — don't construire on it.

TL;DR — Vue 3 is CSR by par défaut, so le contenu isn’t in the raw HTML — Google renders it plus tard via its evergreen Chromium WRS, but the render is queued and async données récupéré in onMounted peut be missed, and Bing/social scrapers souvent don’t run the JS at tout. Non-negotiables: Vue Router createWebHistory() (hash mode has “a bad impact in SEO”), <head> via @unhead/vue / useSeoMeta(), and a rendering strategy que obtient content into HTML — vite-ssg prerendering pour content-stable sites, Nuxt pour complet SSR/SSG. Dynamic rendering is deprecated; prerender-spa-plugin is legacy. Watch hydration mismatches (a server/client content delta is an SEO problem, pas simplement a perf un) and the CSR bundle’s hit on LCP. Pour the framework-agnostic version of tout of ce, voir JavaScript SEO.

The par défaut Vite + Vue scaffold is client-side rendered

A plain npm create vue@latest app (Vite + Vue) ships a near-empty HTML shell and a JavaScript bundle. Le navigateur executes que bundle to construire the DOM. Ce is fine pour utilisateurs and a problem pour robots d’exploration: the raw HTML contient almost aucun content, and everything dépend on rendering.

That’s a property of the par défaut scaffold, pas a ceiling on Vue itself — Vue core aussi supports server rendering (createSSRApp) and static generation as first-class paths, and Vue’s propre SSR guide steers la plupart production SSR/SSG fonctionner toward Nuxt plutôt que a hand-rolled setup. So avant reaching pour a fix, vérifier ce que votre app is en réalité shipping: a bare Vite SPA with aucun rendering config is CSR; the même app behind Nuxt or vite-ssg isn’t.

Vue’s propre SSR guide is direct à propos de the upside of pas doing ce: with rendu côté serveur, “the moteur de recherche robots d’exploration va directement voir the entièrement rendered page.” The flip side is ce que vous ship by par défaut — content que seulement exists après JavaScript runs. Evidence for this claim Vue server-side rendering sends rendered HTML so crawlers can directly see page content. Scope: Vue SSR benefits; does not guarantee indexing. Confidence: high · Verified: Vue: SSR guide

(Vue 2 reached fin of life in December 2023; everything ici is Vue 3.)

How Googlebot handles a Vue app

Google processes JavaScript in phases — explorer the raw HTML, alors render it in an evergreen, headless Chromium instance (the Web Rendering Service), alors index the rendered result. Two choses follow from que:

  • Rendering is queued and delayed. Une page sits in the render queue — usually seconds to minutes, but it peut spike. Content que exists seulement après JS runs is pas immédiatement indexable; content in the raw HTML is.
  • Async données is the réel risk. Synchronous JavaScript renders reliably. Données récupéré from an API après mount n’est pas guaranteed to be caught. Vue’s SSR guide dit it plainly: “Si votre app starts with a chargement spinner, alors récupère content via Ajax, the robot d’exploration ne va pas wait pour vous to finish. Ce signifie si vous have content récupéré asynchronously on pages où SEO is important, SSR pourrait be necessary.” Practically: données récupéré in onMounted is client-only and invisible to robots d’exploration que don’t render — and at risk même with the ones que do.

John Mueller has décrit the échec mode pour SPA-style setups où the static HTML is mostly identical and tout the unique content dépend on JavaScript: si que JS can’t be executed correctement, lune pages fin up looking the même to Google, and the focus stays on the boilerplate HTML plutôt que the JS-loaded content. The lesson is the même as the broader JavaScript SEO page: obtenir le contenu into the DOM, fast and reliably.

Bing and social bots are worse at ce que Google. Bing has aucun rendering pipeline comparable to Google’s, and social preview scrapers (X, Slack, iMessage) généralement don’t run JavaScript at tout — so a bare CSR Vue app produces vide share previews. Si Bing trafic or social sharing matters, prerendering or SSR isn’t optional.

Vue Router: utiliser createWebHistory(), pas hash mode

Vue Router has two history modes. Hash mode (createWebHashHistory()) produces URLs comme example.com/#/about; Vue Router’s docs dire it “fait cependant have a bad impact in SEO” parce que everything après the # is a fragment le serveur ignores. Evidence for this claim Vue Router hash history uses a URL fragment that is not sent to the server and is discouraged for SEO. Scope: Vue Router hash history. Confidence: high · Verified: Vue Router: History modes Utiliser HTML5 history mode:

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

const router = createRouter({
  history: createWebHistory(),
  routes: [/* ... */],
})

History mode donne clean URLs (example.com/about) but exige a server fallback — quelconque direct requête to a route has to serve index.html so Vue peut prendre over, sinon direct hits 404. Configurer que catch-all on votre host (Nginx try_files, an SPA rewrite on Netlify/Vercel/Cloudflare, etc.).

Scope que rewrite carefully — a catch-all that’s aussi broad aussi sert index.html pour manquant static assets, réel API routes, or pages vous en réalité vouloir to 404. Que silently undoes the soft-404 guidance ci-dessous and peut hide a broken lien behind une page que semble comme it “worked.”

Pour genuine not-found states, éviter soft-404s — a client-side “not found” view que encore renvoie 200 peut obtenir indexé as an vide shell. Google’s guidance is to soit redirection to une URL que renvoie a réel 404 status or ajouter a <meta name="robots" content="noindex"> to error pages, and to utiliser the History API pour routing entre views.

Head and meta management: @unhead/vue

By par défaut every Vue route shares un <title> and un meta description. The tag history ici matters parce que the old réponses are dead ends:

  • vue-meta — the Nuxt 2-era library. Legacy.
  • @vueuse/head — its successor, now sunset.
  • @unhead/vue — the current community standard, and ce que vous devez utiliser pour a non-Nuxt Vue 3 app.

The ergonomic entry point is the useSeoMeta() composable — type-safe, XSS-safe, and aware of 100+ meta tags notamment Ouvrir Graph and Twitter Card:

import { useSeoMeta } from '@unhead/vue'

useSeoMeta({
  title: 'Vue SEO Guide',
  description: 'How to make a Vue 3 app crawlable and indexable.',
  ogTitle: 'Vue SEO Guide',
  ogDescription: 'How to make a Vue 3 app crawlable and indexable.',
  twitterCard: 'summary_large_image',
})

Meta peut be reactive — réussir a getter or computed valeur and the tags mettre à jour quand votre données fait. Pour balise canonicals, the safest pattern is encore HTML, pas JS: Google’s guidance is que “the best way to set the canonical URL is to use HTML,” and si vous doit inject it with JavaScript, toujours définir it to the même valeur the HTML voudrait. (I testé JS canonicals at Ahrefs and trouvé Google fait respect les — it même led Google to ajouter an exception to its docs — but HTML is encore the lower-risk chemin.)

Pick a rendering strategy

Ce is the decision que en réalité moves the needle. Même menu as the framework- agnostic JavaScript SEO page, applied to Vue.

Rendu côté client (CSR) — the par défaut, and the risky un. Acceptable pour app-like pages behind a login, dashboards, and anything vous don’t besoin indexé. Pas acceptable pour content que doit rank or be shared.

Prerendering with vite-ssg. Pour content-stable Vue 3 SPAs, vite-ssg generates static HTML at construire temps — swap votre construire script from vite build to vite-ssg build. It ships @unhead/vue construit in. Great pour marketing sites, docs, and blogs; pas a substitute pour SSR on highly dynamic, per-request pages. Remarque que the old webpack-era prerender-spa-plugin is legacy and effectively unmaintainedvite-ssg (or Nuxt) is the modern replacement.

Rendu côté serveur. Vue’s official guide covers a manual SSR setup with @vue/server-renderer, but it explicitly steers la plupart projects to a meta-framework plutôt que rolling votre propre — and pour simple cas it même dit “si you’re seulement investigating SSR to améliorer the SEO of a handful of marketing pages … alors vous probably vouloir SSG au lieu de SSR.” Si vous do roll manual SSR, créer the app, router, and store fresh per requête: on a long-running Node server, module-level singletons obtenir reused à travers requêtes, and mutating shared state with un user’s données peut leak it into un autre user’s réponse. That’s un plus raison Vue’s docs point la plupart projects at Nuxt, qui handles per-request isolation pour vous.

Evidence for this claim Module-scope singleton state in Vue SSR can leak user-specific data across requests; create application, router and store instances per request. Scope: SSR and hydration Confidence: high · Verified: Server-Side Rendering

Nuxt — the recommended complet solution. Nuxt donne vous SSR and SSG out of the box, built-in useSeoMeta() and head management, and the @nuxtjs/seo module pour sitemaps, robots.txt, and données structurées. Pour la plupart Vue projects où SEO matters, ce is the chemin Vue’s propre docs point to. (Nuxt has its propre depth — covered separately; treat ce as the cross-link, pas the deep dive.)

Dynamic rendering — don’t. Serving prerendered HTML to bots and the SPA to utilisateurs was toujours a workaround, and Google deprecated it in 2024, removing the implementation docs. Construire on SSR or SSG à la place.

Pour documentation specifically, VitePress is the Vue-native static site generator. Every page ships as plain HTML — aucun JS rendering barrier — and SEO is configuré via frontmatter (title, description, head) or the transformHead construire hook pour canonical/dynamic tags.

Hydration mismatches are an SEO problem, pas simplement a perf un

Quand vous do SSR/SSG, the client mounts with createSSRApp() — pas the plain createApp() — and hydrates le serveur-rendered HTML au lieu de rebuilding it from scratch. Si le serveur HTML and the client render diverge, Vue discards and re-renders the mismatched nodes. In SEO terms, que signifie le contenu Google indexé from le serveur render peut differ from ce que the utilisateur sees après hydration — a quality-signal inconsistency, pas simplement a flicker. Courant causes: invalid HTML nesting, random valeurs in templates, and date/temps discrepancies entre server and client. Vue 3,5+ adds data-allow-mismatch to suppress the mismatch warning selectively où a difference is intentional — it quiets the console, it doesn’t faire le serveur and client output equivalent, so don’t reach pour it simplement to silence a mismatch vous haven’t en réalité diagnosed.

Core Web Vitals are a Vue-specific concern

Par défaut CSR Vue ships a grand JS bundle que has to download, parse, and execute avant the largest content paints — so LCP suffers si vous ne faites pashing. Levers:

  • Code splitting (Vite’s par défaut) and lazy-loaded routes so vous don’t ship the whole app up front.
  • fetchpriority="high" on the LCP hero image.
  • Vapor Mode — an opt-in compiler mode que bypasses the virtual DOM pour eligible components, qui peut cut hydration cost on SSR pages. It’s Vue 3,6, pas 3,5 — encore experimental (beta/release-candidate as of mid-2026, pas yet in the stable 3,5.x line) — worth watching, pas something to depend on in production today.

Données structurées in Vue

Google executes JavaScript avant reading données structurées, so injecting JSON-LD fonctionne — @unhead/vue’s useHead() with a script of type application/ld+json is the reliable pattern. Parce que it dépend on rendering, confirmer it with Inspection d’URL plutôt que assuming Google saw it.

Où ce fits

Vue SEO is un instance of the general JavaScript SEO problem — parity, interaction, state, timing — and it shares almost everything with the CMS headless situation, où the rendering mode of the frontend decides the outcome.

Un caveat que s’applique to every option ci-dessus: SSR and SSG obtenir votre content into the HTML, but neither guarantees indexation, rankings, Core Web Vitals, or que hydration va match le serveur output exactly. Ils supprimer the JavaScript-rendering barrier — the rest of SEO (and correctness) is encore on vous.

Si vous remember un chose: obtenir votre content into the HTML. Everything sinon is detail.

Add an expert note

Pin an expert quote

New person? Create their unclaimed profile at /admin/experts/ → Pin a quote first.