Explain what a single page app is and how to make one SEO-friendly
TL;DR
A single page application (SPA) performs route transitions and view updates in the browser instead of loading a new HTML document for every navigation. A purely client-rendered SPA may return only an HTML shell initially, which can delay content discovery, metadata, and meaningful paint. SPA navigation does not require client-only initial rendering: the first route can be server-rendered or pre-rendered and then hydrated for client-side navigation.
For indexable routes, return meaningful HTML and correct status codes, canonical URLs, titles, metadata, structured data, and crawlable links. HTML can be produced per request (SSR), at build time or on demand (static generation/prerendering), or through a framework-specific cached regeneration model. Streaming can improve delivery but is not itself an SEO requirement. Test the rendered output and crawler behavior for the actual search engines and link-preview clients you support.
Search-friendly SPA delivery
A crawler-friendly architecture gives each route a stable URL and meaningful HTML and metadata before relying on client-side navigation.
Client rendering can still enhance the page, but route discoverability should not depend on a crawler executing an empty application shell perfectly.
What is a single page app?
A SPA performs navigation and content updates on the client after the initial document load. That initial document can be a minimal client-rendered shell or fully rendered HTML that is later hydrated; the defining characteristic is client-side navigation, not an empty initial response.
Key characteristics:
- Subsequent route transitions reuse the current document and update the view client-side.
- The
fetchAPI (orXMLHttpRequest) is used to communicate with the server without full-page reloads. - A client-side router (for example,
react-routerorvue-router) maps URL changes to view transitions. - Application state is typically held in memory rather than stored per request.
Benefits:
- Smoother navigation after the initial load.
- The browser can avoid full-document reloads, though server load depends on the API, rendering, caching, and data-fetching architecture.
- Application-like interaction patterns, such as preserved state across route transitions.
How search engines render JavaScript
The statement "Google cannot index JavaScript" is out of date. The practical situation is more nuanced:
- Googlebot executes JavaScript using an evergreen Chromium-based renderer. The bot fetches HTML first, and pages requiring JavaScript rendering are added to a separate render queue. The render queue has improved substantially since 2019, but indexing of JS-rendered content is typically slower than indexing of server-rendered HTML.
- Rendering is separate from crawling. This two-phase model means JS-rendered content can appear in the index later than its server-rendered counterpart, which is a disadvantage for time-sensitive content or highly competitive queries.
- Crawler capabilities and budgets vary. Do not assume every search engine or specialized crawler executes the same JavaScript successfully or on the same schedule.
- Many social and preview scrapers rely on response HTML. Return important Open Graph and other preview metadata from the server rather than depending on client-side injection.
For details, see Google's JavaScript SEO documentation and the Google Search Central documentation on rendering.
Rendering strategies
Modern frameworks allow different strategies to be used on different routes within the same application.
Server-side rendering (SSR)
The server renders the HTML for each request using the current application state. This produces indexable HTML on first response and reflects up-to-date data, at the cost of per-request server rendering.
Example with the Next.js App Router:
// app/products/[id]/page.tsx — a React Server Component, async by defaultexport default async function ProductPage({ params }) {const { id } = await params;const res = await fetch(`https://api.example.com/products/${id}`, {cache: 'no-store',});const product = await res.json();return (<div><h1>{product.title}</h1><p>{product.description}</p></div>);}
The App Router, introduced in Next.js 13, replaces the getServerSideProps data-fetching function of the Pages Router with async Server Components. cache: 'no-store' opts out of the framework's data cache to produce a fresh render on each request.
Static site generation (SSG)
HTML is produced at build time and served as static files. This is the cheapest option to host, produces the fastest first-byte response, and is suitable for content that does not change per request.
// app/blog/[slug]/page.tsxexport async function generateStaticParams() {const posts = await fetch('https://api.example.com/posts').then((r) =>r.json(),);return posts.map((p) => ({ slug: p.slug }));}export default async function Post({ params }) {const { slug } = await params;const post = await fetch(`https://api.example.com/posts/${slug}`).then((r) =>r.json(),);return <article>{post.body}</article>;}
Incremental static regeneration (ISR)
Pages are generated statically and cached, with a configurable revalidation interval. The cached HTML is served immediately; when the revalidation interval expires, the framework regenerates the page in the background on the next request. This combines SSG's serving cost with tunable freshness.
// app/categories/[slug]/page.tsxexport default async function Category({ params }) {const { slug } = await params;const res = await fetch(`https://api.example.com/categories/${slug}`, {next: { revalidate: 3600 },});const category = await res.json();return <CategoryView data={category} />;}
React Server Components with streaming
React Server Components execute on the server and produce a serialized component payload that a framework can use alongside server-rendered HTML. Combined with Suspense boundaries and a streaming-capable framework, the server can send ready HTML earlier while slower sections continue rendering. React Server Components and HTML streaming are related framework features, not interchangeable rendering strategies.
import { Suspense } from 'react';export default function Feed() {return (<><Header /><Suspense fallback={<FeedSkeleton />}><SlowFeed /></Suspense></>);}
Client-side rendering (CSR)
The HTML shell is served, and the full view is rendered by JavaScript in the browser. This remains appropriate for views that are not intended to be indexed, such as authenticated dashboards and internal tools, where SSR adds server cost without SEO benefit.
Choosing a strategy per route
The appropriate strategy depends on the route's data characteristics and indexing requirements. A typical allocation is:
| Use case | Recommended strategy | Rationale |
|---|---|---|
| E-commerce product page | ISR, short revalidation window | Prices and inventory change, but not per visit; cached HTML improves Largest Contentful Paint |
| Marketing site, documentation, blog | SSG | No per-request variability; suitable for CDN distribution |
| Dashboard behind authentication | Often CSR; SSR can still help first load | Usually not indexed; choose based on latency, personalization, and server cost |
| Personalized feed or homepage | RSC with streaming | Fast shell response; personalized content is streamed as it resolves |
| Search results page | SSR | Query-dependent output that should be indexable for long-tail queries |
| Real-time dashboard | CSR | Data changes more frequently than server HTML can be regenerated usefully |
| Breaking news article | SSR or short-window ISR | Freshness is important; SSR under traffic spikes, ISR otherwise |
Core Web Vitals comparison
The rendering strategy affects both perceived performance and the Core Web Vitals users experience. The directions below are common tendencies, not guaranteed results; payload size, caching, server location, device speed, hydration work, and data waterfalls can reverse them.
| Strategy | TTFB | LCP | Notes |
|---|---|---|---|
| CSR | Often low | Can be high | Fast shell response; meaningful content may wait for JS and data |
| SSR | Can be higher | Can be lower | Server work affects TTFB; content may become visible sooner |
| SSG | Often low | Often low | Cacheable HTML, subject to asset and hydration costs |
| Cached regeneration | Often low on cache hits | Often low | Miss and revalidation behavior is framework-specific |
| Streaming SSR | Early shell possible | Boundary-dependent | Slow boundaries need not hold back already completed HTML |
Tools such as PageSpeed Insights and WebPageTest provide lab measurements against a specific URL.
Framework coverage
Several frameworks support the strategies above:
- Next.js (React) — App Router is the current default. Supports SSR, SSG, ISR, and React Server Components with streaming.
- Remix (React) — Emphasizes web standards and nested routing with loader and action functions. Merged with React Router in 2024.
- Nuxt (Vue) — Supports SSR, SSG, ISR (via the Nitro server), and hybrid rendering.
- SvelteKit (Svelte) — Adapter-based; deploys as SSR, SSG, or edge functions depending on configuration.
- Astro — Island architecture. Ships zero JavaScript by default and hydrates only the components marked as interactive. Well suited to content-heavy sites with limited interactivity.
- SolidStart (Solid) — Architecturally similar to SvelteKit with Solid's fine-grained reactivity.
General guidance for new projects:
- Content-heavy sites with limited interactivity: Astro.
- React applications with a mix of interactive and indexable routes: Next.js or Remix.
- Vue applications: Nuxt.
A framework with SSR or SSG support is preferable to pure client-side rendering when SEO is a requirement, even for applications that would otherwise be implemented as a traditional SPA.
Common misconceptions
- "SSR means no JavaScript on the client." SSR produces server-rendered HTML, but the client still downloads and hydrates the JavaScript bundle to attach event handlers. The bundle size is generally comparable to the CSR equivalent.
- "CSR is not SEO-friendly." Googlebot indexes content rendered by JavaScript. The practical concerns are latency, indexing reliability, and compatibility with non-Google engines and social scrapers. For high-competition queries these concerns are significant; for long-tail content they may be acceptable.
- "SSR should be used for everything." SSR has a per-request CPU cost. For content that does not vary per request, SSG is substantially cheaper to serve. Defaulting to SSR when SSG would suffice increases hosting cost without benefit.
- "Hydration is free." Hydration re-executes the component tree on the client to attach event handlers. Large hydration trees can affect Interaction to Next Paint (INP) and other interactivity metrics. React Server Components reduce hydration cost by allowing portions of the tree to remain server-only.
Further reading
- Next.js App Router documentation
- React Server Components overview
- Google Search Central: JavaScript SEO basics
- web.dev: Rendering on the Web
- Remix Documentation
- Astro Documentation
- Nuxt Documentation
- SvelteKit Documentation