Back to Blog

Next.js Static Site Generation in 2026: ISR, generateStaticParams

Master Next.js static generation in 2026: generateStaticParams for dynamic routes, ISR with revalidate, on-demand revalidation via revalidatePath and revalidateTag, and PPR.

Viprasol Tech Team
13 min read
Updated 2027

Next.js Static Site Generation in 2026: ISR, generateStaticParams, and On-Demand Revalidation

Quick answer. Next.js App Router keeps static pages fresh three ways: time-based ISR via revalidate, on-demand revalidatePath/revalidateTag for instant cache invalidation, and Partial Prerendering that mixes static and dynamic in one page. Use generateStaticParams to pre-render dynamic routes at build time for sub-100ms TTFB.

Static generation delivers pre-rendered HTML from CDN edge servers โ€” sub-100ms TTFB, no database queries on each request, and infinite scalability at low cost. The challenge is keeping that static content fresh. Next.js App Router handles this through three mechanisms: time-based ISR (revalidate), on-demand revalidation (revalidatePath/revalidateTag), and Partial Prerendering (PPR) that mixes static and dynamic in the same page.

This post covers all three patterns: when to use each, generateStaticParams for pre-rendering dynamic routes, cache tagging for granular invalidation, and the ISR stale-while-revalidate behavior that trips up most teams.


The Three Rendering Modes

// Static (SSG): generated once at build time, never refreshes
// Use for: marketing pages, docs, blog posts (manually revalidated)
export const dynamic = "force-static";

// ISR: static + auto-refresh on a schedule
// Use for: product listings, news feeds, dashboards
export const revalidate = 3600; // Regenerate at most once per hour

// Dynamic: server-rendered on every request
// Use for: user dashboards, real-time data, auth-dependent pages
export const dynamic = "force-dynamic";

generateStaticParams: Pre-Rendering Dynamic Routes

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

// This tells Next.js which [slug] values to pre-render at build time
export async function generateStaticParams() {
  const posts = await db.blogPost.findMany({
    where: { status: "published" },
    select: { slug: true },
    orderBy: { publishedAt: "desc" },
    take: 200, // Pre-render latest 200 posts; others render on-demand
  });

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

// ISR: pages not in generateStaticParams are rendered on first request
// and then cached according to revalidate
export const revalidate = 3600; // Cache for 1 hour

// If a slug not in generateStaticParams is requested:
// - first request: render and cache
// - subsequent requests (within 1h): serve from cache
// - after 1h: serve stale, regenerate in background
export const dynamicParams = true; // Allow on-demand rendering (default)
// Set to false to 404 for slugs not in generateStaticParams

export default async function BlogPostPage({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { slug } = await params;
  const post = await db.blogPost.findUnique({
    where: { slug, status: "published" },
  });

  if (!post) notFound();
  return <BlogPost post={post} />;
}

// OpenGraph image (also statically generated)
export async function generateMetadata({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { slug } = await params;
  const post = await db.blogPost.findUnique({
    where: { slug },
    select: { title: true, excerpt: true, image: true },
  });

  return {
    title: post?.title,
    description: post?.excerpt,
    openGraph: {
      title: post?.title,
      description: post?.excerpt,
      images: [{ url: post?.image ?? "/og-default.jpg" }],
    },
  };
}

๐ŸŒ Looking for a Dev Team That Actually Delivers?

Most agencies sell you a project manager and assign juniors. Viprasol is different โ€” senior engineers only, direct Slack access, and a 5.0โ˜… Upwork record across 1000+ projects.

  • React, Next.js, Node.js, TypeScript โ€” production-grade stack
  • Fixed-price contracts โ€” no surprise invoices
  • Full source code ownership from day one
  • 90-day post-launch support included

ISR: Stale-While-Revalidate Behavior

// app/products/page.tsx
// ISR with 60-second revalidation

export const revalidate = 60; // Revalidate at most every 60 seconds

export default async function ProductsPage() {
  // This fetch is cached by Next.js for 60 seconds
  const products = await db.product.findMany({
    where: { active: true },
    orderBy: { sortOrder: "asc" },
  });

  return <ProductGrid products={products} />;
}

ISR timeline (important to understand):

T=0s:   First request โ†’ render & cache
T=30s:  Request โ†’ serve cached (fast, from CDN)
T=60s:  Revalidate period expires
T=61s:  Request โ†’ serve STALE (still fast) + trigger background regeneration
T=62s:  Background regeneration completes โ†’ cache updated
T=63s:  Request โ†’ serve fresh content

The key: after revalidation, the next request sees stale content while the background re-render happens. The request after that sees fresh content. This is intentional โ€” it avoids making users wait for regeneration.


Cache Tags: Granular Invalidation

Tags let you invalidate multiple pages that share the same data:

// app/products/[id]/page.tsx
import { unstable_cache } from "next/cache";

// Tag this fetch with "products" and specific product ID
const getProduct = unstable_cache(
  async (id: string) => {
    return db.product.findUnique({ where: { id } });
  },
  ["product-detail"],           // Cache key prefix
  {
    tags: ["products", `product-${id}`],  // Tags for invalidation
    revalidate: 3600,
  }
);

export default async function ProductPage({
  params,
}: {
  params: Promise<{ id: string }>;
}) {
  const { id } = await params;
  const product = await getProduct(id);
  if (!product) notFound();
  return <ProductDetail product={product} />;
}
// app/products/page.tsx (listing page โ€” also tagged)
import { unstable_cache } from "next/cache";

const getProducts = unstable_cache(
  async () => db.product.findMany({ where: { active: true } }),
  ["product-list"],
  { tags: ["products"], revalidate: 3600 }
);

export default async function ProductsPage() {
  const products = await getProducts();
  return <ProductGrid products={products} />;
}
// app/actions/products.ts โ€” on-demand revalidation after update
"use server";

import { revalidateTag, revalidatePath } from "next/cache";

export async function updateProduct(productId: string, data: ProductUpdateInput) {
  await db.product.update({ where: { id: productId }, data });

  // Invalidate all pages tagged with this product OR the products collection
  revalidateTag(`product-${productId}`); // โ†’ re-renders /products/[id] page
  revalidateTag("products");             // โ†’ re-renders /products listing

  // Alternatively, invalidate by path:
  // revalidatePath(`/products/${productId}`);
  // revalidatePath("/products", "page");
}

Next.js - Next.js Static Site Generation in 2026: ISR, generateStaticParams

๐Ÿš€ Senior Engineers. No Junior Handoffs. Ever.

You get the senior developer, not a project manager who relays your requirements to someone you never meet. Every Viprasol project has a senior lead from kickoff to launch.

  • MVPs in 4โ€“8 weeks, full platforms in 3โ€“5 months
  • Lighthouse 90+ performance scores standard
  • Works across US, UK, AU timezones
  • Free 30-min architecture review, no commitment

On-Demand Revalidation API Route

For webhooks (e.g., CMS publishes content โ†’ trigger revalidation):

// app/api/revalidate/route.ts
import { NextRequest, NextResponse } from "next/server";
import { revalidatePath, revalidateTag } from "next/cache";

export async function POST(req: NextRequest) {
  // Verify the request is from your CMS/webhook source
  const secret = req.headers.get("x-revalidate-secret");
  if (secret !== process.env.REVALIDATE_SECRET) {
    return NextResponse.json({ error: "Invalid secret" }, { status: 401 });
  }

  const body = await req.json();
  const { type, slug, tag } = body;

  try {
    if (tag) {
      // Invalidate by tag (most efficient โ€” only revalidates tagged pages)
      revalidateTag(tag);
      return NextResponse.json({ revalidated: true, tag });
    }

    if (type === "blog-post" && slug) {
      revalidatePath(`/blog/${slug}`);
      revalidatePath("/blog");       // Invalidate listing too
      return NextResponse.json({ revalidated: true, path: `/blog/${slug}` });
    }

    if (type === "all") {
      revalidatePath("/", "layout"); // Revalidate entire site
      return NextResponse.json({ revalidated: true, scope: "all" });
    }

    return NextResponse.json({ error: "Unknown type" }, { status: 400 });
  } catch (err) {
    return NextResponse.json({ error: "Revalidation failed" }, { status: 500 });
  }
}
## Trigger from CMS webhook or CI/CD:
curl -X POST https://yourdomain.com/api/revalidate \
  -H "Content-Type: application/json" \
  -H "x-revalidate-secret: your-secret" \
  -d '{"type": "blog-post", "slug": "nextjs-image-optimization"}'

Partial Prerendering (PPR)

PPR (stable in Next.js 15) renders a static shell at build time and streams dynamic parts at request time โ€” no loading.tsx flicker:

// next.config.ts
const config: NextConfig = {
  experimental: {
    ppr: true,        // Enable PPR
  },
};

// app/products/[id]/page.tsx
import { Suspense } from "react";

// The product shell (title, image, description) is static
// The "In stock" indicator and recommendations are dynamic
export default async function ProductPage({
  params,
}: {
  params: Promise<{ id: string }>;
}) {
  const { id } = await params;

  // This is fetched at build time (static part)
  const product = await db.product.findUnique({
    where: { id },
    select: { id: true, name: true, description: true, image: true, price: true },
  });

  if (!product) notFound();

  return (
    <div>
      {/* Static shell โ€” pre-rendered, served instantly */}
      <ProductHeader product={product} />
      <ProductDescription text={product.description} />

      {/* Dynamic parts โ€” streamed after static shell */}
      <Suspense fallback={<StockSkeleton />}>
        <StockIndicator productId={id} />
      </Suspense>

      <Suspense fallback={<RecommendationsSkeleton />}>
        <Recommendations productId={id} />
      </Suspense>
    </div>
  );
}

// StockIndicator reads live inventory โ€” opt into dynamic
async function StockIndicator({ productId }: { productId: string }) {
  const stock = await db.inventory.findUnique({ where: { productId } });
  return <StockBadge count={stock?.available ?? 0} />;
}

Caching Comparison Table

StrategyTTFBFreshnessUse Case
SSG (build-time only)<50msStale until redeployDocs, landing pages
ISR (time-based)<50ms (stale ok)Up to X seconds oldBlogs, product listings
On-demand revalidation<50msFresh after webhookCMS content, editorial
PPR<50ms (shell)Shell static, parts dynamicProduct pages with live stock
Dynamic (SSR)100โ€“500msAlways freshUser-specific, real-time

Cost and Timeline

ComponentTimelineCost (USD)
generateStaticParams setup0.5 day$300โ€“$500
ISR configuration + cache tag design0.5โ€“1 day$400โ€“$800
On-demand revalidation webhook0.5 day$300โ€“$500
PPR implementation1โ€“2 days$800โ€“$1,600
Full SSG/ISR/PPR strategy1โ€“2 weeks$5,000โ€“$10,000

More on This Topic


Inside Viprasol

We architect Next.js caching strategies for high-traffic applications โ€” from simple ISR blog pages through complex PPR product pages with live inventory. Our team has shipped Next.js sites serving millions of static pages with sub-100ms TTFB globally.

What we deliver:

  • generateStaticParams configuration for all dynamic routes
  • ISR revalidate period tuning per content type
  • Cache tag taxonomy for granular on-demand invalidation
  • Revalidation webhook for CMS integration (Contentful, Sanity, Strapi)
  • PPR implementation for pages with mixed static/dynamic content

Explore our web development services or contact us to optimize your Next.js caching strategy.

Next.js App Router Static Generation with generateStaticParams and revalidate: What the Docs Cover

If you are reading the Next.js App Router static generation generateStaticParams revalidate docs and still feel unsure where each piece fits, here is the practical mental model. The generateStaticParams function tells Next.js which dynamic route segments to prerender at build time, replacing the old getStaticPaths. Pair it with the route segment revalidate option to control Incremental Static Regeneration, so pages rebuild in the background after the interval you set. Together they cover what the Next.js App Router static rendering generateStaticParams revalidate docs describe as hybrid rendering: fully static HTML at build, refreshed on demand.

In practice, set revalidate per route, return only the params you genuinely want prerendered, and let dynamicParams handle the rest on first request. At Viprasol Tech, our senior engineers own these rendering decisions end to end, balancing build time, cache freshness, and infrastructure cost for each project.

Next.jsTypeScriptPerformanceSSGISRReact
Share this article:

About the Author

V

Viprasol Tech Team

Custom Software Development Specialists

The Viprasol Tech team specialises in algorithmic trading software, AI agent systems, and SaaS development. With 1000+ projects delivered across MT4/MT5 EAs, fintech platforms, and production AI systems, the team brings deep technical experience to every engagement.

MT4/MT5 EA DevelopmentAI Agent SystemsSaaS DevelopmentAlgorithmic Trading

Need a Modern Web Application?

From landing pages to complex SaaS platforms โ€” we build it all with Next.js and React.

Free consultation โ€ข No commitment โ€ข Response within 24 hours

Viprasol ยท Web Development

Need a custom web application built?

We build React and Next.js web applications with Lighthouse โ‰ฅ90 scores, mobile-first design, and full source code ownership. Senior engineers only โ€” from architecture through deployment.