ssrchecker
HomeCheckFrameworksVue

Is your Vue app server-rendered, or a client-only SPA?

Short answer
Vue on its own does not render on the server. A default create-vue or Vite setup ships an empty <div id="app"> that JavaScript fills in, so crawlers see nothing on the first pass. The realistic way to server-render a Vue app is Nuxt, Vue's meta-framework — plain Vue has no built-in SSR for a full site.

Vue behaves much like React here: the library renders in the browser, and server rendering comes from a framework built on top of it. For React that framework is Next.js; for Vue it is Nuxt. So the practical question on this page is whether your Vue app is a plain client-only SPA — and if it is, whether you should move to Nuxt.

Check your Vue app
https://

The empty #app signature

A client-only Vue app has a recognisable fingerprint in the raw HTML. Reduced to essentials:

<div id="app"></div> <script type="module" src="/assets/index-9f8e7d.js"></script>

That is the whole page to a crawler. The checker reports a content ratio near zero, few or no links, and usually a placeholder title. The rendered screenshot shows your complete site, which is exactly why this is hard to catch by looking — the browser fills everything in.

The mount element is Vue's tell. React leaves <div id="root">, Angular leaves <app-root>, Vue conventionally leaves <div id="app"> — though the id is configurable. The signature is the emptiness, not the exact id.

Options API vs Composition API does not matter here

Worth killing a common misconception before it wastes anyone's time. Whether you write components with the Options API or the Composition API has no effect on rendering. Both run in the same place at the same time. Neither puts content in the initial HTML on a client-only app, and neither prevents it on an SSR app.

If someone suggests rewriting components from one API to the other to fix indexing, that is not a rendering fix and will change nothing the crawler sees. The rendering mode is decided by whether there is a server rendering the components at all — not by how the components are authored.

vue-router: history mode vs hash mode

Like Angular, Vue's router can run in hash mode, and it carries the same penalty. In hash mode your URLs look like site.com/#/about. Everything after the # is a fragment, and crawlers treat fragments as part of the same base URL — so every route on the site collapses into one indexable page.

How to spot it
Watch the address bar as you click around. If the URLs contain #/, you are in hash mode. History mode (real paths) is the SEO-friendly choice, but it needs a server rewrite so that deep links resolve to index.html instead of 404ing. Fix this regardless of rendering — a hash-routed site cannot index per-page even when it renders correctly.

The dynamic title trap

A failure worth checking even when body content passes. If Google shows a title that is not the one you set, the title is probably being written after the app boots.

Head-management libraries in the Vue ecosystem — @unhead/vue, useHead, or the older vue-meta — set the document title and meta tags from within your components. On a client-only app that happens when Vue runs, which is after the crawler has already captured the initial HTML. The tags are correct in the browser and absent from the raw response. The checker reports title, description and canonical separately from body content so you can see this directly.

When the answer is Nuxt

If your Vue app needs to rank and this check comes back CSR, the durable fix is usually Nuxt rather than hand-rolling server rendering. Nuxt is to Vue what Next.js is to React: it provides server rendering, file-based routing, static generation and head management as first-class features, and it server-renders by default.

Vue does expose its own renderToStringfor building SSR by hand, but very few teams maintain that themselves — the realistic path is the framework. If you are already on Nuxt and still seeing a CSR or HYBRID verdict, the cause is different and specific to Nuxt's data-fetching model. That has its own page.

Next stepAlready on Nuxt, or considering it? /check/nuxt covers why an SSR-by-default Nuxt app can still come back client-rendered.
Side-by-side comparison of a Vue SPA's raw HTML showing an empty div id app and the rendered DOM showing the full application
Empty shell on the left, full app on the right. The crawler acts on the left.

Your setup and the verdict to expect

Find your configuration. A result that differs from the expectation is worth investigating.

SetupExpectWhy
create-vue / Vite default SPACSRClient-only. Ships an empty <div id="app"> filled in by JavaScript.
Old Vue CLI (vue-cli-service build)CSRSame shell model. Different tooling, identical crawler result.
Nuxt (Vue's SSR meta-framework), SSR onSSRThis is the realistic SSR path for Vue. See /check/nuxt.
vue-router in hash mode (URLs contain #/)CSREvery route collapses to one URL for crawlers. Fragments don't index.
Hand-rolled server with Vue's renderToStringSSRGenuine SSR if data resolves before render. Rare outside Nuxt.
SPA behind a prerendering serviceSSRCrawlers get a cached rendered snapshot. Works, adds drift risk.
Any view fetching its main content in onMountedCSR or HYBRIDThe shell may render; the content arrives only after mount.
Static build with content inlined at build timeSSRIf the data is baked into the HTML, crawlers receive everything.

Vue rendering FAQ

No. Vue is a client-side framework, and a default create-vue or Vite project produces an empty div with id app that JavaScript fills in after loading. Crawlers receive no content on the first pass. Server rendering for a full Vue app realistically comes from Nuxt, Vue's meta-framework, rather than from Vue itself.