Next.js Static Site Generation (SSG)

Static Site Generation means Next.js builds your pages into plain HTML files at build time — before any user visits your site. Those HTML files sit on a server and get sent out instantly when someone requests them. No database query happens, no server code runs per request. The page is already built and ready.

The Newspaper Analogy

Imagine a newspaper. The printing press runs at 2 AM, prints thousands of copies, and by morning every reader gets the same paper instantly. No one waits while the press prints their copy on demand.

SSG works exactly this way. Next.js is the printing press. Your build step runs once, generates all pages as HTML files, and every visitor gets a pre-printed copy immediately. This is why statically generated sites feel so fast.

Traditional Server Rendering:
User requests /blog/my-post
  → Server wakes up
  → Server queries database
  → Server builds HTML
  → Server sends response
  → User sees page   (might take 500ms–2s)

Static Site Generation:
User requests /blog/my-post
  → CDN finds pre-built HTML file
  → Sends file immediately
  → User sees page   (might take 50–100ms)

How SSG Works in Next.js App Router

In the App Router, pages are statically generated by default when they do not opt into dynamic behavior. If your page fetches data at build time using the default fetch settings, Next.js pre-renders it into static HTML during npm run build.

// app/about/page.js
// No dynamic data — this is fully static
export default function AboutPage() {
  return (
    <main>
      <h1>About Us</h1>
      <p>We build great software.</p>
    </main>
  );
}

This page has no data fetching at all. Next.js turns it into a static HTML file during the build. Done.

Static Pages With Data

Static pages can still include data — that data just gets fetched once at build time, not on every request.

// app/blog/page.js
async function getPosts() {
  const res = await fetch("https://api.example.com/posts", {
    cache: "force-cache",  // Default behavior — cache the response
  });
  return res.json();
}

export default async function BlogPage() {
  const posts = await getPosts();

  return (
    <main>
      <h1>Blog</h1>
      <ul>
        {posts.map((post) => (
          <li key={post.id}>
            <a href={`/blog/${post.slug}`}>{post.title}</a>
          </li>
        ))}
      </ul>
    </main>
  );
}

At build time: Next.js calls getPosts(), gets the list, builds the HTML page with all post titles already inside it. At request time: the browser receives the finished HTML. No API call happens per visitor.

Generating Dynamic Routes Statically: generateStaticParams

Blog posts have individual pages like /blog/hello-world and /blog/nextjs-tutorial. These URLs are dynamic — the slug changes per post. To generate all these pages statically, use generateStaticParams.

// app/blog/[slug]/page.js

// Step 1: Tell Next.js all possible slugs at build time
export async function generateStaticParams() {
  const posts = await fetch("https://api.example.com/posts").then((r) => r.json());

  return posts.map((post) => ({
    slug: post.slug,
  }));
}

// Step 2: Fetch the individual post's content
async function getPost(slug) {
  const res = await fetch(`https://api.example.com/posts/${slug}`);
  return res.json();
}

// Step 3: Render the page
export default async function BlogPost({ params }) {
  const post = await getPost(params.slug);

  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </article>
  );
}

Diagram: generateStaticParams in Action

Build Time:
─────────────────────────────────────────────────
generateStaticParams() returns:
  [
    { slug: "hello-world" },
    { slug: "nextjs-tutorial" },
    { slug: "react-basics" }
  ]

Next.js builds:
  /blog/hello-world     → hello-world.html
  /blog/nextjs-tutorial → nextjs-tutorial.html
  /blog/react-basics    → react-basics.html

CDN stores all three files.
─────────────────────────────────────────────────
Request Time:
User visits /blog/hello-world
  → CDN sends hello-world.html instantly ✓

What Happens With Unknown Slugs

If a user requests a URL that was not included in generateStaticParams, Next.js returns a 404 by default. You can change this behavior with the dynamicParams export.

// app/blog/[slug]/page.js

// Allow pages NOT in generateStaticParams to be rendered on-demand
export const dynamicParams = true;  // default — generates unknown pages at request time

// Block any page not pre-generated
export const dynamicParams = false; // returns 404 for unknown slugs

Setting dynamicParams = true means unknown slugs get server-rendered once, then cached. This is the bridge between SSG and ISR.

Force Static: Preventing Accidental Dynamic Rendering

Use the dynamic export to force a page to always be static, even if it accidentally uses dynamic features.

// app/about/page.js
export const dynamic = "force-static";

export default function AboutPage() {
  return <p>This page is always static.</p>;
}

Identifying Static Pages After Build

Run npm run build and Next.js shows you exactly which pages are static vs dynamic in the terminal output.

npm run build output:
─────────────────────────────────────────
Route (app)              Size   
─────────────────────────────────────────
○ /                      1.2 kB    ← Static
○ /about                 890 B     ← Static
● /blog                  2.1 kB    ← Static with data
● /blog/[slug]           1.8 kB    ← Static (generateStaticParams)
ƒ /api/contact           0 B       ← Dynamic (server function)
─────────────────────────────────────────
○ = Static   ● = SSG   ƒ = Dynamic

Best Content Types for SSG

Great for SSG:                       Not suited for SSG:
────────────────────────────         ────────────────────────────
Marketing pages                      User dashboards
Blog posts and articles              Shopping cart pages
Documentation                        Real-time data (stock prices)
Product landing pages                User-specific content
Portfolio pages                      Pages that change per user
FAQs                                 Admin panels
Company pages (About, Contact)       Live chat interfaces

Metadata for Static Pages

Static pages support both static and dynamic metadata. Dynamic metadata runs at build time for SSG pages, so it does not add per-request cost.

// app/blog/[slug]/page.js

// Metadata also uses the same data fetch — Next.js deduplicates requests
export async function generateMetadata({ params }) {
  const post = await getPost(params.slug);
  return {
    title: post.title,
    description: post.excerpt,
  };
}

export async function generateStaticParams() {
  const posts = await fetch("https://api.example.com/posts").then(r => r.json());
  return posts.map((post) => ({ slug: post.slug }));
}

Next.js calls getPost for both generateMetadata and the page component, but it only makes one actual network request because of automatic request deduplication.

SSG With Multiple Dynamic Segments

// app/shop/[category]/[product]/page.js

export async function generateStaticParams() {
  const products = await fetchAllProducts();

  return products.map((product) => ({
    category: product.category,
    product: product.slug,
  }));
}

// This generates paths like:
//   /shop/electronics/laptop-pro
//   /shop/electronics/headphones-x
//   /shop/clothing/blue-shirt
//   /shop/clothing/red-hoodie

Caching Behavior for Static Data Fetches

fetch("https://api.example.com/data")
  // Default: cache: "force-cache" — fetched once at build, reused forever

fetch("https://api.example.com/data", { cache: "force-cache" })
  // Explicit static cache — same as default

fetch("https://api.example.com/data", { cache: "no-store" })
  // Never cached — makes this a dynamic page (not SSG)

fetch("https://api.example.com/data", { next: { revalidate: 3600 } })
  // Cached for 1 hour — this becomes ISR, not pure SSG

SSG vs Other Rendering Strategies

                   BUILD TIME    REQUEST TIME    FRESHNESS
SSG                ✓ Built       Fast (CDN)      Stale until rebuild
ISR                ✓ Built       Fast (CDN)      Refreshes automatically
SSR                ✗ Not built   Slower          Always fresh
Client-side        ✗ Not built   Slow (API call) Always fresh

Rebuilding Static Pages

When your content changes — a new blog post, updated product info — you need to trigger a new build for those changes to appear. Most teams automate this with CI/CD pipelines that run npm run build whenever content changes in a CMS.

For content that changes frequently, Incremental Static Regeneration is a better fit than full SSG, since it updates individual pages automatically without a full rebuild.

Summary

Static Site Generation gives you the fastest possible page delivery by building HTML files once at build time. Use generateStaticParams to pre-generate dynamic route pages. Default fetch behavior in Next.js uses SSG automatically. Pages without any dynamic data or cookies are static by default, and Next.js tells you exactly which pages are static after every build.

Leave a Comment