Skip to main content
Back to Blog
Tutorials
3 min read
December 30, 2024

How to Build SSG and ISR Blog Patterns in Next.js

Master Static Site Generation and Incremental Static Regeneration in Next.js for blogs with dynamic routes, on-demand revalidation, and caching.

Ryel Banfield

Founder & Lead Developer

Static generation gives you the fastest possible page loads. ISR keeps content fresh without full rebuilds.

Static Blog with generateStaticParams

// app/blog/[slug]/page.tsx
import { notFound } from "next/navigation";

interface Post {
  slug: string;
  title: string;
  content: string;
  date: string;
  author: string;
}

async function getAllPosts(): Promise<Post[]> {
  // From CMS, database, or file system
  const res = await fetch(`${process.env.CMS_URL}/posts`, {
    next: { revalidate: 3600 },
  });
  return res.json();
}

async function getPost(slug: string): Promise<Post | null> {
  const res = await fetch(`${process.env.CMS_URL}/posts/${slug}`, {
    next: { revalidate: 3600 },
  });
  if (!res.ok) return null;
  return res.json();
}

// Generate static pages at build time
export async function generateStaticParams() {
  const posts = await getAllPosts();
  return posts.map((post) => ({ slug: post.slug }));
}

// Metadata
export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }) {
  const { slug } = await params;
  const post = await getPost(slug);
  if (!post) return {};

  return {
    title: post.title,
    description: post.content.slice(0, 155),
    openGraph: {
      title: post.title,
      type: "article",
      publishedTime: post.date,
      authors: [post.author],
    },
  };
}

export default async function BlogPostPage({ params }: { params: Promise<{ slug: string }> }) {
  const { slug } = await params;
  const post = await getPost(slug);
  if (!post) notFound();

  return (
    <article className="max-w-3xl mx-auto py-12">
      <h1 className="text-4xl font-bold">{post.title}</h1>
      <div className="flex items-center gap-2 text-sm text-muted-foreground mt-4">
        <span>{post.author}</span>
        <span>·</span>
        <time dateTime={post.date}>
          {new Date(post.date).toLocaleDateString("en-US", {
            year: "numeric",
            month: "long",
            day: "numeric",
          })}
        </time>
      </div>
      <div className="prose mt-8" dangerouslySetInnerHTML={{ __html: post.content }} />
    </article>
  );
}

ISR with Time-Based Revalidation

// app/blog/page.tsx
// Revalidate every 60 seconds
export const revalidate = 60;

export default async function BlogIndexPage() {
  const posts = await getAllPosts();

  return (
    <div className="max-w-3xl mx-auto py-12">
      <h1 className="text-3xl font-bold mb-8">Blog</h1>
      <div className="space-y-6">
        {posts.map((post) => (
          <article key={post.slug} className="border-b pb-6">
            <a href={`/blog/${post.slug}`} className="group">
              <h2 className="text-xl font-semibold group-hover:text-primary transition-colors">
                {post.title}
              </h2>
              <p className="text-muted-foreground mt-1">
                {post.content.slice(0, 155)}...
              </p>
              <time className="text-xs text-muted-foreground mt-2 block">
                {new Date(post.date).toLocaleDateString()}
              </time>
            </a>
          </article>
        ))}
      </div>
    </div>
  );
}

On-Demand Revalidation

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

export async function POST(request: NextRequest) {
  const secret = request.headers.get("x-revalidation-secret");
  if (secret !== process.env.REVALIDATION_SECRET) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }

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

  if (tag) {
    revalidateTag(tag);
    return NextResponse.json({ revalidated: true, tag });
  }

  if (type === "post" && slug) {
    revalidatePath(`/blog/${slug}`);
    revalidatePath("/blog");
    return NextResponse.json({ revalidated: true, paths: [`/blog/${slug}`, "/blog"] });
  }

  // Revalidate all blog pages
  revalidatePath("/blog", "layout");
  return NextResponse.json({ revalidated: true, scope: "all-blog" });
}

Tag-Based Caching

// Use fetch tags for granular cache control
async function getPostBySlug(slug: string): Promise<Post | null> {
  const res = await fetch(`${process.env.CMS_URL}/posts/${slug}`, {
    next: { tags: [`post-${slug}`, "posts"] },
  });
  if (!res.ok) return null;
  return res.json();
}

async function getCategories() {
  const res = await fetch(`${process.env.CMS_URL}/categories`, {
    next: { tags: ["categories"] },
  });
  return res.json();
}

// Then revalidate specific tags:
// revalidateTag("post-my-slug") — just one post
// revalidateTag("posts") — all posts
// revalidateTag("categories") — just categories

Draft Mode for Previews

// app/api/draft/route.ts
import { draftMode } from "next/headers";
import { NextRequest, NextResponse } from "next/server";

export async function GET(request: NextRequest) {
  const secret = request.nextUrl.searchParams.get("secret");
  const slug = request.nextUrl.searchParams.get("slug");

  if (secret !== process.env.DRAFT_SECRET) {
    return NextResponse.json({ error: "Invalid secret" }, { status: 401 });
  }

  (await draftMode()).enable();
  return NextResponse.redirect(new URL(`/blog/${slug}`, request.url));
}

// app/api/draft/disable/route.ts
export async function GET() {
  (await draftMode()).disable();
  return NextResponse.redirect(new URL("/blog", process.env.NEXT_PUBLIC_URL));
}
// app/blog/[slug]/page.tsx — with draft mode
import { draftMode } from "next/headers";

export default async function BlogPostPage({ params }: { params: Promise<{ slug: string }> }) {
  const { slug } = await params;
  const { isEnabled: isDraft } = await draftMode();

  const post = isDraft
    ? await getDraftPost(slug) // Fetch draft version
    : await getPost(slug);      // Fetch published version

  if (!post) notFound();

  return (
    <>
      {isDraft && (
        <div className="bg-yellow-100 border-b border-yellow-200 px-4 py-2 text-sm text-yellow-800">
          Draft mode enabled.{" "}
          <a href="/api/draft/disable" className="underline">
            Exit preview
          </a>
        </div>
      )}
      <article className="max-w-3xl mx-auto py-12">
        <h1 className="text-4xl font-bold">{post.title}</h1>
        <div className="prose mt-8" dangerouslySetInnerHTML={{ __html: post.content }} />
      </article>
    </>
  );
}

Sitemap Generation

// app/sitemap.ts
import type { MetadataRoute } from "next";

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  const posts = await getAllPosts();
  const baseUrl = process.env.NEXT_PUBLIC_URL ?? "https://yourdomain.com";

  const postEntries = posts.map((post) => ({
    url: `${baseUrl}/blog/${post.slug}`,
    lastModified: new Date(post.date),
    changeFrequency: "weekly" as const,
    priority: 0.7,
  }));

  return [
    { url: baseUrl, lastModified: new Date(), changeFrequency: "daily", priority: 1.0 },
    { url: `${baseUrl}/blog`, lastModified: new Date(), changeFrequency: "daily", priority: 0.9 },
    ...postEntries,
  ];
}

Need a High-Performance Blog?

We build blazing fast statically generated sites with smart caching strategies. Contact us to get started.

SSGISRstatic generationblogNext.jstutorial

Ready to Start Your Project?

RCB Software builds world-class websites and applications for businesses worldwide.

Get in Touch

Related Articles