Vue SEO
Vue 3 defaults to client-side rendering, so the content isn't in the raw HTML. This is how to make Vue apps crawlable and indexable — router mode, @unhead/vue, prerendering with vite-ssg, SSR/SSG with Nuxt, hydration, and Core Web Vitals.
Vue 3 defaults to client-side rendering, so a plain Vite + Vue app ships an empty HTML shell and builds the page in the browser. Google can render it — but rendering is delayed and async data can be missed, and Bing and social scrapers often can't run the JS at all. The fixes: use Vue Router history mode (not 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 build on it.
TL;DR — Vue builds your page in the browser by default. That means the raw HTML a search engine first downloads is nearly empty — the real content only shows up after JavaScript runs. Google can usually handle this, but Bing and the bots that make social-share previews often can’t. The fixes: use Vue Router’s “history” mode for clean URLs, add a small library called
@unhead/vuefor your titles and descriptions, and for pages that need to rank, build the HTML ahead of time (prerendering or Nuxt) instead of only in the browser.
Why Vue needs special SEO attention
When you build a site with Vue 3 the normal way (Vite + Vue), the server sends a tiny HTML file — basically an empty container — and a big JavaScript bundle. The browser runs that JavaScript to draw the actual page. This is called client-side rendering (CSR). Evidence for this claim A Vue application can render its interface in the browser with client-side JavaScript. Scope: Vue client-side rendering architecture. Confidence: high · Verified: Vue: SSR guide
A human never notices. A search engine might. The first thing a crawler downloads is that nearly-empty container. It has to run your JavaScript to see anything — and not every crawler does that well.
- Google runs an up-to-date version of Chrome and can render your Vue app. But it does the rendering later, in a queue, and if your content waits on a slow data request, Google can move on before it loads.
- Bing, DuckDuckGo, and social-preview bots (the ones that make the little card when you paste a link into X, Slack, or iMessage) are much less reliable at running JavaScript. On a plain Vue app, your social previews can come up blank.
The three things to get right
-
Use “history” mode in Vue Router, not “hash” mode. Hash-mode URLs look like
example.com/#/about. Vue Router’s own docs say hash mode “has a bad impact in SEO.” UsecreateWebHistory()so your URLs look likeexample.com/about. Evidence for this claim Vue Router recommends HTML5 history mode for normal-looking URLs and warns that hash mode has a negative SEO effect. Scope: Vue Router history modes. Confidence: high · Verified: Vue Router: History modes -
Manage your titles and meta tags with
@unhead/vue. Out of the box, every Vue page shares the same title and description.@unhead/vuelets each page set its own — including the Open Graph and Twitter tags that drive social previews. -
Put your content in the HTML for pages that need to rank. Instead of building everything in the browser, build it ahead of time so the content is already there when a crawler arrives. The easy on-ramp is prerendering with a tool called
vite-ssg. For bigger or more dynamic sites, Nuxt (the official Vue framework for this) does it for you.
Should you just use Nuxt?
For most sites where SEO actually matters, yes — and Vue’s own documentation points
you there. Nuxt handles the rendering, the meta tags, sitemaps, and robots.txt for
you. If your site is fairly static (a marketing site, docs, a blog), vite-ssg is
a lighter option that doesn’t require switching frameworks.
Want the deeper version — how Google’s render queue works, the @unhead/vue code,
prerendering vs. SSR, hydration mismatches, and Core Web Vitals? Switch to the
Advanced tab.
TL;DR — Vue 3 is CSR by default, so the content isn’t in the raw HTML — Google renders it later via its evergreen Chromium WRS, but the render is queued and async data fetched in
onMountedcan be missed, and Bing/social scrapers often don’t run the JS at all. Non-negotiables: Vue RoutercreateWebHistory()(hash mode has “a bad impact in SEO”),<head>via@unhead/vue/useSeoMeta(), and a rendering strategy that gets content into HTML —vite-ssgprerendering for content-stable sites, Nuxt for full SSR/SSG. Dynamic rendering is deprecated;prerender-spa-pluginis legacy. Watch hydration mismatches (a server/client content delta is an SEO problem, not just a perf one) and the CSR bundle’s hit on LCP. For the framework-agnostic version of all of this, see JavaScript SEO.
The default 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. The browser executes that bundle to build the DOM. This is
fine for users and a problem for crawlers: the raw HTML contains almost no
content, and everything depends on rendering.
That’s a property of the default scaffold, not a ceiling on Vue itself — Vue core
also supports server rendering (createSSRApp) and static generation as
first-class paths, and Vue’s own SSR guide steers most production SSR/SSG work
toward Nuxt rather than a hand-rolled setup. So before reaching for a fix, check
what your app is actually shipping: a bare Vite SPA with no rendering config is
CSR; the same app behind Nuxt or vite-ssg isn’t.
Vue’s own SSR guide is direct about the upside of not doing this: with server-side rendering, “the search engine crawlers will directly see the fully rendered page.” The flip side is what you ship by default — content that only exists after 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 end of life in December 2023; everything here is Vue 3.)
How Googlebot handles a Vue app
Google processes JavaScript in phases — crawl the raw HTML, then render it in an evergreen, headless Chromium instance (the Web Rendering Service), then index the rendered result. Two things follow from that:
- Rendering is queued and delayed. A page sits in the render queue — usually seconds to minutes, but it can spike. Content that exists only after JS runs is not immediately indexable; content in the raw HTML is.
- Async data is the real risk. Synchronous JavaScript renders reliably. Data
fetched from an API after mount is not guaranteed to be caught. Vue’s SSR guide
says it plainly: “If your app starts with a loading spinner, then fetches content
via Ajax, the crawler will not wait for you to finish. This means if you have
content fetched asynchronously on pages where SEO is important, SSR might be
necessary.” Practically: data fetched in
onMountedis client-only and invisible to crawlers that don’t render — and at risk even with the ones that do.
John Mueller has described the failure mode for SPA-style setups where the static HTML is mostly identical and all the unique content depends on JavaScript: if that JS can’t be executed properly, the pages end up looking the same to Google, and the focus stays on the boilerplate HTML rather than the JS-loaded content. The lesson is the same as the broader JavaScript SEO page: get the content into the DOM, fast and reliably.
Bing and social bots are worse at this than Google. Bing has no rendering pipeline comparable to Google’s, and social preview scrapers (X, Slack, iMessage) generally don’t run JavaScript at all — so a bare CSR Vue app produces empty share previews. If Bing traffic or social sharing matters, prerendering or SSR isn’t optional.
Vue Router: use createWebHistory(), not hash mode
Vue Router has two history modes. Hash mode (createWebHashHistory()) produces
URLs like example.com/#/about; Vue Router’s docs say it “does however have a bad
impact in SEO” because everything after the # is a fragment the server 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
Use HTML5 history mode:
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(),
routes: [/* ... */],
})History mode gives clean URLs (example.com/about) but requires a server
fallback — any direct request to a route has to serve index.html so Vue can
take over, otherwise direct hits 404. Configure that catch-all on your host (Nginx
try_files, an SPA rewrite on Netlify/Vercel/Cloudflare, etc.).
Scope that rewrite carefully — a catch-all that’s too broad also serves
index.html for missing static assets, real API routes, or pages you actually
want to 404. That silently undoes the soft-404 guidance below and can hide a
broken link behind a page that looks like it “worked.”
For genuine not-found states, avoid soft-404s — a client-side “not found” view that
still returns 200 can get indexed as an empty shell. Google’s guidance is to
either redirect to a URL that returns a real 404 status or add a
<meta name="robots" content="noindex"> to error pages, and to use the History API
for routing between views.
Head and meta management: @unhead/vue
By default every Vue route shares one <title> and one meta description. The tag
history here matters because the old answers are dead ends:
vue-meta— the Nuxt 2-era library. Legacy.@vueuse/head— its successor, now sunset.@unhead/vue— the current community standard, and what you should use for a non-Nuxt Vue 3 app.
The ergonomic entry point is the useSeoMeta() composable — type-safe, XSS-safe,
and aware of 100+ meta tags including Open 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 can be reactive — pass a getter or computed value and the tags update when your data does. For canonical tags, the safest pattern is still HTML, not JS: Google’s guidance is that “the best way to set the canonical URL is to use HTML,” and if you must inject it with JavaScript, always set it to the same value the HTML would. (I tested JS canonicals at Ahrefs and found Google does respect them — it even led Google to add an exception to its docs — but HTML is still the lower-risk path.)
Pick a rendering strategy
This is the decision that actually moves the needle. Same menu as the framework- agnostic JavaScript SEO page, applied to Vue.
Client-side rendering (CSR) — the default, and the risky one. Acceptable for app-like pages behind a login, dashboards, and anything you don’t need indexed. Not acceptable for content that must rank or be shared.
Prerendering with vite-ssg. For content-stable Vue 3 SPAs, vite-ssg
generates static HTML at build time — swap your build script from vite build to
vite-ssg build. It ships @unhead/vue built in. Great for marketing sites, docs,
and blogs; not a substitute for SSR on highly dynamic, per-request pages. Note that
the old webpack-era prerender-spa-plugin is legacy and effectively
unmaintained — vite-ssg (or Nuxt) is the modern replacement.
Server-side rendering. Vue’s official guide covers a manual SSR setup with
@vue/server-renderer, but it explicitly steers most projects to a meta-framework
rather than rolling your own — and for simple cases it even says “if you’re only
investigating SSR to improve the SEO of a handful of marketing pages … then you
probably want SSG instead of SSR.” If you do roll manual SSR, create the app,
router, and store fresh per request: on a long-running Node server, module-level
singletons get reused across requests, and mutating shared state with one user’s
data can leak it into another user’s response. That’s one more reason Vue’s docs
point most projects at Nuxt, which handles per-request isolation for you.
Nuxt — the recommended full solution. Nuxt gives you SSR and SSG out of the
box, built-in useSeoMeta() and head management, and the @nuxtjs/seo module for
sitemaps, robots.txt, and structured data. For most Vue projects where SEO matters,
this is the path Vue’s own docs point to. (Nuxt has its own depth — covered
separately; treat this as the cross-link, not the deep dive.)
Dynamic rendering — don’t. Serving prerendered HTML to bots and the SPA to users was always a workaround, and Google deprecated it in 2024, removing the implementation docs. Build on SSR or SSG instead.
For documentation specifically, VitePress is the Vue-native static site
generator. Every page ships as plain HTML — no JS rendering barrier — and SEO is
configured via frontmatter (title, description, head) or the transformHead
build hook for canonical/dynamic tags.
Hydration mismatches are an SEO problem, not just a perf one
When you do SSR/SSG, the client mounts with createSSRApp() — not the plain
createApp() — and hydrates the server-rendered HTML instead of rebuilding it
from scratch. If the server HTML and the client render diverge, Vue discards and
re-renders the mismatched nodes. In SEO terms, that means the content Google indexed
from the server render can differ from what the user sees after hydration — a
quality-signal inconsistency, not just a flicker. Common causes: invalid HTML
nesting, random values in templates, and date/time discrepancies between server and
client. Vue 3.5+ adds data-allow-mismatch to suppress the mismatch warning
selectively where a difference is intentional — it quiets the console, it doesn’t
make the server and client output equivalent, so don’t reach for it just to silence
a mismatch you haven’t actually diagnosed.
Core Web Vitals are a Vue-specific concern
Default CSR Vue ships a large JS bundle that has to download, parse, and execute before the largest content paints — so LCP suffers if you do nothing. Levers:
- Code splitting (Vite’s default) and lazy-loaded routes so you don’t ship the whole app up front.
fetchpriority="high"on the LCP hero image.- Vapor Mode — an opt-in compiler mode that bypasses the virtual DOM for eligible components, which can cut hydration cost on SSR pages. It’s Vue 3.6, not 3.5 — still experimental (beta/release-candidate as of mid-2026, not yet in the stable 3.5.x line) — worth watching, not something to depend on in production today.
Structured data in Vue
Google executes JavaScript before reading structured data, so injecting JSON-LD
works — @unhead/vue’s useHead() with a script of type
application/ld+json is the reliable pattern. Because it depends on rendering,
confirm it with URL Inspection rather than assuming Google saw it.
Where this fits
Vue SEO is one instance of the general JavaScript SEO problem — parity, interaction, state, timing — and it shares almost everything with the headless CMS situation, where the rendering mode of the frontend decides the outcome.
One caveat that applies to every option above: SSR and SSG get your content into the HTML, but neither guarantees indexing, rankings, Core Web Vitals, or that hydration will match the server output exactly. They remove the JavaScript-rendering barrier — the rest of SEO (and correctness) is still on you.
If you remember one thing: get your content into the HTML. Everything else is detail.
AI summary
A condensed take on the Advanced version:
- The default Vite + Vue scaffold is CSR — a plain
npm create vue@latestapp ships a near-empty HTML shell and builds the DOM in the browser. That’s a property of the bare scaffold, not a ceiling on Vue itself — Vue core also supports SSR (createSSRApp) and SSG as first-class paths. - Google renders Vue, but with caveats — its evergreen Chromium WRS runs the JS,
but rendering is queued/delayed and async data (e.g. fetched in
onMounted) can be missed. Vue’s SSR docs: a crawler “will not wait for you to finish” an Ajax fetch behind a spinner. - Bing and social scrapers are worse — they often don’t run JS at all, so a bare CSR Vue app produces empty share previews.
- Router: use
createWebHistory(), not hash mode (hash has “a bad impact in SEO”). History mode needs a serverindex.htmlfallback — scoped so it doesn’t also swallow real static assets, API routes, or intended 404s. Avoid soft-404s. - Head/meta:
@unhead/vuewithuseSeoMeta()is the current standard;vue-metais legacy,@vueuse/headis sunset. Prefer HTML canonicals. - Rendering strategy decides it: CSR (risky, OK for app pages) →
vite-ssgprerendering (content-stable sites) → SSR / SSG via Nuxt (the recommended full solution).prerender-spa-pluginis legacy; dynamic rendering is deprecated. If you roll manual SSR, build the app/router/store fresh per request to avoid cross-request state leaks. - VitePress for docs sites (plain static HTML).
- Hydration uses
createSSRApp(); mismatches create a server/client content delta — an SEO quality issue, not just a flicker. Vue 3.5+‘sdata-allow-mismatchsuppresses the warning for intentional cases — it doesn’t make the two renders equivalent. - Core Web Vitals: big CSR bundle hurts LCP — code-split, lazy routes,
fetchpriority="high", and (experimental, Vue 3.6 beta/RC — not stable yet) Vapor Mode. - Structured data via
@unhead/vueuseHead()JSON-LD; verify with URL Inspection. - No option here guarantees indexing, rankings, CWV, or hydration parity — SSR/SSG remove the JS-rendering barrier; the rest of SEO is still on you.
Official documentation
Primary-source documentation from Vue and the search engines.
Vue
- Server-Side Rendering (SSR) | Vue.js — the SEO benefit of SSR, the async/spinner caveat, and the SSG-for-marketing-pages recommendation.
- Different History modes | Vue Router —
createWebHistory()vs. hash mode, the SEO note, and the server fallback requirement. - useSeoMeta() · Unhead — the current
@unhead/vuecomposable for type-safe meta management. - Site Config | VitePress and Frontmatter Config | VitePress — SEO configuration for Vue-powered docs sites.
- antfu-collective/vite-ssg | GitHub — build-time prerendering for Vue 3 SPAs.
- Understand the JavaScript SEO basics — two-phase processing, the History API recommendation, soft-404 handling, and HTML canonicals.
- Dynamic Rendering as a workaround — now flagged as deprecated; SSR/static rendering/hydration recommended instead.
- Introducing a new JavaScript SEO video series — Google’s official JS SEO series (includes the Vue.js episode).
Quotes from the source
On-the-record statements from Vue and Google. Each link is a deep link that jumps to the quoted passage on the source page.
Vue — SSR & SEO
- “Better SEO: the search engine crawlers will directly see the fully rendered page.” — Vue.js SSR guide. Jump to quote
- “As of now, Google and Bing can index synchronous JavaScript applications just fine. Synchronous being the key word there. If your app starts with a loading spinner, then fetches content via Ajax, the crawler will not wait for you to finish.” Jump to quote
- “If you’re only investigating SSR to improve the SEO of a handful of marketing pages … then you probably want SSG instead of SSR.” Jump to quote
Vue Router — history mode
- On hash mode: “It does however have a bad impact in SEO.” Jump to quote
Google — JavaScript SEO basics
- On canonicals: “The best way to set the canonical URL is to use HTML, but if you have to use JavaScript, make sure that you always set the canonical URL to the same value as the original HTML.” Jump to quote
Google — dynamic rendering (deprecated)
- “Dynamic rendering was a workaround and not a long-term solution for problems with JavaScript-generated content in search engines.” Jump to quote
Vue SEO checklist
A pass to confirm a Vue 3 app is crawlable and indexable:
- Vue Router uses
createWebHistory()(HTML5 history mode), notcreateWebHashHistory(). - Server has an
index.htmlfallback so direct route hits don’t 404. -
@unhead/vueis wired up; every page sets its own title, description, and canonical (not all sharing the default). - Open Graph / Twitter Card tags are present (and actually render for social scrapers — i.e. they’re in the served HTML, not CSR-only).
- Canonical is set in HTML where possible; if injected via JS, it matches.
- SEO-critical content is in the HTML via SSR/SSG/prerender — not fetched in
onMountedon a CSR page. - Client-side not-found states return a real
404or anoindex(no soft-404 shells). - JavaScript and CSS are not blocked in
robots.txt. - No dynamic-rendering dependency in new builds (deprecated by Google).
- Core Web Vitals pass — code splitting, lazy routes,
fetchpriority="high"on the LCP image. - If SSR/SSG: no hydration mismatches (check the console; use
data-allow-mismatchonly for intentional ones). - Structured data (JSON-LD) confirmed in the rendered HTML via URL Inspection.
The mental models
1. The default is the trap. Vue 3 out of the box is CSR — content isn’t in the raw HTML. Everything in Vue SEO is about overriding that default for pages that need to be found.
2. Rendering decides the outcome.
The single highest-leverage choice is how the HTML is produced: CSR (risky) →
prerender/vite-ssg (content-stable) → SSR/SSG via Nuxt (dynamic, SEO-critical).
Pick the lightest option that puts your content in the HTML.
3. Synchronous in the HTML, not async after mount. Content that’s there synchronously renders reliably; data fetched after mount may be missed — by the crawlers that render late, and entirely by the ones that don’t.
4. Two audiences of bots. Google renders (eventually). Bing and social scrapers largely don’t. Design for the worse one if Bing traffic or share previews matter — that means HTML, not CSR.
5. The decision tree for a new Vue project.
Pure app behind a login? CSR is fine. Content-stable marketing/docs/blog? vite-ssg
or VitePress. Dynamic, per-request, SEO-critical content? Nuxt. Never reach for
dynamic rendering or prerender-spa-plugin — both are dead ends in 2026.
6. Server and client must agree. Hydration mismatch isn’t just a perf nit — it’s a content delta between what Google indexed and what the user gets. Keep the two renders identical.
Vue SEO — cheat sheet
Router mode
| Mode | URL | SEO |
|---|---|---|
createWebHistory() | example.com/about | Use this (needs server fallback) |
createWebHashHistory() | example.com/#/about | ”Bad impact in SEO” — avoid |
Head / meta libraries
| Library | Status |
|---|---|
vue-meta | Legacy (Nuxt 2 era) |
@vueuse/head | Sunset |
@unhead/vue (useSeoMeta()) | Current standard |
Rendering strategies
| Strategy | When | SEO risk |
|---|---|---|
| CSR (default) | App pages, behind login | Highest |
vite-ssg prerender | Content-stable SPAs (marketing/docs/blog) | Low |
| SSR (Nuxt) | Dynamic, per-request pages | Low |
| SSG (Nuxt / VitePress) | Content known at build time | Lowest |
prerender-spa-plugin | — | Legacy/unmaintained — don’t |
| Dynamic rendering | — | Deprecated by Google — don’t |
Fast facts
- Google renders Vue (evergreen Chromium WRS) — but delayed, and async data can be missed.
- Bing + social scrapers often don’t run JS — bare CSR = empty share previews.
- Canonicals: HTML first; JS canonicals work but are riskier.
- Vue 2 is EOL (Dec 2023) — Vue 3 only.
How should a Vue route render?
Choose a Vue SEO rendering path
Vue SEO mistakes
- Shipping search landing pages as a Vite SPA shell. Use Nuxt SSR/SSG or prerendering so essential content is in source HTML.
- Using hash mode for public content routes. Fragments do not create normal server URLs. Use history routing with host fallback configured correctly.
- Setting head tags only after client fetches. Generate metadata from server/build data through the head integration.
- Using dynamic rendering as the long-term fix. Maintain one consistent user/crawler rendering path instead of bot-specific output.
- Testing only with Google. Raw HTML matters to other search engines, social scrapers, and tools that do not execute the application fully.
Raw HTML contains only the Vue mount element
Likely cause: the route is CSR-only. Fix: move it to Nuxt SSR/SSG or prerender the finite route set. Confirm: curl returns the primary content and links.
Direct route requests return 404
Likely cause: Vue Router history mode lacks server fallback or the deployment omitted generated routes. Fix: configure host rewrites for SPA-only routes or deploy actual SSR/static route output. Confirm: refresh and direct requests return the intended status and page.
Titles update in the browser but not in source
Likely cause: metadata depends on client lifecycle or an asynchronous browser fetch. Fix: resolve data during SSR/SSG and render head tags with @unhead/vue or Nuxt’s head APIs. Confirm: raw and rendered titles, canonicals, and robots directives agree.
Hydration replaces correct server content
Likely cause: server/client data or environment branches disagree. Fix: make initial data deterministic and remove browser-only conditions from essential markup. Confirm: hydration completes without mismatch warnings or changing indexable content.
Compare Vue raw and rendered output
curl -fsSL https://example.com/page/ > raw.html
grep -Eio '<title>[^<]+|<h1[^>]*>[^<]+|<link[^>]+rel="canonical"[^>]*' raw.html Run this in DevTools Console after hydration:
({title: document.title, h1: document.querySelector('h1')?.textContent.trim(), canonical: document.querySelector('link[rel="canonical"]')?.href, links: [...document.querySelectorAll('a[href]')].length});If the important values exist only in the Console result, SSR/prerendering is incomplete.
Find hash-based content links
[...document.querySelectorAll('a[href^="#"]')].map(a => ({text: a.textContent.trim(), href: a.getAttribute('href')}));Review the list manually: in-page navigation is fine; using fragments as separate content routes is the problem.
Patrick's relevant free tools
- Raw vs. Rendered HTML Checker — See what's in your page's initial HTML versus after JavaScript runs — headless-Chrome rendering only when the page actually needs it, a rendering-strategy verdict (SSR / prerendered / CSR / hybrid), ~15 calibrated JavaScript-SEO checks (noindex, canonicals, robots.txt blocking, links, soft 404s), a side-by-side raw-vs-rendered diff, and shareable reports.
- SEO Incident Simulator — Practice thirty deterministic technical SEO incident investigations — indexability, crawl controls, redirects, sitemaps, markup, caching, DNS, bot verification, rendering, hreflang, and faceted navigation — with clearly labeled fixture evidence and Find → Fix → Verify handoffs.
- Staging vs. Production SEO Diff — Compare matched release URLs across redirects, canonicals, robots directives, hreflang, selected headers, schema eligibility, and raw or optionally rendered content with honest not-evaluated states.
Tools for Vue SEO
- URL Inspection (Google Search Console) — the source of truth. Run a live test and check the rendered HTML, screenshot, page resources (was any JS/CSS blocked?), and console messages. This is how you confirm your Vue content, meta, and JSON-LD actually rendered.
- Rich Results Test — a fast rendered-HTML + structured-data check for a single URL without verifying the site.
- Chrome DevTools — diff View Source (raw HTML, what a non-rendering bot sees) against the Elements panel (the hydrated DOM). The Console surfaces hydration mismatch warnings.
- Vue Devtools — inspect component state and router mode while you debug what’s client-only vs. server-rendered.
vite-ssg— build-time prerendering for content-stable Vue 3 SPAs (swapvite build→vite-ssg build).- JavaScript-rendering crawlers — Ahrefs Site Audit and Screaming Frog (JS-rendering mode) execute the JS so you can diff raw vs. rendered across the whole Vue site.
- Lighthouse / PageSpeed Insights — catch the CSR bundle’s hit on LCP and the Core Web Vitals levers (code splitting, lazy routes, image priority).
Test yourself: Vue SEO
Five quick questions on making Vue apps crawlable and indexable. Pick an answer for each, then check.
Resources worth your time
My writing
- JavaScript SEO: A Definitive Guide — my full guide to rendering, DOM parity, and the framework-level decisions; includes the Vue Router history-vs-hash guidance and my JS-canonical test.
- The Beginner’s Guide to Technical SEO — where rendering and JavaScript SEO fit in the bigger picture.
My speaking
- How Search Works (SlideShare) — my walkthrough of crawling, rendering, indexing, and ranking. (My standing disclaimer applies: “This is my understanding of systems… not going to be 100% complete or accurate.”)
From around the industry
- Server-Side Rendering (SSR) | Vue.js — the official guide: SSR’s SEO benefit, the async caveat, and when to pick SSG instead.
- Different History modes | Vue Router — why hash mode hurts SEO and how to configure history mode.
- useSeoMeta() · Unhead — the current
@unhead/vuemeta API. - antfu-collective/vite-ssg | GitHub — build-time prerendering for Vue 3 SPAs.
- Site Config | VitePress — SEO for Vue-powered documentation sites.
- How Nuxt.js solves the SEO problems in Vue.js | LogRocket — the case for Nuxt as the SSR/SSG path.
- Google no longer recommends using dynamic rendering | Search Engine Land — coverage of the dynamic-rendering deprecation.
- Vue.js And SEO: How To Optimize Reactive Websites | Smashing Magazine — a widely-cited 2019 piece; useful for the async-timing experiments, but dated on tooling (pre-evergreen Chromium).
Videos
- Technical SEO tips for Vue.js by Martin Splitt (Google Search Central, 2019) — Google’s own Vue-specific JavaScript SEO episode: making titles, descriptions, and URLs discoverable when you build with Vue. Watch
- Google Search Central (YouTube) — Martin Splitt’s broader JavaScript SEO series covers rendering, the render queue, and the failure modes that apply to any Vue app. Channel
Vue SEO
Vue SEO is the practice of making web apps built with Vue.js crawlable, renderable, and indexable. Because Vue 3 defaults to client-side rendering, the content isn't in the raw HTML — so router mode, head management, and a rendering strategy (CSR, prerendering, SSR, or SSG) all matter.
Related: JavaScript SEO, Headless CMS SEO
Vue SEO
Vue SEO is the practice of making web applications built with Vue.js discoverable, crawlable, and indexable by search engines. The catch is that Vue 3 defaults to client-side rendering (CSR) — a plain Vite + Vue app ships a near-empty HTML shell and builds the DOM in the browser, so the content isn’t in the raw HTML that crawlers fetch first.
Google can render Vue apps with its evergreen Chromium-based Web Rendering Service, but rendering is queued and delayed, and content behind slow async API calls may be missed. Other crawlers — Bing, social preview scrapers — are far less reliable at executing JavaScript. So Vue SEO comes down to a few decisions: use Vue Router’s HTML5 history mode (createWebHistory()) rather than hash mode; manage <head> with @unhead/vue; and pick a rendering strategy that gets content into the HTML — prerendering (vite-ssg), full SSR, or SSG via Nuxt for anything where SEO matters.
Vue 2 reached end of life in December 2023, so current Vue SEO is Vue 3 work. Dynamic rendering — serving prerendered HTML to bots — is deprecated by Google and shouldn’t anchor a new build.
Related: JavaScript SEO, Headless CMS SEO
Build-time retrieval analysis plus live signals for this exact article. The automatic chunk report includes a deterministic readiness score and is ready without a model download.
Search Console
sampleGA4 traffic (28d)
sampleCloudflare traffic (7d)
sampledCrUX field data (28d, phone)
sampleGoogle NLP entities
localChangelog
Updated Jul 18, 2026.
Editorial summary and recorded change details.Summary
Fixed a wrong Vapor Mode version claim (it shipped experimental in the Vue 3.6 beta/RC line, not stable Vue 3.5), reframed the CSR-by-default claim to scope it to the plain Vite scaffold rather than Vue core, and added grounded guidance on SSR per-request state, hydration-suppression limits, router-fallback scoping, and the no-outcome-guarantee caveat for SSR/SSG.
Change details
- Before
Vapor Mode (Vue 3.5) bypasses the virtual DOM for eligible components, which can cut hydration cost on SSR pages.AfterCorrected Vapor Mode to Vue 3.6 (currently beta/RC, not yet stable) rather than 3.5, and marked it experimental rather than production-ready. -
Reworded the CSR-by-default framing to attribute the default to the plain Vite + Vue scaffold, not to Vue core, since Vue also supports SSR/SSG as first-class paths.
-
Added a caution that a Vue Router history-mode server fallback must not swallow real static assets, API routes, or intended 404 responses.
-
Added createSSRApp context and clarified that Vue 3.5+'s data-allow-mismatch suppresses the hydration-mismatch warning, not the underlying content difference.
-
Added per-request app/router/store guidance for manual SSR to avoid cross-request state pollution, grounded in Vue's official SSR docs.
-
Added an explicit caveat that SSR/SSG gets content into the HTML but doesn't guarantee indexing, ranking, Core Web Vitals, or hydration parity.
Full comparison unavailable — no prior snapshot was archived for this revision.