Next.js

Static Site Generation (SSG)

40 min Lesson 9 of 40

Static Site Generation (SSG) in Next.js

Static Site Generation (SSG) is one of the most powerful features in Next.js, allowing you to pre-render pages at build time. This lesson explores static rendering, dynamic parameters, Incremental Static Regeneration (ISR), and on-demand revalidation.

What is SSG? Static Site Generation creates HTML pages at build time that can be cached and served instantly from a CDN. This results in the fastest possible page loads and excellent SEO.

Static Rendering by Default

In the Next.js App Router, pages are statically generated by default unless they use dynamic features:

// app/about/page.tsx // This page is automatically statically generated export default function AboutPage() { return ( <div> <h1>About Us</h1> <p>We build amazing web applications.</p> </div> ); }

Pages with data fetching are also statically generated if they don't opt out of caching:

// app/posts/page.tsx // Statically generated at build time async function getPosts() { const res = await fetch('https://api.example.com/posts', { cache: 'force-cache' // Default behavior }); return res.json(); } export default async function PostsPage() { const posts = await getPosts(); return ( <div> <h1>All Posts</h1> {posts.map(post => ( <article key={post.id}> <h2>{post.title}</h2> <p>{post.excerpt}</p> </article> ))} </div> ); }

Dynamic Routes with generateStaticParams

For dynamic routes, use generateStaticParams to specify which pages to pre-render at build time:

// app/posts/[id]/page.tsx async function getPost(id: string) { const res = await fetch(`https://api.example.com/posts/${id}`); return res.json(); } // Generate static params at build time export async function generateStaticParams() { const posts = await fetch('https://api.example.com/posts').then(res => res.json() ); // Return array of params objects return posts.map((post: any) => ({ id: post.id.toString() })); } // This page is statically generated for each ID export default async function PostPage({ params }: { params: { id: string } }) { const post = await getPost(params.id); return ( <article> <h1>{post.title}</h1> <div>{post.content}</div> </article> ); }
Build Output: When you run `npm run build`, Next.js will show which routes are statically generated (○) and which are dynamic (λ). Look for the build output to verify your pages are being generated correctly.

Multiple Dynamic Segments

generateStaticParams works with multiple dynamic segments:

// app/blog/[category]/[slug]/page.tsx export async function generateStaticParams() { const posts = await fetch('https://api.example.com/posts').then(res => res.json() ); // Return all combinations of category and slug return posts.map((post: any) => ({ category: post.category, slug: post.slug })); } export default async function PostPage({ params }: { params: { category: string; slug: string } }) { const post = await fetch( `https://api.example.com/posts/${params.category}/${params.slug}` ).then(res => res.json()); return ( <article> <h1>{post.title}</h1> <p>Category: {params.category}</p> <div>{post.content}</div> </article> ); }

Nested Dynamic Routes

Handle parent-child relationships in dynamic routes:

// app/authors/[authorId]/posts/[postId]/page.tsx // Generate params for parent route // app/authors/[authorId]/posts/page.tsx export async function generateStaticParams() { const authors = await fetch('https://api.example.com/authors').then(res => res.json() ); return authors.map((author: any) => ({ authorId: author.id.toString() })); } // Generate params for child route // app/authors/[authorId]/posts/[postId]/page.tsx export async function generateStaticParams({ params: { authorId } }: { params: { authorId: string } }) { const posts = await fetch( `https://api.example.com/authors/${authorId}/posts` ).then(res => res.json()); return posts.map((post: any) => ({ postId: post.id.toString() })); } export default async function AuthorPostPage({ params }: { params: { authorId: string; postId: string } }) { const post = await fetch( `https://api.example.com/authors/${params.authorId}/posts/${params.postId}` ).then(res => res.json()); return ( <article> <h1>{post.title}</h1> <p>By Author #{params.authorId}</p> <div>{post.content}</div> </article> ); }

Incremental Static Regeneration (ISR)

ISR allows you to update static pages after build time without rebuilding the entire site:

// app/posts/[id]/page.tsx // Revalidate this page every 60 seconds export const revalidate = 60; async function getPost(id: string) { const res = await fetch(`https://api.example.com/posts/${id}`, { next: { revalidate: 60 } }); return res.json(); } export async function generateStaticParams() { const posts = await fetch('https://api.example.com/posts').then(res => res.json() ); return posts.map((post: any) => ({ id: post.id.toString() })); } export default async function PostPage({ params }: { params: { id: string } }) { const post = await getPost(params.id); return ( <article> <h1>{post.title}</h1> <p>Last updated: {new Date(post.updatedAt).toLocaleString()}</p> <div>{post.content}</div> </article> ); }
How ISR Works:
  1. First request: Serves the statically generated page from build time
  2. After revalidation period: Next request triggers background regeneration while serving stale content
  3. Once regeneration completes: New version is cached and served to subsequent requests
  4. If regeneration fails: Old cached version continues to be served

Time-Based Revalidation Options

1. Route Segment Revalidation

// app/blog/page.tsx // Revalidate every 30 minutes export const revalidate = 1800; export default async function BlogPage() { const posts = await fetch('https://api.example.com/posts').then(res => res.json() ); return ( <div> {posts.map(post => ( <article key={post.id}> <h2>{post.title}</h2> </article> ))} </div> ); }

2. Fetch-Level Revalidation

// Different revalidation times for different data sources async function getData() { // Revalidate posts every 60 seconds const posts = await fetch('https://api.example.com/posts', { next: { revalidate: 60 } }); // Revalidate comments every 30 seconds const comments = await fetch('https://api.example.com/comments', { next: { revalidate: 30 } }); return { posts: await posts.json(), comments: await comments.json() }; } export default async function Page() { const { posts, comments } = await getData(); return ( <div> <h1>Posts</h1> {/* Render posts and comments */} </div> ); }

3. Disable Revalidation

// Never revalidate - pure static generation export const revalidate = false; // Or use Infinity export const revalidate = Infinity;

On-Demand Revalidation

Trigger revalidation manually using revalidatePath() or revalidateTag():

// app/api/revalidate/route.ts import { revalidatePath, revalidateTag } from 'next/cache'; import { NextRequest } from 'next/server'; export async function POST(request: NextRequest) { const secret = request.nextUrl.searchParams.get('secret'); // Verify secret token if (secret !== process.env.REVALIDATE_SECRET) { return Response.json({ message: 'Invalid token' }, { status: 401 }); } const path = request.nextUrl.searchParams.get('path'); const tag = request.nextUrl.searchParams.get('tag'); try { if (path) { // Revalidate specific path revalidatePath(path); return Response.json({ revalidated: true, path, now: Date.now() }); } if (tag) { // Revalidate by tag revalidateTag(tag); return Response.json({ revalidated: true, tag, now: Date.now() }); } return Response.json( { message: 'Missing path or tag' }, { status: 400 } ); } catch (err) { return Response.json( { message: 'Error revalidating' }, { status: 500 } ); } }

Using cache tags for granular control:

// app/posts/[id]/page.tsx async function getPost(id: string) { const res = await fetch(`https://api.example.com/posts/${id}`, { next: { tags: ['posts', `post-${id}`] } }); return res.json(); } export default async function PostPage({ params }: { params: { id: string } }) { const post = await getPost(params.id); return ( <article> <h1>{post.title}</h1> <div>{post.content}</div> </article> ); } // Trigger revalidation: // POST /api/revalidate?secret=xxx&tag=post-123 // This revalidates only the post with ID 123 // POST /api/revalidate?secret=xxx&tag=posts // This revalidates all posts
Webhook Integration: Connect your CMS webhook to the revalidation API endpoint. When content is updated in your CMS, it automatically triggers revalidation of affected pages.

generateStaticParams with Pagination

Generate static pages for paginated content:

// app/posts/page/[pageNumber]/page.tsx const POSTS_PER_PAGE = 10; export async function generateStaticParams() { const totalPosts = await fetch('https://api.example.com/posts/count') .then(res => res.json()); const totalPages = Math.ceil(totalPosts / POSTS_PER_PAGE); // Generate pages 1 through totalPages return Array.from({ length: totalPages }, (_, i) => ({ pageNumber: (i + 1).toString() })); } async function getPosts(page: number) { const res = await fetch( `https://api.example.com/posts?page=${page}&limit=${POSTS_PER_PAGE}` ); return res.json(); } export default async function PostsPage({ params }: { params: { pageNumber: string } }) { const page = parseInt(params.pageNumber); const posts = await getPosts(page); return ( <div> <h1>Posts - Page {page}</h1> {posts.map(post => ( <article key={post.id}> <h2>{post.title}</h2> </article> ))} <nav> {page > 1 && ( <a href={`/posts/page/${page - 1}`}>Previous</a> )} <a href={`/posts/page/${page + 1}`}>Next</a> </nav> </div> ); }

Handling Missing Pages

Control what happens when a dynamic route doesn't have a pre-generated page:

// app/posts/[id]/page.tsx // Generate only the first 100 posts at build time export async function generateStaticParams() { const posts = await fetch('https://api.example.com/posts?limit=100') .then(res => res.json()); return posts.map((post: any) => ({ id: post.id.toString() })); } // Configure what happens for non-generated pages export const dynamicParams = true; // default - generate on-demand // export const dynamicParams = false; // return 404 for non-generated pages export default async function PostPage({ params }: { params: { id: string } }) { // This will be called for non-generated pages if dynamicParams = true const post = await fetch(`https://api.example.com/posts/${params.id}`) .then(res => { if (!res.ok) return null; return res.json(); }); if (!post) { return ( <div> <h1>Post not found</h1> </div> ); } return ( <article> <h1>{post.title}</h1> <div>{post.content}</div> </article> ); }
dynamicParams Options:
  • true (default): Non-generated pages are rendered on-demand and cached
  • false: Non-generated pages return 404

Metadata Generation for Static Pages

Generate metadata dynamically for each static page:

// app/posts/[id]/page.tsx import { Metadata } from 'next'; async function getPost(id: string) { const res = await fetch(`https://api.example.com/posts/${id}`); return res.json(); } // Generate metadata for each page export async function generateMetadata({ params }: { params: { id: string } }): Promise<Metadata> { const post = await getPost(params.id); return { title: post.title, description: post.excerpt, openGraph: { title: post.title, description: post.excerpt, images: [post.featuredImage], type: 'article', publishedTime: post.publishedAt, authors: [post.author.name] }, twitter: { card: 'summary_large_image', title: post.title, description: post.excerpt, images: [post.featuredImage] } }; } export async function generateStaticParams() { const posts = await fetch('https://api.example.com/posts') .then(res => res.json()); return posts.map((post: any) => ({ id: post.id.toString() })); } export default async function PostPage({ params }: { params: { id: string } }) { const post = await getPost(params.id); return ( <article> <h1>{post.title}</h1> <div>{post.content}</div> </article> ); }

Build Performance Optimization

Large Sites: Generating thousands of pages at build time can be slow. Consider these strategies:
  • Generate only popular pages at build time
  • Use ISR for less frequently accessed content
  • Set dynamicParams = true to generate other pages on-demand
  • Use parallel builds in your CI/CD pipeline
// Strategy: Generate top 1000 posts, render others on-demand export async function generateStaticParams() { // Only generate the most popular posts const posts = await fetch( 'https://api.example.com/posts?sortBy=views&limit=1000' ).then(res => res.json()); return posts.map((post: any) => ({ id: post.id.toString() })); } // Allow on-demand generation for other posts export const dynamicParams = true; // Cache on-demand pages for 1 hour export const revalidate = 3600;

Practice Exercise

Task: Build a blog with static generation:

  1. Create a posts listing page that is statically generated
  2. Implement dynamic post pages using generateStaticParams
  3. Add pagination with 10 posts per page (generate first 5 pages)
  4. Implement ISR with 5-minute revalidation for post pages
  5. Create an API route for on-demand revalidation
  6. Add cache tags for granular revalidation control
  7. Generate proper metadata for each post page

Requirements:

  • Use TypeScript with proper types
  • Implement error handling for missing posts
  • Set dynamicParams = true for posts not in first 100
  • Add proper SEO metadata for all pages
  • Verify build output shows static generation markers (○)

Bonus Challenge: Implement category pages with generateStaticParams that pre-generate all category/post combinations, and add a webhook endpoint to trigger revalidation when posts are updated.

Summary

  • Static Site Generation pre-renders pages at build time for maximum performance
  • Pages are static by default in the App Router unless they opt out
  • Use generateStaticParams to specify dynamic routes to pre-render
  • ISR allows updating static pages without full rebuilds using revalidate
  • Time-based revalidation can be set at route or fetch level
  • On-demand revalidation uses revalidatePath() and revalidateTag()
  • Cache tags enable granular control over which pages to revalidate
  • dynamicParams controls behavior for non-generated pages
  • Generate metadata dynamically for each static page using generateMetadata
  • Optimize build performance by generating only popular pages at build time