Back to Blog
Next.jsSSGStatic GenerationPerformance

Static Site Generation with Next.js

Master SSG in Next.js. From static pages to dynamic routes to incremental regeneration.

B
Bootspring Team
Engineering
June 1, 2021
6 min read

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 directory

Hybrid 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 demand

Best 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.

Share this article

Help spread the word about Bootspring