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.