Static generation pre-renders pages at build time. Here's how to use it effectively.
Basic Static Generation#
1// App Router: Server Components are static by default
2// app/page.tsx
3export default function HomePage() {
4 return (
5 <main>
6 <h1>Welcome</h1>
7 <p>This page is statically generated</p>
8 </main>
9 );
10}
11
12// With data fetching
13// app/posts/page.tsx
14async function getPosts() {
15 const res = await fetch('https://api.example.com/posts', {
16 cache: 'force-cache', // Static generation (default)
17 });
18 return res.json();
19}
20
21export default async function PostsPage() {
22 const posts = await getPosts();
23
24 return (
25 <ul>
26 {posts.map((post: Post) => (
27 <li key={post.id}>{post.title}</li>
28 ))}
29 </ul>
30 );
31}Dynamic Routes with generateStaticParams#
1// app/posts/[slug]/page.tsx
2interface PageProps {
3 params: { slug: string };
4}
5
6// Generate static paths at build time
7export async function generateStaticParams() {
8 const posts = await fetch('https://api.example.com/posts').then(r => r.json());
9
10 return posts.map((post: Post) => ({
11 slug: post.slug,
12 }));
13}
14
15// Fetch data for each page
16async function getPost(slug: string) {
17 const res = await fetch(`https://api.example.com/posts/${slug}`);
18 if (!res.ok) return null;
19 return res.json();
20}
21
22export default async function PostPage({ params }: PageProps) {
23 const post = await getPost(params.slug);
24
25 if (!post) {
26 notFound();
27 }
28
29 return (
30 <article>
31 <h1>{post.title}</h1>
32 <div>{post.content}</div>
33 </article>
34 );
35}
36
37// Generate metadata
38export async function generateMetadata({ params }: PageProps) {
39 const post = await getPost(params.slug);
40
41 return {
42 title: post?.title ?? 'Post Not Found',
43 description: post?.excerpt,
44 };
45}Multiple Dynamic Segments#
1// app/blog/[category]/[slug]/page.tsx
2interface PageProps {
3 params: {
4 category: string;
5 slug: string;
6 };
7}
8
9export async function generateStaticParams() {
10 const posts = await getAllPosts();
11
12 return posts.map((post) => ({
13 category: post.category,
14 slug: post.slug,
15 }));
16}
17
18export default async function PostPage({ params }: PageProps) {
19 const post = await getPost(params.category, params.slug);
20
21 return (
22 <article>
23 <span>{params.category}</span>
24 <h1>{post.title}</h1>
25 </article>
26 );
27}Incremental Static Regeneration (ISR)#
1// Revalidate every 60 seconds
2// app/products/page.tsx
3async function getProducts() {
4 const res = await fetch('https://api.example.com/products', {
5 next: { revalidate: 60 }, // ISR: regenerate every 60 seconds
6 });
7 return res.json();
8}
9
10export default async function ProductsPage() {
11 const products = await getProducts();
12
13 return (
14 <div>
15 <h1>Products</h1>
16 <ul>
17 {products.map((product: Product) => (
18 <li key={product.id}>{product.name}</li>
19 ))}
20 </ul>
21 </div>
22 );
23}
24
25// Or set at segment level
26export const revalidate = 60;
27
28export default async function Page() {
29 // All data fetches in this route use 60 second revalidation
30}On-Demand Revalidation#
1// app/api/revalidate/route.ts
2import { revalidatePath, revalidateTag } from 'next/cache';
3import { NextRequest, NextResponse } from 'next/server';
4
5export async function POST(request: NextRequest) {
6 const { path, tag, secret } = await request.json();
7
8 // Validate secret
9 if (secret !== process.env.REVALIDATION_SECRET) {
10 return NextResponse.json({ error: 'Invalid secret' }, { status: 401 });
11 }
12
13 // Revalidate by path
14 if (path) {
15 revalidatePath(path);
16 return NextResponse.json({ revalidated: true, path });
17 }
18
19 // Revalidate by tag
20 if (tag) {
21 revalidateTag(tag);
22 return NextResponse.json({ revalidated: true, tag });
23 }
24
25 return NextResponse.json({ error: 'Path or tag required' }, { status: 400 });
26}
27
28// Tag-based caching
29async function getPosts() {
30 const res = await fetch('https://api.example.com/posts', {
31 next: { tags: ['posts'] },
32 });
33 return res.json();
34}
35
36// Revalidate all posts
37revalidateTag('posts');
38
39// Revalidate specific path
40revalidatePath('/posts');
41revalidatePath('/posts/[slug]', 'page');Static Export#
1// next.config.js
2module.exports = {
3 output: 'export',
4 // Optional: Set base path
5 basePath: '/my-app',
6 // Optional: Trailing slashes
7 trailingSlash: true,
8};
9
10// Build static site
11// npm run build
12// Output in /out directoryHybrid Approaches#
1// Static page with client-side data
2// app/dashboard/page.tsx
3import { Suspense } from 'react';
4import { DynamicData } from './DynamicData';
5
6// Static shell
7export default function DashboardPage() {
8 return (
9 <div>
10 <h1>Dashboard</h1>
11 {/* Static content */}
12 <StaticSidebar />
13
14 {/* Dynamic content loaded client-side */}
15 <Suspense fallback={<LoadingSkeleton />}>
16 <DynamicData />
17 </Suspense>
18 </div>
19 );
20}
21
22// components/DynamicData.tsx
23'use client';
24
25import useSWR from 'swr';
26
27export function DynamicData() {
28 const { data, error } = useSWR('/api/stats', fetcher);
29
30 if (error) return <div>Error loading data</div>;
31 if (!data) return <div>Loading...</div>;
32
33 return <div>Live stats: {data.value}</div>;
34}MDX Content#
1// next.config.mjs
2import createMDX from '@next/mdx';
3
4const withMDX = createMDX({
5 extension: /\.mdx?$/,
6 options: {
7 remarkPlugins: [],
8 rehypePlugins: [],
9 },
10});
11
12export default withMDX({
13 pageExtensions: ['js', 'jsx', 'ts', 'tsx', 'md', 'mdx'],
14});1// app/posts/[slug]/page.tsx
2import { compileMDX } from 'next-mdx-remote/rsc';
3import fs from 'fs';
4import path from 'path';
5
6const postsDirectory = path.join(process.cwd(), 'content/posts');
7
8export async function generateStaticParams() {
9 const files = fs.readdirSync(postsDirectory);
10
11 return files.map((filename) => ({
12 slug: filename.replace(/\.mdx$/, ''),
13 }));
14}
15
16async function getPost(slug: string) {
17 const filePath = path.join(postsDirectory, `${slug}.mdx`);
18 const source = fs.readFileSync(filePath, 'utf8');
19
20 const { content, frontmatter } = await compileMDX({
21 source,
22 options: { parseFrontmatter: true },
23 });
24
25 return { content, frontmatter };
26}
27
28export default async function PostPage({ params }: { params: { slug: string } }) {
29 const { content, frontmatter } = await getPost(params.slug);
30
31 return (
32 <article>
33 <h1>{frontmatter.title}</h1>
34 {content}
35 </article>
36 );
37}Performance Optimization#
1// Preload critical data
2import { preload } from 'react-dom';
3
4export default async function Layout({ children }) {
5 preload('/fonts/inter.woff2', { as: 'font' });
6
7 return (
8 <html>
9 <body>{children}</body>
10 </html>
11 );
12}
13
14// Image optimization
15import Image from 'next/image';
16
17export default function Page() {
18 return (
19 <Image
20 src="/hero.jpg"
21 alt="Hero image"
22 width={1200}
23 height={600}
24 priority // LCP image
25 placeholder="blur"
26 blurDataURL="data:image/jpeg;base64,..."
27 />
28 );
29}
30
31// Static import for small images
32import heroImage from '../public/hero.jpg';
33
34<Image src={heroImage} alt="Hero" placeholder="blur" />Build Output#
1# Build and analyze
2npm run build
3
4# Output shows static/dynamic pages
5Route (app) Size First Load JS
6┌ ○ / 5.2 kB 89 kB
7├ ○ /about 2.1 kB 86 kB
8├ ● /posts/[slug] 3.4 kB 87 kB
9│ ├ /posts/first-post
10│ └ /posts/second-post
11└ λ /api/revalidate 0 B 0 B
12
13○ (Static) prerendered as static content
14● (SSG) prerendered as static HTML (uses generateStaticParams)
15λ (Dynamic) server-rendered on demandBest Practices#
Static Generation:
✓ Use for content that doesn't change often
✓ Pre-render as many pages as possible
✓ Use ISR for frequently updated content
✓ Implement on-demand revalidation
Performance:
✓ Optimize images with next/image
✓ Preload critical assets
✓ Use static export when possible
✓ Minimize client-side JavaScript
Content:
✓ Use MDX for rich content
✓ Generate metadata statically
✓ Create sitemaps at build time
✓ Handle 404 pages properly
Conclusion#
Static generation provides the best performance by pre-rendering pages at build time. Use ISR for content that updates periodically, on-demand revalidation for immediate updates, and client-side fetching for user-specific data. This hybrid approach gives you the best of both worlds.