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:
- First request: Serves the statically generated page from build time
- After revalidation period: Next request triggers background regeneration while serving stale content
- Once regeneration completes: New version is cached and served to subsequent requests
- 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:
- Create a posts listing page that is statically generated
- Implement dynamic post pages using generateStaticParams
- Add pagination with 10 posts per page (generate first 5 pages)
- Implement ISR with 5-minute revalidation for post pages
- Create an API route for on-demand revalidation
- Add cache tags for granular revalidation control
- 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