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.