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.