Join our community of websites already using SEOJuice to automate the boring SEO work.
See what our customers say and learn about sustainable SEO that drives long-term growth.
Explore the blog →TL;DR: Single-page applications can rank, but only if URLs, status codes, metadata, and primary content exist in the first HTML response. Google may render JavaScript late; most AI crawlers do not render it at all. Pick a rendering model per route, ship documents first and app states second, then let JavaScript hydrate on top. Below: a 2026 field guide for Next.js, Nuxt, SvelteKit, and Astro teams.
The opening question for SPA SEO is no longer "can Googlebot render JavaScript." Yes, it can. Martin Splitt has been saying so for years. People still debug single-page apps by staring at view-source: as if that were the page Google indexed.
"A lot of people are still looking at view source. That is not what we use for indexing. We use the rendered HTML."
Martin Splitt, Developer Advocate at Google
That kills one bad habit. If you only inspect the initial source of a React, Vue, Angular, SvelteKit, Nuxt, Remix, or Next.js app, you miss what Google eventually sees. Rendered DOM matters.
But the question for 2026 is not whether Google can render. It is whether the document arrives soon enough to be useful for Googlebot, social parsers, and the AI crawlers that increasingly dominate the citation graph.
For years, I treated this as a Google-only problem (I was wrong about that for years). Then AI crawlers showed up with a different posture.
"The results consistently show that none of the major AI crawlers currently render JavaScript."
Vercel Engineering
GPTBot, ClaudeBot, PerplexityBot, the Gemini training fetch — all of them fetch raw HTML. If your most important content lives behind hydration, those crawlers see an empty app shell where your pricing table, product copy, FAQ, or comparison content should be. Run our AI Crawler Inspector against a few SPA pages and you will see what they see.
Most teams skip this step and ask whether the whole SPA needs SSR. That framing wastes engineering time.
A real SPA mixes two things: crawlable pages and private app states. Pricing pages, blog posts, documentation, templates, integrations, comparison pages, and product landing pages are pages. Dashboards, onboarding flows, modals, filters, account screens, and saved reports are app states.
Start with classification. Do not ask engineers to "fix SEO for the app." Ask them to mark which routes deserve search traffic, then choose rendering, indexing, canonicals, and status codes per route.
| Route type | Should rank? | Rendering choice | Indexing rule |
|---|---|---|---|
| Blog post | Yes | SSG or SSR | Index, canonical self |
| Product landing page | Yes | SSG or SSR | Index, canonical self |
| Internal search results | Usually no | CSR or SSR | Often noindex |
| Dashboard route | No | CSR is fine | Block behind auth or noindex |
| Faceted filter URL | Sometimes | SSR only if curated | Canonical or noindex |
A SPA with 40 public URLs and 4,000 dashboard states does not have 4,040 SEO pages. It has 40 pages and a product interface. Public routes need stable URLs, self-canonicals, server-delivered metadata, crawlable links, useful first-byte HTML, and correct status codes. Dashboard routes need fast interaction, auth, and state management. Forcing both groups into one rendering model usually makes both worse.
Rendering strategy is an architecture choice, not a plugin setting. If you pick the wrong model, every later ticket becomes a workaround.
Client-side rendering is perfectly fine for authenticated software screens. If a user must log in, crawlers should not index the route anyway. CSR becomes risky when the same app shell serves pricing pages, docs, articles, and product pages where content appears only after JavaScript runs and APIs respond.
Static site generation builds pages into HTML ahead of time. For blogs, docs, changelogs, glossary pages, templates, and most marketing content, SSG is hard to beat. Fast, cacheable, cheap, crawler-friendly. Astro 5 leans hardest in this direction with its islands architecture and zero-JS-by-default posture.
Server-side rendering fits when public content varies by request, geography, inventory, permissions, or freshness. Lee Robinson described the basic Next.js model plainly:
"Next.js pre-renders the page into HTML on the server on every request."
Lee Robinson, VP of Developer Experience at Vercel
SSR gives crawlers HTML without waiting for the client bundle while still letting the page reflect fresh data.
Incremental static regeneration refreshes static pages after publish. For programmatic SEO, docs, and large template libraries, ISR can prevent rebuild pain without falling back to full CSR. Next.js 15 promoted Partial Prerendering to a stable config option, so you can ship a static shell and stream the dynamic holes from the same route. That mixes the two postures inside one URL, which is closer to how real product pages actually behave.
Dynamic rendering serves one version to crawlers and another to users. It can rescue a legacy SPA when a migration is not ready, but I would not design a new search strategy around it.
"So I would see this as something kind of as a temporary workaround – where temporary might mean a couple of years – but it's more of a time-limited workaround."
John Mueller, Search Advocate at Google
Use dynamic rendering as a bridge. Replace the bridge with server-rendered or static-first public routes.
The right rendering model is also a framework conversation. Defaults moved in the last twelve months, and a few of those moves change SPA SEO decisions.
app/ by default. Separate TypeScript projects for app, server, shared, and builder code reduce the type-bleed problems that used to leak server-only secrets into client bundles. Nuxt 4.4 (March 2026) added vue-router v5 and typed layout props, both small wins for shipping route-specific metadata cleanly.The framework matters less than the per-route discipline. Any of these four can ship a crawlable SPA. All four can ship an invisible one if you put public content behind hydration.
The hard SPA SEO failures are usually boring. Not mysterious ranking penalties — delivery bugs.
The first trap is the universal shell. Every URL returns the same 200 response, the same empty root, the same bundle. The router decides later whether /pricing, /docs/api, or /totally-fake-url exists. That makes crawlers work too hard, and it creates the second trap: soft 404s.
"Instead of responding with 404, it just responds with 200 … always showing a page based on the JavaScript execution."
Martin Splitt, Developer Advocate at Google
Invalid routes should return real 404 or 410 status codes. A cute client-side "page not found" component served with 200 still wastes crawl budget and confuses indexing signals.
The third trap is navigation crawlers cannot follow. Buttons, click handlers, and router events are fine for interaction, but internal discovery still needs anchors with real href values. If your most important pages are reachable only after a JavaScript handler runs, your crawlability is weaker than it looks.
Metadata is another common failure. Many SPAs update titles, descriptions, canonicals, robots tags, Open Graph, and schema after route changes. That works visually in a browser tab. It still fails for crawlers, social parsers, and AI bots. Route-specific metadata should be in the returned HTML.
Canonicals deserve their own warning. I have seen hydrated apps overwrite a correct canonical with a staging domain, a root URL, or the previous route. The bug is quiet until duplicate URLs cluster badly or the wrong page starts ranking.
Infinite scroll hides content behind client state. If page two, three, and older items have no crawlable URLs, search engines may never discover them. Use paginated fallback URLs for important archives and category pages.
API-loaded main content is fragile. If the H1, body copy, product details, reviews, or internal links require API calls after hydration, you have more failure points. Bot traffic hits rate limits. APIs block unfamiliar user agents. Timeouts leave the rendered DOM thin.
This is the 2026 wrinkle. Google's AI Overviews summarize answers above the organic list and cite the sources they used to compose the summary. Those citations are the new top of the funnel, and they reward a specific HTML shape.
The citation system pulls from clear, static signals: visible text, headings, lists, tables, semantic HTML available at first response. It does not patiently wait for hydration. If your most quotable paragraph appears only after a useEffect call, an AI Overview will not see it.
Three things matter for SPA pages that want to be cited:
The same logic applies to Perplexity citations, ChatGPT browsing answers, and Claude's web reads. Their crawlers fetch raw HTML; if the answer is not there, the page is not cited. Optimizing for AI Overview citations goes deeper, and our AIO impact analysis explains why the impression bleed feels worse than the click loss.
The rule I keep coming back to: if the route deserves search traffic, the first response should look like a page.
That means each public URL returns useful HTML with the core signals already in place:
<title>.
JavaScript can then hydrate components, personalize elements, track events, and enrich the experience. It should not be required for the crawler to understand what the page is about.
Site architecture matters too. A public route with no crawlable links pointing to it is still weak — even when it is server-rendered. A perfectly rendered document buried five clicks deep behind client-only navigation does not perform like one inside a clear internal linking system.
If you only test the browser experience, you test the happiest path. SPA SEO needs uglier tests.
curl -I https://example.com/missing-route.The uncomfortable test is the H1 test. If Googlebot needs five steps and two API calls to find the H1, the page is fragile even if it eventually gets indexed. Apply the same test to GPTBot, ClaudeBot, and PerplexityBot: if those crawlers cannot see the H1 in raw HTML, the route will not show up as an AI citation.
Use this at route level. A sitewide "pass" hides too many SPA failures.
404 or 410, not 200.href attributes.Search, AI answers, link previews, and discovery systems all depend on access. Rankings come second.
If I were starting a modern SPA with search traffic in mind, I would not make the entire product server-rendered. I would split it.
| Site area | Recommended approach |
|---|---|
| Marketing site | Static generation |
| Blog and docs | Static generation or ISR |
| Product pages | SSR, ISR, or Next.js PPR |
| Programmatic SEO pages | Static generation with strong pruning |
| Dashboard | CSR behind auth |
| Search and filter pages | Noindex unless manually curated |
| Invalid routes | Real 404 or 410 |
| Shared layout | Server-rendered metadata and navigation |
Marketing pages and articles should be HTML-first. Product surfaces that need freshness use SSR, ISR, or PPR. The dashboard stays app-like because ranking it would be pointless. Programmatic SEO pages need restraint. Static generation makes thousands of pages easy, including thousands nobody should index. Generate only pages with real search demand, useful content, and internal links. Prune the rest before Google has to make the decision for you.
The winning SPA is not the one that proves crawlers can run JavaScript. The winning SPA is the one that does not make crawlers do unnecessary work.
Yes. A SPA can rank if indexable routes return crawlable content, correct metadata, internal links, and valid status codes in the first HTML response. Google can render JavaScript, but relying on rendering for everything makes the site more fragile and invisible to AI crawlers that do not render at all.
Not for every route. SSR fits public pages with changing content. SSG or ISR is often better for stable content. Next.js 15's Partial Prerendering blends both in one route. CSR is fine for private dashboards and app states that should not be indexed.
Hash routes are a poor choice for indexable pages. They can work for on-page fragments, but public content should have clean URLs, route-specific metadata, and server-level status codes.
Usually no. Internal search pages and faceted filters often create thin or duplicate URLs. Curated filter pages can be indexed when they have unique demand, stable content, and a clear canonical strategy.
Request a fake URL and check the status code. If /this-page-should-not-exist returns 200 with a client-side not-found message, you have a soft 404 risk that wastes crawl budget.
Only if the cited content exists in raw HTML. Most AI crawlers do not execute JavaScript. Server-render or pre-render the answer paragraphs, lists, and tables you want quoted.
SEOJuice helps teams strengthen crawlable internal links and HTML-first signals across the public pages that deserve search traffic. If your SPA has orphaned routes, buried templates, or pages Google never seems to reach, automated internal linking can make the document layer easier for crawlers and AI bots to follow.
<script type="application/ld+json"> { "@context": "https://schema.org", "@type": "FAQPage", "mainEntity": [ { "@type": "Question", "name": "Can a single-page application rank on Google in 2026?", "acceptedAnswer": { "@type": "Answer", "text": "Yes. A SPA can rank if indexable routes return crawlable content, correct metadata, internal links, and valid status codes in the first HTML response. Google can render JavaScript, but relying on rendering for everything makes the site more fragile and invisible to AI crawlers that do not render at all." } }, { "@type": "Question", "name": "Is server-side rendering required for SPA SEO?", "acceptedAnswer": { "@type": "Answer", "text": "Not for every route. SSR fits public pages with changing content. SSG or ISR is often better for stable content. Next.js 15's Partial Prerendering blends both in one route. CSR is fine for private dashboards and app states that should not be indexed." } }, { "@type": "Question", "name": "Are hash routes bad for SEO?", "acceptedAnswer": { "@type": "Answer", "text": "Hash routes are a poor choice for indexable pages. They can work for on-page fragments, but public content should have clean URLs, route-specific metadata, and server-level status codes." } }, { "@type": "Question", "name": "Should SPA search results pages be indexed?", "acceptedAnswer": { "@type": "Answer", "text": "Usually no. Internal search pages and faceted filters often create thin or duplicate URLs. Curated filter pages can be indexed when they have unique demand, stable content, and a clear canonical strategy." } }, { "@type": "Question", "name": "How do I know if my SPA has a soft 404 problem?", "acceptedAnswer": { "@type": "Answer", "text": "Request a fake URL and check the status code. If /this-page-should-not-exist returns 200 with a client-side not-found message, you have a soft 404 risk that wastes crawl budget." } }, { "@type": "Question", "name": "Will AI Overviews cite a JavaScript-rendered SPA?", "acceptedAnswer": { "@type": "Answer", "text": "Only if the cited content exists in raw HTML. Most AI crawlers do not execute JavaScript. Server-render or pre-render the answer paragraphs, lists, and tables you want quoted." } } ] } </script>Love this deep dive on SPAs and client-side rendering — the React/Vue/Angular SEO pitfalls were explained really well! 🙌 I migrated a React SPA to hybrid SSR + prerendering and saw a crawl/index uptick in under a week. Please do a tutorial on hydration and sitemap strategies next 🙏
Nice — glad it helped and awesome you saw a quick uptick! I did the same move from CRA SPA → hybrid SSR + prerendering last year and saw similar gains, fwiw.
A few practical tips from that migration that might help for the tutorial you asked for:
- Hydration pitfalls: mismatches usually come from non-deterministic things in render (Date.now(), Math.random(), generated IDs, or useEffect producing visible DOM changes). Fix by moving client-only stuff into useEffect or guarding it (if (typeof window === 'undefined') ...), or use deterministic id libs.
- Streaming/partial hydration: if you’re using React 18/Next, streaming SSR + selective client hydration (islands-ish or client boundary components) reduces TTI without sacrificing SEO — imo worth covering.
- Debugging: curl or fetch the page server-side and compare to what Chrome renders after hydration; React devtools console will show hydration mismatch warnings. Also check Search Console’s “Inspect URL” to see what Googlebot sees.
- Sitemap strategy: generate sitemaps at build for static routes, dynamically for API-driven content (rebuild or incremental), split into sitemap index if >50k URLs, include lastmod, and reference it from robots.txt. For multi-lingual sites include hreflang entries or separate sitemaps per locale.
- Tools I used: Next.js (SSR + static props), next-sitemap for generation, prerender.io for tricky bots, and Search Console + server logs to confirm indexing.
If you want, I can write that hydration + sitemap tutorial — what would you prefer: code-heavy step-by-step for Next.js, or framework-agnostic notes + examples? Any stack specifics (Next/Remix/Vite/Netlify) you’re on?
ngl SPAs can rank.
no credit card required