Client-Side JavaScript Frameworks
React, Vue, Angular, Svelte, and SolidJS share one SEO problem — client-side rendering ships an empty shell — and one fix pattern: head management plus a rendering strategy.
React, Vue, Angular, Svelte, and SolidJS are UI libraries that, by default, ship a near-empty HTML shell and build the page in the browser (client-side rendering). That means crawlers get a blank page until JavaScript runs — Google can render it, but on a delayed queue, and other crawlers (Bing, AI bots) are far less reliable. The fix is the same across all five: (1) a head-management package so meta tags exist for each route, and (2) a rendering strategy — prerendering, SSR, or moving to the matching meta-framework. This hub explains the shared problem and the shared fix, then points you to the per-framework deep dives.
TL;DR — React, Vue, Angular, Svelte, and SolidJS are tools developers use to build interactive sites. By default, they send the browser a nearly empty HTML page and then build the real content with JavaScript. That’s great for users but risky for SEO — a search crawler can land on a blank page. The fix is the same for all of them: manage your
<title>and meta tags per page, and make sure your content exists in the HTML before JavaScript runs.
What client-side frameworks are
Client-side frameworks can render route content in the browser after an initial HTML response. Evidence for this claim Primary standard or official documentation supporting the adjacent article claim. Scope: Protocol semantics and Search behavior are kept separate; no indexing, ranking, or migration-timing guarantee is inferred. Confidence: high · Verified: MDN: Client-side frameworks Google can render JavaScript, but resource availability and rendering still affect what is processed. Evidence for this claim Primary standard or official documentation supporting the adjacent article claim. Scope: Protocol semantics and Search behavior are kept separate; no indexing, ranking, or migration-timing guarantee is inferred. Confidence: high · Verified: Google: JavaScript SEO basics
A client-side framework is a JavaScript library that builds the page in your visitor’s browser. The five you’ll run into most are React, Vue, Angular, Svelte, and SolidJS. They’re how most modern web apps and dashboards get built.
The catch is in the word client-side. By default, these frameworks ship a tiny HTML file that looks roughly like this:
<body>
<div id="root"></div>
<script src="/bundle.js"></script>
</body>There’s no content in that HTML — just an empty container and a script. The browser downloads the script, runs it, and then the headline, the text, the links, and everything else appear. This default behavior is called client-side rendering (CSR), and a site built this way is often called a single-page application (SPA).
Why that’s a problem for SEO
A search crawler is not a person clicking around. When Googlebot or another crawler fetches your page, the first thing it gets is that empty shell. If the crawler doesn’t run your JavaScript, it sees a blank page — no content to index.
Google can run JavaScript, so it usually does eventually see your content. But:
- It happens on a delay (rendering is a separate, queued step).
- Other crawlers — Bing and the AI crawlers feeding tools like ChatGPT — are far less reliable at running JavaScript.
So “it works in my browser” doesn’t mean “search engines can see it.”
The fix is the same for all five
No matter which framework you picked, the solution comes in two parts:
- Manage the head. Each page needs its own
<title>and meta description. A plain CSR app has one HTML file, so without help every page shares the same title. Each framework has a small package that fixes this (more in the Advanced tab). - Pick a rendering strategy. Get your content into the HTML before JavaScript runs — by prerendering (building static HTML ahead of time), server-side rendering (SSR), or moving to the framework’s matching meta-framework (Next.js for React, Nuxt for Vue, and so on).
Want the per-framework specifics — which head package, which rendering option, and how Google actually handles these apps? Switch to the Advanced tab, then jump to the dedicated guide for your framework.
TL;DR — React, Vue, Angular, Svelte, and SolidJS are all UI libraries that default to client-side rendering: they ship a near-empty HTML shell and build the DOM in the browser. The SEO problem is identical across all five — crawlers get an empty shell, and content only exists after JS executes. Google renders these apps but on a delayed, queued basis; Bing and AI crawlers are much less dependable. The fix is also identical: (1) head management (a per-route package for
<title>/meta) and (2) a rendering strategy (prerendering, SSR, or migrating to the matching meta-framework). The differences between the frameworks are mostly which package and which strategy — covered in each deep dive below.
They’re libraries, not full frameworks — and that’s the root of it
Framework architecture varies, so client rendering is not an automatic indexing failure. Evidence for this claim Primary standard or official documentation supporting the adjacent article claim. Scope: Protocol semantics and Search behavior are kept separate; no indexing, ranking, or migration-timing guarantee is inferred. Confidence: high · Verified: MDN: Client-side frameworks Server rendering or prerendering reduces dependence on crawler-side JavaScript execution without guaranteeing indexing. Evidence for this claim Primary standard or official documentation supporting the adjacent article claim. Scope: Protocol semantics and Search behavior are kept separate; no indexing, ranking, or migration-timing guarantee is inferred. Confidence: high · Verified: Google: JavaScript SEO basics
React, Vue, Angular, Svelte, and SolidJS are primarily UI rendering libraries. Their job is to turn your data into a DOM and keep it in sync as state changes. Out of the box, that DOM is built in the browser — which is exactly what makes them feel fast and app-like, and exactly what creates the SEO problem.
A default build ships an HTML shell with an empty mount node (<div id="root">,
<div id="app">, <app-root>) and a JavaScript bundle. Everything a search
engine cares about — headings, body copy, internal links, structured data — is
injected by that bundle after it downloads and executes. Before JS runs, there’s
nothing there.
This is client-side rendering — the out-of-the-box behavior for the bare libraries before you add a meta-framework or a build-time prerender step. It’s not a bug; it’s the default you start from. But “default” isn’t “permanent” or “universal”: the default changes by version and by which CLI or starter you used, and two routes in the same app can end up on different rendering modes depending on how each one is built (a marketing page prerendered, a dashboard left as pure CSR). Don’t assume a framework’s behavior from its name — check what a given route in a given build actually ships.
The shared problem: an empty shell
Google is explicit that it processes JS apps in phases — crawl, render, index — and that “without rendering Google might not see that content.” For a CSR app, all the content lives behind that render step. Three consequences fall out of that:
- The render is delayed, and the wait isn’t fixed. Rendering is a separate, queued step after crawling — Google documents crawl, render, and index as three distinct phases, but it doesn’t commit to one universal wait time for every page. The delay varies by URL and by Google’s own resourcing at the time; your content doesn’t exist for indexing until the render actually completes, however long that takes for that page.
- Non-Google crawlers aren’t a given. Bing renders JavaScript inconsistently, and most AI crawlers (the bots fetching pages for LLMs and AI search) do little or no JS execution. These are separate companies with separate infrastructure, so none of it generalizes from Google’s documentation — verify each provider’s current behavior rather than assuming it matches Google’s or last year’s test. A CSR-only site risks being near-invisible to whichever crawlers don’t render, and AI crawlers are a growing share of traffic.
- Per-page metadata is missing. One HTML file means one
<title>and one meta description for every route unless you actively manage the head per route.
The shared fix, part 1: head management
Because a SPA has a single HTML document, you need code that updates the document head as the user (and the crawler’s renderer) moves between routes. Every framework has a canonical package or built-in API for this:
- React →
react-helmet-async(or the framework’s metadata API if you’ve moved to Next.js). - Vue →
@unhead/vue(the engine behind Nuxt’s SEO utilities). - Angular → the built-in
TitleandMetaservices from@angular/platform-browser— no extra dependency. - Svelte → the built-in
<svelte:head>element. - SolidJS →
@solidjs/meta(and SolidStart for the SSR path).
Head management gets you correct, unique meta tags — but it does not solve the empty-shell problem on its own. The tags still only appear after JS runs. That’s why you also need part 2.
The shared fix, part 2: a rendering strategy
To put real content (and head tags) into the HTML before JavaScript executes, you choose one of three approaches. The ranking, by how much SEO risk they remove:
- Prerendering / static generation (SSG). Build static HTML for each route at build time. Lowest risk for content that doesn’t change per request. Each library has a prerender path (e.g. SolidJS’s built-in prerender, Vue prerender plugins, Angular’s prerendering).
- Server-side rendering (SSR). Render the HTML on the server per request, then hydrate in the browser. This is where the meta-frameworks come in — Next.js (React), Nuxt (Vue), Angular SSR (formerly Angular Universal), SvelteKit (Svelte), and SolidStart (SolidJS). For most content sites that must rank, adopting the matching meta-framework is the cleanest fix.
- Stay full CSR, but make it crawlable. Sometimes acceptable for app-like surfaces behind a login that don’t need to rank — but a poor default for anything you want in search.
Make this call per route, not per app. A marketing page and a logged-in dashboard in the same codebase can reasonably land on different rows of that list. For each route, ask:
- Output — does the content need to be in the initial response, or is it fine to appear after JS runs?
- Freshness — is it stable enough to build once (prerender), or does it change per request (SSR)?
- Personalization — is it different per visitor? Prerendering can’t help there; you’re choosing between SSR and CSR.
- Server cost — SSR adds compute on every request; prerendering front-loads that cost at build time instead.
- JS dependence — how much of the route’s usefulness depends on client JavaScript running at all?
- Failure behavior — if JS fails or is blocked, does the route degrade to something usable, or to nothing?
The decision is the same conversation regardless of framework: does this content need to rank or be cited? If yes, get it into the server-rendered or prerendered HTML. If it’s purely interactive app UI, CSR is fine.
How Google handles these apps (and why others don’t)
Google can render every one of these frameworks — it runs an evergreen,
headless Chrome and executes your JS. But it does so on a deferred queue, and
the renderer is stateless (no persisted cookies/localStorage, no service
workers, no interaction — it won’t scroll or click). So the same JS-SEO rules from
the JavaScript SEO hub apply on top of the
framework choice: real <a href> links, parity between raw and rendered DOM, no
content gated behind interaction, no JS-injected noindex.
Outside Google, the picture is worse. Bing’s JS rendering is patchier, and the AI crawlers largely don’t render at all. For a CSR app, that can mean your content exists for Google (eventually) but not for Bing or for the AI tools that an increasing number of people search with. SSR or prerendering closes that gap for everyone, not just Google — which is the strongest argument for not shipping CSR-only content.
The five frameworks at a glance
| Framework | Default | Head management | SSR / meta-framework | SEO note |
|---|---|---|---|---|
| React | CSR | react-helmet-async | Next.js (Metadata API) | The most common CSR-first SPA; Next.js is the standard SEO fix |
| Vue | CSR | @unhead/vue | Nuxt (useSeoMeta()) | Nuxt is SSR-by-default; plain Vue needs prerender or Nuxt |
| Angular | CSR (SPA) | built-in Title/Meta services | Angular SSR (was Universal) | Big SPAs; modern Angular adds SSR + incremental hydration |
| Svelte | CSR (Svelte) / SSR (SvelteKit) | <svelte:head> | SvelteKit (SSR default) | SvelteKit is SSR by default — watch the ssr:false static trap |
| SolidJS | CSR | @solidjs/meta | SolidStart | Fine-grained reactivity; SolidStart adds SSR + prerender |
The pattern is consistent: a CSR default, a head package, and a rendering strategy (usually the matching meta-framework). What differs is the names and a few sharp edges.
Where to go next: the framework deep dives
This hub is the map. Each framework has its own guide with the specific packages, config, and gotchas:
- React SEO — why CSR-first React creates indexing risk, React Router and the
History API,
react-helmet-async, and when to reach for Next.js. - Vue SEO — Vue 3’s CSR default,
createWebHistory(),@unhead/vue, prerendering without a meta-framework, and when Nuxt is the right call. - Angular SEO — Angular’s SPA defaults,
@angular/ssr, the built-inTitleandMetaservices, prerendering, and incremental hydration. - Svelte SEO — Svelte vs SvelteKit, SSR by default in SvelteKit,
<svelte:head>, theadapter-static+ssr: falsetrap, and AI-crawler implications. - SolidJS SEO — SolidJS’s CSR default,
@solidjs/meta, SolidStart for SSR and prerendering, and how its fine-grained model affects rendered output.
For the rendering modes themselves (CSR, SSR, SSG, ISR, hydration, dynamic rendering) and the broader render/parity rules, see the parent JavaScript SEO hub.
AI summary
A condensed take on the Advanced version:
- One shared starting point. React, Vue, Angular, Svelte, and SolidJS
default to client-side rendering out of the box — a near-empty HTML shell
(
<div id="root">+ a bundle) that builds the DOM in the browser. That default changes by version, starter, and meta-framework, and can vary route by route within the same app — check what a given route actually ships rather than assuming from the framework name. - Google renders it, but the wait isn’t fixed. Rendering is a separate, queued step after crawling, but Google doesn’t commit to one universal delay for every page — it varies by URL. The renderer is also stateless and won’t scroll/click.
- Other crawlers aren’t a given. Bing renders JS inconsistently; most AI crawlers don’t render at all. These are separate companies, so none of this generalizes from Google’s docs — verify each provider’s current behavior.
- One shared fix, two parts. (1) Head management — a per-route package for
<title>/meta:react-helmet-async(React),@unhead/vue(Vue), built-inTitle/Meta(Angular),<svelte:head>(Svelte),@solidjs/meta(SolidJS). (2) A rendering strategy — prerender/SSG, SSR, or move to the matching meta-framework (Next.js / Nuxt / Angular SSR / SvelteKit / SolidStart). - Head management alone isn’t enough — the tags still only appear after JS runs; you still need a rendering strategy to fill the shell.
- Decide per route, weighing output timing, freshness, personalization, server cost, JS dependence, and failure behavior — not per app. If a route needs to rank or be cited, get it into server-rendered or prerendered HTML. If it’s app-only UI behind a login, CSR is fine.
Official documentation
Primary-source documentation from the search engines and the frameworks.
- Understand the JavaScript SEO basics — the crawl → render → index phases, crawlable links, and testing rendered HTML.
- Fix Search-related JavaScript problems — soft-404 handling after client-side routing, the History API, and renderer constraints.
- In-Depth Guide to How Google Search Works — where rendering sits in crawl → index → serve.
Bing / Microsoft
- bingbot Series: JavaScript, Dynamic Rendering, and Cloaking. Oh My! — Bing’s take on rendering JS and why dynamic rendering exists.
The frameworks (head + rendering)
- React docs — the library that popularized the CSR-first SPA; see also Next.js metadata.
- Vue.js — Rendering / SSR and Nuxt SEO utilities.
- Angular — Server-side rendering and the
Title/Metaservices. - SvelteKit — Page options (SSR/prerender) and
<svelte:head>. - SolidStart — SSR & prerendering and
@solidjs/meta.
Quotes from the source
On-the-record statements that ground the shared CSR problem and fix. Each search-engine link is a deep link that jumps to the quoted passage.
Google — rendering & why the shell matters
- “Google processes JavaScript web apps in three main phases: 1. Crawling 2. Rendering 3. Indexing.” Jump to quote
- “Rendering is important because websites often rely on JavaScript to bring content to the page, and without rendering Google might not see that content.” Jump to quote
- “Google can only discover your links if they are <a> HTML elements with an href attribute.” — applies to every framework’s router. Jump to quote
- “Google Search does not interact with your page.” — the renderer won’t scroll or click to reveal content. Jump to quote
Patrick Stox (my own work — JavaScript SEO: A Definitive Guide)
- “Any kind of SSR, static rendering, and prerendering setup is going to be fine for search engines.” — the through-line for every framework on this hub.
- The renderer takes the most restrictive robots directive across raw and rendered HTML — so a framework that injects
noindexclient-side can deindex a page even when the shell saysindex.
Client-side framework SEO checklist
Works for React, Vue, Angular, Svelte, and SolidJS alike:
- You know whether your app is CSR, SSR, or prerendered today (check View Source — is content in the raw HTML, or just an empty mount node?).
- Content that must rank or be cited is in the server-rendered or prerendered HTML — not injected only after JS runs.
- Per-route
<title>and meta description are managed (head package or built-in API), and they’re unique per page in the rendered HTML. - Router links are real
<a href>anchors (History API routing, not#-fragment routing oronclickon a<div>). - JavaScript and CSS are not blocked in
robots.txt(Google won’t render from blocked files). - No content is gated behind a scroll, click, or hover.
- Client-side “not found” views return a real
404or carrynoindex(no soft-404 shells). - No JS injects a
noindexthat contradicts the raw HTML (Google takes the most restrictive). - You’ve considered Bing and AI crawlers, not just Google — SSR/prerender is the only reliable answer for them.
- You’ve verified in URL Inspection (rendered HTML + screenshot + console), not just your own browser.
Those checks collapse several distinct outputs into one page. Verify each stage separately — a route can pass one and fail the next:
| Stage | What to check |
|---|---|
| Direct response (JS disabled/blocked) | Fetch the URL with JS off. Is your unique heading and body copy present, or just the mount node? |
| Streamed chunks (if streaming) | Does content arrive progressively, or does a slow chunk hold up everything after it? |
| Rendered DOM (JS executed) | After the bundle runs, is the final DOM complete — headings, links, structured data all present? |
| Hydration | Does the client take over cleanly, or do console errors/hydration mismatches appear? |
| Client navigation | Does an in-app route change update the URL, title, and canonical the same way a direct load would? |
| Status codes | Does a real 404/410/5xx route return that status directly, not just a client-rendered message inside a 200? |
| Metadata | Is the rendered <title>, description, canonical, and robots directive correct per route — not just present? |
Frameworks → fix, at a glance
| Framework | Default render | Head management | SSR / meta-framework | Prerender path |
|---|---|---|---|---|
| React | CSR | react-helmet-async | Next.js | Next.js SSG / react-snap |
| Vue | CSR | @unhead/vue | Nuxt | Nuxt nuxi generate / prerender plugin |
| Angular | CSR (SPA) | built-in Title / Meta | Angular SSR (was Universal) | ng build prerender |
| Svelte | CSR (Svelte) / SSR (SvelteKit) | <svelte:head> | SvelteKit | adapter-static (mind ssr:false) |
| SolidJS | CSR | @solidjs/meta | SolidStart | SolidStart prerender |
The two-part fix (memorize this)
- Head management → unique
<title>/ meta per route. - Rendering strategy → prerender (SSG) or SSR or the matching meta-framework. Head management alone does not fill the empty shell.
Risk ladder (lowest → highest SEO risk)
| Approach | SEO risk | When |
|---|---|---|
| Prerender / SSG | Lowest | Content stable per request |
| SSR (meta-framework) | Low | Per-request / dynamic content that must rank |
| Hydration (isomorphic) | Low | App + content hybrid |
| Full CSR | Highest | App UI behind login that needn’t rank |
Crawler reality check
| Crawler | Runs your JS? |
|---|---|
| Googlebot | Yes — but delayed, queued, stateless |
| Bingbot | Inconsistently |
| AI crawlers (LLM / AI search) | Mostly no |
CSR-only content is a gamble everywhere except Google — and even there it’s delayed. SSR/prerender removes the gamble for all of them.
Common client-side framework SEO issues
Search tools show an empty page or only the app shell
Symptom: Raw HTML contains only a mount element such as #root, #app, or
app-root, while the visible page contains the real copy. Likely cause: The route
is fully client-rendered. Fix: Prerender stable routes or move the route to the
framework’s SSR-capable meta-framework. Confirm the fix by fetching the URL with
JavaScript disabled and finding its unique heading and body copy in the response.
Every route has the same title or canonical
Symptom: Several URLs render different views but expose the same title, description, or canonical. Likely cause: The shared HTML shell owns the head and route changes do not update it. Fix: Use the framework-specific head API and set metadata from route data. Confirm each route’s final DOM contains one self-referencing canonical and its own title.
Links work for users but crawlers do not discover routes
Symptom: Navigation works in the browser, yet linked routes stay undiscovered.
Likely cause: Click handlers on buttons or div elements replace crawlable
anchors. Fix: Render real <a href="..."> links and let the router enhance them.
Confirm the destination appears as an href in the rendered DOM without clicking.
Console shows hydration errors or mismatched content
Symptom: The server-rendered/prerendered HTML looks right, but the browser
console logs hydration warnings, or content briefly flashes and changes right
after load. Likely cause: The server output and the client’s first render
disagree — often from date/locale formatting, random IDs, or window-dependent
code running during the initial render. Fix: Make the server and client
render deterministically for the same input; move browser-only logic to run
after hydration, not during it. Confirm by loading the route with JS enabled and
checking for zero hydration errors in the console.
A route changes state in the browser but the direct URL doesn’t match
Symptom: Clicking through the app produces a working page, but requesting that same URL directly (or after a refresh) returns a different result — a generic shell, a wrong status code, or stale metadata. Likely cause: Client navigation updates the in-browser view without a matching server-side route handler, so state that exists after a client transition was never a real, independently-requestable page. Fix: Make sure every route a user can reach by navigating is also directly requestable and returns the same content, status, and metadata. Confirm by loading the URL fresh (not by clicking into it) with JS disabled first, then enabled.
Test yourself: Client-side frameworks
Five quick questions on the shared SEO problem and fix across React, Vue, Angular, Svelte, and SolidJS. Pick an answer for each, then check.
Resources worth your time
My related writing
- JavaScript SEO: A Definitive Guide — my full guide to rendering, DOM parity, the most-restrictive-directive rule, and choosing a rendering setup. The foundation under every framework on this hub.
- The Beginner’s Guide to Technical SEO — where client-side 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
- web.dev — Rendering on the Web — the Chrome team’s canonical explainer of CSR vs. SSR vs. SSG vs. hydration; the best framework-agnostic mental model for the rendering strategy half of the fix.
- Google Search Central — JavaScript SEO basics — official primary-source docs on the crawl → render → index process and crawlable links.
- Google Search Central — Fix search-related JavaScript problems — soft-404s after client-side routing and the History API, which every SPA router hits.
- Martin Splitt’s JavaScript SEO playlist — Google’s official video series on how it handles JS apps, framework-agnostic.
- Onely — JavaScript SEO hub — deep technical posts on rendering and JS-SEO auditing from a specialist agency.
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 17, 2026.
Editorial summary and recorded change details.Summary
Corrected the universal-shell and fixed-delay framing per the brief's evidence review, added a per-route rendering decision list, and expanded the checklist and troubleshooting sections with route/stage-level validation cases.
Change details
-
Reframed the empty-shell default as version/build-scoped rather than a universal fixed behavior, and removed the unsourced median-delay figure for Google's render queue.
-
Added a per-route decision list (output, freshness, personalization, server cost, JS dependence, failure behavior) to the rendering-strategy section.
-
Added a stage-by-stage validation table to the checklist and two new troubleshooting entries (hydration mismatches, client-navigation vs. direct-URL parity).
Full comparison unavailable — no prior snapshot was archived for this revision.