Back to Home

SSR for SPA: indexing setup

The article covers setting up Server-Side Rendering for indexing SPA applications by search engines. Covers Next.js, Nuxt.js, Angular Universal with code examples. Improving Core Web Vitals and organic traffic.

SSR for SPA indexing: React, Vue, Angular
Advertisement 728x90

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 |

|----------|-----|-----|--------------|-----|

Google AdInline article slot

| 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 |

Google AdInline article slot

| 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.

Google AdInline article slot

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 window or document.
  • 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

Advertisement 728x90

Read Next