Server-Side Rendering for SPAs: Fixing Search Engine Indexing
Search crawlers like Googlebot and YandexBot often see empty HTML when crawling CSR-based SPAs. Without Server-Side Rendering (SSR), they don’t wait for JavaScript to execute—and leave pages unindexed. SSR solves this by generating fully rendered HTML on the server before sending it to the client. This accelerates FCP, LCP, and TTFB—boosting Core Web Vitals and organic visibility.
Rendering Approach Comparison
| Parameter | CSR | SSR | Prerendering | SSG |
|----------|-----|-----|--------------|-----|
| Rendering location | Browser | Server (per request) | Pre-built for specific URLs | Pre-built for all pages |
| FCP/LCP | Slow | Fast | Fast | Very fast |
| SEO | Poor | Excellent | Good | Excellent |
| Data freshness | Real-time | Real-time | Stale | Stale |
| Server load | Minimal | High | Low | Minimal |
SSR works best for dynamic pages—product catalogs, news feeds, dashboards. Prerendering suits infrequently updated URLs (e.g., marketing pages). SSG is ideal for static content like documentation or landing pages.
React + Next.js: Step-by-Step Implementation
Create a new project:
npx create-next-app@latest my-ssr-app
cd my-ssr-app
npm run dev
For dynamic pages, use getServerSideProps:
// pages/product/[id].jsx
export async function getServerSideProps(context) {
const { id } = context.params;
const res = await fetch(`https://api.example.com/products/${id}`);
const product = await res.json();
if (!res.ok) {
return { notFound: true };
}
return {
props: { product },
};
}
export default function ProductPage({ product }) {
return (
<main>
<h1>{product.name}</h1>
<p>{product.description}</p>
</main>
);
}
Add SEO-friendly meta tags:
import Head from 'next/head';
export default function ProductPage({ product }) {
return (
<>
<Head>
<title>{product.name} | Shop</title>
<meta name="description" content={product.shortDescription} />
<meta property="og:title" content={product.name} />
<meta property="og:image" content={product.imageUrl} />
</Head>
<main>
<h1>{product.name}</h1>
</main>
</>
);
}
Deployment: Vercel, Netlify, or Node.js with PM2.
Hydration Without Errors
Hydration is how client-side React “revives” the server-rendered HTML. Avoid mismatches:
- Keep server and client data in sync.
- Check the execution environment before using
windowordocument. - Monitor browser console warnings.
const isBrowser = typeof window !== 'undefined';
Vue + Nuxt.js: Server-Side Rendering
Initialize a project:
npx nuxi@latest init my-nuxt-app
cd my-nuxt-app
npm install
npm run dev
Fetch server-side data:
<!-- pages/product/[id].vue -->
<script setup>
const route = useRoute();
const { data: product, error } = await useFetch(
`/api/products/${route.params.id}`
);
if (error.value) {
throw createError({ statusCode: 404, message: 'Not found' });
}
useHead({
title: product.value?.name,
meta: [
{ name: 'description', content: product.value?.shortDescription },
],
});
</script>
<template>
<main>
<h1>{{ product?.name }}</h1>
<p>{{ product?.description }}</p>
</main>
</template>
Configure nuxt.config.ts:
export default defineNuxtConfig({
ssr: true,
app: {
head: {
htmlAttrs: { lang: 'en' },
link: [{ rel: 'canonical', href: 'https://example.com' }],
},
},
nitro: {
preset: 'node-server',
},
});
Angular Universal: SSR Integration
For Angular 17+:
ng add @angular/ssr
Define the server endpoint in server.ts:
import { APP_BASE_HREF } from '@angular/common';
import { CommonEngine } from '@angular/ssr';
import express from 'express';
// ... other imports
const server = express();
// ...
server.get('*', (req, res, next) => {
commonEngine
.render({
bootstrap: AppServerModule,
documentFilePath: join(browserDistFolder, 'index.html'),
url: req.originalUrl,
publicPath: browserDistFolder,
providers: [{ provide: APP_BASE_HREF, useValue: req.baseUrl }],
})
.then((html) => res.send(html))
.catch(next);
});
Build and serve:
npm run build:ssr
npm run serve:ssr
Safely detect platform:
import { isPlatformBrowser } from '@angular/common';
import { inject, PLATFORM_ID } from '@angular/core';
const platformId = inject(PLATFORM_ID);
if (isPlatformBrowser(platformId)) {
console.log(window.location.href);
}
Integrating with Headless CMS
For Next.js + Contentful GraphQL:
export async function getServerSideProps({ params }) {
const query = `
query GetArticle($slug: String!) {
articleCollection(where: { slug: $slug }) {
items {
title
body
seo {
title
description
}
}
}
}
`;
const res = await fetch(
`https://graphql.contentful.com/content/v1/spaces/${process.env.CONTENTFUL_SPACE_ID}`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${process.env.CONTENTFUL_TOKEN}`,
},
body: JSON.stringify({ query, variables: { slug: params.slug } }),
}
);
const { data } = await res.json();
// ...
}
Key Takeaways
- SSR delivers fully rendered HTML to crawlers—solving the core indexing problem of CSR apps.
- Next.js, Nuxt.js, and Angular Universal are battle-tested frameworks for production SSR.
- Hydration mismatches trigger full re-renders—always validate runtime environment.
- Core Web Vitals (FCP, LCP, TTFB) improve by 50–80% after switching to SSR.
- Add Redis caching to keep TTFB under 200 ms during traffic spikes.
— Editorial Team
No comments yet.