Static Site Generation (SSG): Pre-built Performance
Static Site Generation (SSG) is a rendering method where pages are pre-built at build time and served as static HTML files. This approach offers the best performance and scalability for content that doesn't change frequently.
How SSG Works
SSG Flow in Next.js 15
When to Use SSG
Perfect For:
- Blogs and Documentation: Content that changes infrequently
- Marketing Sites: Landing pages, company websites
- Portfolio Sites: Personal or business portfolios
- E-commerce Product Pages: Product catalogs with stable information
- News Sites: Articles that don't need real-time updates
Not Ideal For:
- Real-time Data: Stock prices, live feeds
- User-specific Content: Personalized dashboards
- Frequently Changing Data: Social media feeds
- Dynamic Forms: User input that affects page content
Implementation with Next.js 15 App Router
Basic SSG Page
// app/blog/[slug]/page.tsx
import { Metadata } from 'next'
import { notFound } from 'next/navigation'
interface BlogPost {
slug: string
title: string
content: string
publishedAt: string
author: {
name: string
avatar: string
}
tags: string[]
}
interface PageProps {
params: { slug: string }
}
// Generate static params at build time
export async function generateStaticParams(): Promise<{ slug: string }[]> {
const posts = await fetch('https://api.example.com/posts').then(res => res.json())
return posts.map((post: BlogPost) => ({
slug: post.slug,
}))
}
// Generate metadata for each page
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
const post = await fetch(`https://api.example.com/posts/${params.slug}`).then(res => res.json())
return {
title: post.title,
description: post.content.substring(0, 160),
openGraph: {
title: post.title,
description: post.content.substring(0, 160),
type: 'article',
publishedTime: post.publishedAt,
authors: [post.author.name],
},
}
}
// This function runs at build time
export default async function BlogPostPage({ params }: PageProps) {
const post = await fetch(`https://api.example.com/posts/${params.slug}`).then(res => res.json())
if (!post) {
notFound()
}
return (
<article className="max-w-4xl mx-auto py-8 px-4">
<header className="mb-8">
<h1 className="text-4xl font-bold mb-4">{post.title}</h1>
<div className="flex items-center space-x-4 text-gray-600 mb-4">
<div className="flex items-center space-x-2">
<img
src={post.author.avatar}
alt={post.author.name}
className="w-8 h-8 rounded-full"
/>
<span>{post.author.name}</span>
</div>
<time>{new Date(post.publishedAt).toLocaleDateString()}</time>
</div>
<div className="flex space-x-2">
{post.tags.map((tag) => (
<span
key={tag}
className="px-3 py-1 bg-gray-100 text-gray-700 rounded-full text-sm"
>
{tag}
</span>
))}
</div>
</header>
<div className="prose lg:prose-xl max-w-none">
{post.content}
</div>
</article>
)
}
Blog Index with SSG
// app/blog/page.tsx
import Link from 'next/link'
import { Metadata } from 'next'
interface BlogPost {
slug: string
title: string
excerpt: string
publishedAt: string
author: {
name: string
}
}
export const metadata: Metadata = {
title: 'Blog Posts',
description: 'Latest articles and insights from our team',
}
// This runs at build time
export default async function BlogIndexPage() {
const posts = await fetch('https://api.example.com/posts').then(res => res.json())
return (
<div className="max-w-4xl mx-auto py-8 px-4">
<h1 className="text-3xl font-bold mb-8">Blog Posts</h1>
<div className="space-y-6">
{posts.map((post: BlogPost) => (
<article key={post.slug} className="border-b pb-6">
<Link href={`/blog/${post.slug}`} className="block group">
<h2 className="text-xl font-semibold group-hover:text-blue-600 transition-colors">
{post.title}
</h2>
</Link>
<p className="text-gray-600 mt-2">{post.excerpt}</p>
<div className="flex items-center space-x-4 text-sm text-gray-500 mt-2">
<span>By {post.author.name}</span>
<time>{new Date(post.publishedAt).toLocaleDateString()}</time>
</div>
</article>
))}
</div>
</div>
)
}
E-commerce Product Page
// app/products/[id]/page.tsx
import { Metadata } from 'next'
import { notFound } from 'next/navigation'
interface Product {
id: string
name: string
description: string
price: number
images: string[]
category: string
specifications: Record<string, string>
}
interface PageProps {
params: { id: string }
}
export async function generateStaticParams() {
const products = await fetch('https://api.example.com/products').then(res => res.json())
return products.map((product: Product) => ({
id: product.id,
}))
}
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
try {
const product = await fetch(`https://api.example.com/products/${params.id}`).then(res => res.json())
return {
title: `${product.name} - Our Store`,
description: product.description,
openGraph: {
images: product.images,
type: 'product',
},
}
} catch {
return {
title: 'Product Not Found',
}
}
}
export default async function ProductPage({ params }: PageProps) {
try {
const product = await fetch(`https://api.example.com/products/${params.id}`).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">
${product.price.toFixed(2)}
</p>
<p className="text-gray-600 mb-6">{product.description}</p>
{/* Specifications */}
<div className="mb-6">
<h3 className="font-semibold mb-2">Specifications:</h3>
<div className="space-y-2">
{Object.entries(product.specifications).map(([key, value]) => (
<div key={key} className="flex justify-between">
<span className="text-gray-600">{key}:</span>
<span>{value}</span>
</div>
))}
</div>
</div>
<button className="bg-blue-600 text-white px-6 py-3 rounded-lg hover:bg-blue-700 transition-colors">
Add to Cart
</button>
</div>
</div>
</div>
)
} catch {
notFound()
}
}
Advanced SSG Patterns
Conditional SSG with Draft Mode
// app/posts/[slug]/page.tsx
import { draftMode } from 'next/headers'
import { Metadata } from 'next'
import { notFound } from 'next/navigation'
interface Post {
slug: string
title: string
content: string
isDraft: boolean
publishedAt?: string
}
interface PageProps {
params: { slug: string }
}
export async function generateStaticParams() {
const { draftMode } = await import('next/headers')
// Only generate static pages for published posts
const posts = await fetch('https://api.example.com/posts').then(res => res.json())
const publishedPosts = posts.filter((post: Post) => !post.isDraft)
return publishedPosts.map((post: Post) => ({
slug: post.slug,
}))
}
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
const post = await fetch(`https://api.example.com/posts/${params.slug}`).then(res => res.json())
return {
title: post.title,
description: post.content.substring(0, 160),
}
}
export default async function PostPage({ params }: PageProps) {
const { draftMode } = await import('next/headers')
// Handle draft mode for unpublished posts
const post = await fetch(`https://api.example.com/posts/${params.slug}`).then(res => res.json())
if (post.isDraft && !draftMode().isEnabled) {
notFound()
}
return (
<article className="max-w-4xl mx-auto py-8 px-4">
<h1 className="text-4xl font-bold mb-4">{post.title}</h1>
{post.publishedAt && (
<time className="text-gray-600 mb-8 block">
{new Date(post.publishedAt).toLocaleDateString()}
</time>
)}
<div className="prose lg:prose-xl max-w-none">
{post.content}
</div>
</article>
)
}
Data Fetching with Error Handling
// lib/posts.ts
interface Post {
slug: string
title: string
content: string
}
export async function getPosts(): Promise<Post[]> {
try {
const response = await fetch('https://api.example.com/posts', {
next: { revalidate: 3600 }, // Cache for 1 hour
})
if (!response.ok) {
throw new Error(`Failed to fetch posts: ${response.status}`)
}
return response.json()
} catch (error) {
console.error('Error fetching posts:', error)
return []
}
}
export async function getPost(slug: string): Promise<Post | null> {
try {
const response = await fetch(`https://api.example.com/posts/${slug}`, {
next: { revalidate: 3600 },
})
if (!response.ok) {
return null
}
return response.json()
} catch (error) {
console.error(`Error fetching post ${slug}:`, error)
return null
}
}
// app/blog/[slug]/page.tsx
import { getPost } from '@/lib/posts'
import { Metadata } from 'next'
import { notFound } from 'next/navigation'
interface PageProps {
params: { slug: string }
}
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
const post = await getPost(params.slug)
if (!post) {
return {
title: 'Post Not Found',
}
}
return {
title: post.title,
description: post.content.substring(0, 160),
}
}
export default async function BlogPostPage({ params }: PageProps) {
const post = await getPost(params.slug)
if (!post) {
notFound()
}
return (
<article className="max-w-4xl mx-auto py-8 px-4">
<h1 className="text-4xl font-bold mb-4">{post.title}</h1>
<div className="prose lg:prose-xl max-w-none">
{post.content}
</div>
</article>
)
}
Performance Benefits
Build Time Analysis
Runtime Performance
Best Practices
1. Optimize Build Time
// Only fetch necessary data
export async function generateStaticParams() {
const posts = await fetch('https://api.example.com/posts?fields=slug').then(res => res.json())
return posts.map((post: { slug: string }) => ({ slug: post.slug }))
}
2. Handle Large Datasets
// Paginate or limit data fetching
export async function generateStaticParams() {
const posts = await fetch('https://api.example.com/posts?limit=1000').then(res => res.json())
return posts.slice(0, 1000).map((post: { slug: string }) => ({ slug: post.slug }))
}
3. 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>
)
}
Deployment Considerations
Vercel Deployment
// vercel.json
{
"buildCommand": "npm run build",
"outputDirectory": ".next",
"framework": "nextjs"
}
Static Export (Optional)
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'export', // For static hosting
trailingSlash: true,
images: {
unoptimized: true
}
}
module.exports = nextConfig
Monitoring and Analytics
// Track build performance
export async function generateStaticParams() {
const startTime = Date.now()
const posts = await fetch('https://api.example.com/posts').then(res => res.json())
console.log(`Generated ${posts.length} static paths in ${Date.now() - startTime}ms`)
return posts.map((post: { slug: string }) => ({ slug: post.slug }))
}
SSG is perfect for content-heavy sites where performance and SEO are critical. By pre-building pages at build time, you get instant page loads and excellent search engine visibility while maintaining the flexibility of React components.