Server-Side Rendering (SSR): Dynamic Content with SEO

Server-Side Rendering (SSR) generates HTML on the server for each request, making it perfect for dynamic content that needs to be fresh and SEO-friendly. Unlike Static Site Generation, SSR pages are rendered at request time, allowing for personalized content and real-time data.

How SSR Works

SSR Flow in Next.js 15

When to Use SSR

Perfect For:

  • E-commerce Sites: Product pages with real-time inventory and pricing
  • News Websites: Articles with breaking news and live updates
  • Social Media: User feeds with personalized content
  • Dashboard Applications: Real-time data and user-specific views
  • Search Results: Dynamic content based on user queries
  • User Authentication: Pages that depend on user session state

Not Ideal For:

  • Static Content: Blogs, documentation, marketing pages
  • High-Traffic Static Pages: Where performance is critical
  • Simple Landing Pages: Where content rarely changes

Implementation with Next.js 15 App Router

Basic SSR Page

// app/dashboard/page.tsx
import { headers } from 'next/headers'
import { redirect } from 'next/navigation'
import { Metadata } from 'next'

interface User {
  id: string
  name: string
  email: string
  role: string
}

interface DashboardData {
  user: User
  recentActivity: Array<{
    id: string
    action: string
    timestamp: string
  }>
  stats: {
    totalOrders: number
    totalRevenue: number
    pendingOrders: number
  }
}

// Generate metadata dynamically
export async function generateMetadata(): Promise<Metadata> {
  const headersList = await headers()
  const authToken = headersList.get('authorization')
  
  if (!authToken) {
    return {
      title: 'Login Required',
      description: 'Please log in to access your dashboard',
    }
  }
  
  try {
    const user = await fetch('https://api.example.com/me', {
      headers: { 'Authorization': authToken },
      cache: 'no-store',
    }).then(res => res.json())
    
    return {
      title: `${user.name}'s Dashboard`,
      description: `Welcome to your personalized dashboard`,
    }
  } catch {
    return {
      title: 'Dashboard',
      description: 'Your personalized dashboard',
    }
  }
}

// This function runs on every request
export default async function DashboardPage() {
  const headersList = await headers()
  const authToken = headersList.get('authorization')
  
  if (!authToken) {
    redirect('/login')
  }
  
  // Fetch user-specific data on each request
  const data: DashboardData = await fetch('https://api.example.com/dashboard', {
    headers: {
      'Authorization': authToken,
    },
    cache: 'no-store', // Disable caching for fresh data
  }).then(res => res.json())
  
  return (
    <div className="max-w-6xl mx-auto py-8 px-4">
      <h1 className="text-3xl font-bold mb-8">
        Welcome back, {data.user.name}!
      </h1>
      
      {/* Stats Grid */}
      <div className="grid md:grid-cols-3 gap-6 mb-8">
        <div className="bg-white p-6 rounded-lg shadow">
          <h3 className="text-lg font-semibold text-gray-600">Total Orders</h3>
          <p className="text-3xl font-bold text-blue-600">{data.stats.totalOrders}</p>
        </div>
        <div className="bg-white p-6 rounded-lg shadow">
          <h3 className="text-lg font-semibold text-gray-600">Total Revenue</h3>
          <p className="text-3xl font-bold text-green-600">
            ${data.stats.totalRevenue.toLocaleString()}
          </p>
        </div>
        <div className="bg-white p-6 rounded-lg shadow">
          <h3 className="text-lg font-semibold text-gray-600">Pending Orders</h3>
          <p className="text-3xl font-bold text-orange-600">{data.stats.pendingOrders}</p>
        </div>
      </div>
      
      {/* Recent Activity */}
      <div className="bg-white p-6 rounded-lg shadow">
        <h2 className="text-xl font-semibold mb-4">Recent Activity</h2>
        <div className="space-y-3">
          {data.recentActivity.map((activity) => (
            <div key={activity.id} className="flex justify-between items-center py-2 border-b">
              <span>{activity.action}</span>
              <time className="text-sm text-gray-500">
                {new Date(activity.timestamp).toLocaleString()}
              </time>
            </div>
          ))}
        </div>
      </div>
    </div>
  )
}

E-commerce Product Page with SSR

// app/products/[id]/page.tsx
import { Metadata } from 'next'
import { notFound } from 'next/navigation'

interface Product {
  id: string
  name: string
  description: string
  price: number
  stock: number
  images: string[]
  category: string
  variants: Array<{
    id: string
    name: string
    price: number
    stock: number
  }>
}

interface PageProps {
  params: { id: string }
  searchParams: { [key: string]: string | string[] | undefined }
}

export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
  try {
    const product = await fetch(`https://api.example.com/products/${params.id}`, {
      cache: 'no-store',
    }).then(res => res.json())
    
    return {
      title: `${product.name} - Our Store`,
      description: product.description,
      openGraph: {
        images: product.images,
      },
    }
  } catch {
    return {
      title: 'Product Not Found',
    }
  }
}

export default async function ProductPage({ params, searchParams }: PageProps) {
  try {
    // Fetch real-time product data
    const product: Product = await fetch(`https://api.example.com/products/${params.id}`, {
      cache: 'no-store', // Always fetch fresh data
    }).then(res => res.json())
    
    // Get user's location for pricing
    const userCountry = searchParams.country as string || 'US'
    const pricing = await fetch(`https://api.example.com/pricing/${product.id}?country=${userCountry}`, {
      cache: 'no-store',
    }).then(res => res.json())
    
    return (
      <div className="max-w-6xl mx-auto py-8 px-4">
        <div className="grid md:grid-cols-2 gap-8">
          <div>
            <img 
              src={product.images[0]} 
              alt={product.name}
              className="w-full rounded-lg"
            />
          </div>
          <div>
            <h1 className="text-3xl font-bold mb-4">{product.name}</h1>
            <p className="text-2xl font-semibold text-green-600 mb-4">
              {pricing.currency} {pricing.price.toFixed(2)}
            </p>
            <p className="text-gray-600 mb-4">{product.description}</p>
            
            {/* Stock Status */}
            <div className="mb-6">
              {product.stock > 0 ? (
                <span className="text-green-600 font-semibold">
                  In Stock ({product.stock} available)
                </span>
              ) : (
                <span className="text-red-600 font-semibold">Out of Stock</span>
              )}
            </div>
            
            {/* Variants */}
            {product.variants.length > 0 && (
              <div className="mb-6">
                <h3 className="font-semibold mb-2">Variants:</h3>
                <div className="space-y-2">
                  {product.variants.map((variant) => (
                    <div key={variant.id} className="flex justify-between items-center p-2 border rounded">
                      <span>{variant.name}</span>
                      <span className="font-semibold">
                        {pricing.currency} {variant.price.toFixed(2)}
                      </span>
                    </div>
                  ))}
                </div>
              </div>
            )}
            
            <button 
              className="bg-blue-600 text-white px-6 py-3 rounded-lg hover:bg-blue-700 transition-colors disabled:opacity-50"
              disabled={product.stock === 0}
            >
              {product.stock > 0 ? 'Add to Cart' : 'Out of Stock'}
            </button>
          </div>
        </div>
      </div>
    )
  } catch {
    notFound()
  }
}

Search Results Page with Streaming

// app/search/page.tsx
import { Suspense } from 'react'
import { Metadata } from 'next'

interface SearchResult {
  id: string
  title: string
  description: string
  url: string
  category: string
  relevance: number
}

interface PageProps {
  searchParams: { [key: string]: string | string[] | undefined }
}

// Async component for search results
async function SearchResults({ query }: { query: string }) {
  // Simulate search delay
  await new Promise(resolve => setTimeout(resolve, 100))
  
  const results: SearchResult[] = await fetch(`https://api.example.com/search?q=${encodeURIComponent(query)}`, {
    cache: 'no-store',
  }).then(res => res.json())
  
  return (
    <div className="space-y-4">
      {results.map((result) => (
        <div key={result.id} className="border-b pb-4">
          <h3 className="text-lg font-semibold">
            <a href={result.url} className="text-blue-600 hover:underline">
              {result.title}
            </a>
          </h3>
          <p className="text-gray-600 mt-1">{result.description}</p>
          <div className="flex items-center mt-2 text-sm text-gray-500">
            <span>{result.url}</span>
            <span className="mx-2">•</span>
            <span>{result.category}</span>
          </div>
        </div>
      ))}
    </div>
  )
}

export async function generateMetadata({ searchParams }: PageProps): Promise<Metadata> {
  const query = searchParams.q as string || ''
  
  return {
    title: query ? `Search Results for "${query}"` : 'Search',
    description: query ? `Search results for "${query}"` : 'Search our content',
  }
}

export default async function SearchPage({ searchParams }: PageProps) {
  const query = searchParams.q as string || ''
  
  return (
    <div className="max-w-4xl mx-auto py-8 px-4">
      <h1 className="text-3xl font-bold mb-8">
        Search Results for "{query}"
      </h1>
      
      {query ? (
        <Suspense fallback={
          <div className="space-y-4">
            {[...Array(5)].map((_, i) => (
              <div key={i} className="border-b pb-4 animate-pulse">
                <div className="h-6 bg-gray-200 rounded mb-2"></div>
                <div className="h-4 bg-gray-200 rounded mb-2"></div>
                <div className="h-3 bg-gray-200 rounded w-1/3"></div>
              </div>
            ))}
          </div>
        }>
          <SearchResults query={query} />
        </Suspense>
      ) : (
        <div className="text-center text-gray-600">
          Enter a search term to get started
        </div>
      )}
    </div>
  )
}

Advanced SSR Patterns

User Authentication with Middleware

// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
  const authToken = request.cookies.get('auth-token')
  const isProtectedRoute = request.nextUrl.pathname.startsWith('/dashboard')
  
  if (isProtectedRoute && !authToken) {
    return NextResponse.redirect(new URL('/login', request.url))
  }
  
  return NextResponse.next()
}

export const config = {
  matcher: ['/dashboard/:path*', '/profile/:path*'],
}
// lib/auth.ts
import { cookies } from 'next/headers'

interface User {
  id: string
  name: string
  email: string
  role: string
}

export async function getCurrentUser(): Promise<User | null> {
  const cookieStore = await cookies()
  const token = cookieStore.get('auth-token')?.value
  
  if (!token) {
    return null
  }
  
  try {
    const response = await fetch('https://api.example.com/me', {
      headers: {
        'Authorization': `Bearer ${token}`,
      },
      cache: 'no-store',
    })
    
    if (!response.ok) {
      return null
    }
    
    return response.json()
  } catch {
    return null
  }
}

// app/profile/page.tsx
import { getCurrentUser } from '@/lib/auth'
import { redirect } from 'next/navigation'
import { Metadata } from 'next'

export async function generateMetadata(): Promise<Metadata> {
  const user = await getCurrentUser()
  
  if (!user) {
    return {
      title: 'Login Required',
      description: 'Please log in to view your profile',
    }
  }
  
  return {
    title: `${user.name}'s Profile`,
    description: `Profile page for ${user.name}`,
  }
}

export default async function ProfilePage() {
  const user = await getCurrentUser()
  
  if (!user) {
    redirect('/login')
  }
  
  // Fetch user-specific data
  const userData = await fetch(`https://api.example.com/users/${user.id}/profile`, {
    cache: 'no-store',
  }).then(res => res.json())
  
  return (
    <div className="max-w-4xl mx-auto py-8 px-4">
      <h1 className="text-3xl font-bold mb-8">Profile</h1>
      <div className="bg-white p-6 rounded-lg shadow">
        <h2 className="text-xl font-semibold mb-4">Personal Information</h2>
        <div className="space-y-4">
          <div>
            <label className="block text-sm font-medium text-gray-700">Name</label>
            <p className="mt-1">{userData.name}</p>
          </div>
          <div>
            <label className="block text-sm font-medium text-gray-700">Email</label>
            <p className="mt-1">{userData.email}</p>
          </div>
          <div>
            <label className="block text-sm font-medium text-gray-700">Member Since</label>
            <p className="mt-1">
              {new Date(userData.createdAt).toLocaleDateString()}
            </p>
          </div>
        </div>
      </div>
    </div>
  )
}

Real-time Data with SSR

// app/weather/[city]/page.tsx
import { Metadata } from 'next'

interface WeatherData {
  city: string
  temperature: number
  condition: string
  humidity: number
  windSpeed: number
  updatedAt: string
}

interface PageProps {
  params: { city: string }
}

export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
  const weather = await fetch(`https://api.example.com/weather/${params.city}`, {
    cache: 'no-store',
  }).then(res => res.json())
  
  return {
    title: `Weather in ${weather.city}`,
    description: `Current weather in ${weather.city}: ${weather.temperature}°C, ${weather.condition}`,
  }
}

export default async function WeatherPage({ params }: PageProps) {
  const weather: WeatherData = await fetch(`https://api.example.com/weather/${params.city}`, {
    cache: 'no-store',
    next: { revalidate: 300 }, // Revalidate every 5 minutes
  }).then(res => res.json())
  
  return (
    <div className="max-w-2xl mx-auto py-8 px-4">
      <h1 className="text-3xl font-bold mb-8">Weather in {weather.city}</h1>
      
      <div className="bg-white p-6 rounded-lg shadow">
        <div className="text-center">
          <div className="text-6xl font-bold text-blue-600 mb-4">
            {weather.temperature}°C
          </div>
          <div className="text-xl text-gray-600 mb-6">{weather.condition}</div>
          
          <div className="grid grid-cols-2 gap-4">
            <div>
              <span className="text-sm text-gray-500">Humidity</span>
              <p className="font-semibold">{weather.humidity}%</p>
            </div>
            <div>
              <span className="text-sm text-gray-500">Wind Speed</span>
              <p className="font-semibold">{weather.windSpeed} km/h</p>
            </div>
          </div>
          
          <p className="text-sm text-gray-500 mt-4">
            Last updated: {new Date(weather.updatedAt).toLocaleString()}
          </p>
        </div>
      </div>
    </div>
  )
}

Performance Considerations

Caching Strategies

// Different caching approaches for SSR
export default async function Page() {
  // No caching - always fresh data
  const freshData = await fetch('https://api.example.com/data', {
    cache: 'no-store',
  }).then(res => res.json())
  
  // Cache for 1 hour
  const cachedData = await fetch('https://api.example.com/data', {
    next: { revalidate: 3600 },
  }).then(res => res.json())
  
  // Force cache revalidation
  const revalidatedData = await fetch('https://api.example.com/data', {
    next: { revalidate: 0 },
  }).then(res => res.json())
  
  return <div>{/* Your content */}</div>
}

Error Handling with Error Boundaries

// app/error.tsx
'use client'

import { useEffect } from 'react'

export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string }
  reset: () => void
}) {
  useEffect(() => {
    console.error(error)
  }, [error])

  return (
    <div className="text-center py-8">
      <h2 className="text-2xl font-bold text-red-600 mb-4">
        Something went wrong!
      </h2>
      <p className="text-gray-600 mb-4">
        {error.message || 'An unexpected error occurred'}
      </p>
      <button
        onClick={reset}
        className="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700"
      >
        Try again
      </button>
    </div>
  )
}

// app/products/[id]/page.tsx
export default async function ProductPage({ params }: { params: { id: string } }) {
  try {
    const product = await fetch(`https://api.example.com/products/${params.id}`, {
      cache: 'no-store',
    }).then(res => {
      if (!res.ok) {
        throw new Error(`Product not found: ${res.status}`)
      }
      return res.json()
    })
    
    return <div>{/* Product content */}</div>
  } catch (error) {
    // Handle different types of errors
    if (error instanceof Error && error.message.includes('not found')) {
      notFound()
    }
    
    // Re-throw to trigger error boundary
    throw error
  }
}

Best Practices

1. Optimize Data Fetching

// Parallel data fetching
export default async function Dashboard() {
  const [user, orders, analytics] = await Promise.all([
    fetch('https://api.example.com/user', { cache: 'no-store' }).then(res => res.json()),
    fetch('https://api.example.com/orders', { cache: 'no-store' }).then(res => res.json()),
    fetch('https://api.example.com/analytics', { cache: 'no-store' }).then(res => res.json()),
  ])
  
  return <div>{/* Your content */}</div>
}

2. Use Suspense for Better UX

// Use Suspense for better UX
import { Suspense } from 'react'

export default function Page() {
  return (
    <div>
      <h1>Dashboard</h1>
      <Suspense fallback={<div>Loading user data...</div>}>
        <UserData />
      </Suspense>
      <Suspense fallback={<div>Loading orders...</div>}>
        <OrdersList />
      </Suspense>
    </div>
  )
}

3. Security Considerations

// Sanitize user input
export default async function SearchPage({ searchParams }: { searchParams: { q: string } }) {
  const query = searchParams.q?.trim() || ''
  
  // Validate and sanitize query
  if (query.length < 2) {
    return <div>Search query too short</div>
  }
  
  const results = await fetch(`https://api.example.com/search?q=${encodeURIComponent(query)}`, {
    cache: 'no-store',
  }).then(res => res.json())
  
  return <div>{/* Results */}</div>
}

SSR is perfect for applications that need fresh, personalized content while maintaining excellent SEO. By rendering on the server, you get the best of both worlds: dynamic content and search engine visibility.