When I built 32blog.com with Next.js 16's App Router, the biggest mental shift was rethinking SSR (Server-Side Rendering) from scratch.
I was used to Pages Router's getServerSideProps and getStaticProps. When App Router introduced Server Components, I spent a lot of time figuring out "how do I actually use these?" This article answers all those questions in one place.
By the end, you'll know how to decide between SSR and SSG in App Router, where to put your data fetching, and how to control caching behavior.
How Did SSR Change with App Router?
In Pages Router, SSR meant using getServerSideProps — a special function that runs server-side processing on every request.
// Pages Router (old way)
// pages/posts/[id].tsx
export async function getServerSideProps(context) {
const { id } = context.params;
const post = await fetchPost(id);
return {
props: { post },
};
}
export default function PostPage({ post }) {
return <article>{post.title}</article>;
}
In App Router, this pattern changed fundamentally. Components themselves can be async.
// App Router (current)
// app/posts/[id]/page.tsx
async function fetchPost(id: string) {
const res = await fetch(`https://api.example.com/posts/${id}`);
return res.json();
}
export default async function PostPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
const post = await fetchPost(id);
return <article>{post.title}</article>;
}
You can write await directly inside the component. The indirect pattern of passing props through getServerSideProps is gone — data fetching and UI live in the same file.
The core shift is that components now run on the server by default. Server Components execute server-side only. You can write database queries or API calls directly, and that code never gets sent to the browser.
How to Decide Between Server and Client Components
The decision rule is simple:
| Feature | Server Component | Client Component |
|---|---|---|
useState / useEffect | ❌ | ✅ |
| Browser APIs (localStorage, etc.) | ❌ | ✅ |
| Direct database access | ✅ | ❌ |
| API keys (never exposed to client) | ✅ | ❌ |
| Click events and interactions | ❌ | ✅ |
| SEO-critical content | ✅ | △ |
Typical Server Component Pattern
// app/components/ArticleList.tsx
// No "use client" = Server Component
import { getArticles } from "@/lib/content";
export async function ArticleList({ category }: { category: string }) {
// This only runs on the server
// API keys are safe to use directly here
const articles = await getArticles(category);
return (
<ul>
{articles.map((article) => (
<li key={article.slug}>
<a href={`/en/${category}/${article.slug}`}>{article.title}</a>
</li>
))}
</ul>
);
}
Keep Client Components at the Leaf Level
// app/components/LikeButton.tsx
"use client";
import { useState } from "react";
// Only the minimum interactive unit becomes a Client Component
export function LikeButton({ initialCount }: { initialCount: number }) {
const [count, setCount] = useState(initialCount);
const [liked, setLiked] = useState(false);
const handleLike = async () => {
if (liked) return;
setCount((c) => c + 1);
setLiked(true);
await fetch("/api/likes", { method: "POST" });
};
return (
<button onClick={handleLike} aria-pressed={liked}>
{liked ? "❤️" : "🤍"} {count}
</button>
);
}
// app/posts/[id]/page.tsx (Server Component)
import { LikeButton } from "@/components/LikeButton";
export default async function PostPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
const post = await fetchPost(id);
const likeCount = await getLikeCount(id);
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
{/* Embed Client Component inside Server Component */}
<LikeButton initialCount={likeCount} />
</article>
);
}
How to Control fetch Caching
In App Router, fetch was cached by default (before Next.js 15). Since Next.js 15, the default changed to no-cache (no-store). Since 32blog.com runs on Next.js 16, the default is no cache.
// Next.js 15+ — fetch defaults to no-store
const res = await fetch("https://api.example.com/posts");
// Explicitly opt into SSG (fetch once at build time)
const res = await fetch("https://api.example.com/posts", {
cache: "force-cache",
});
// Regenerate at a set interval (ISR equivalent)
const res = await fetch("https://api.example.com/posts", {
next: { revalidate: 3600 }, // recheck every hour
});
// Always fresh (SSR equivalent)
const res = await fetch("https://api.example.com/posts", {
cache: "no-store",
});
These are options on the fetch function. For data fetching that doesn't use fetch — like direct Prisma database queries — caching works differently.
Set Caching per Route Segment
Route segment config lets you set caching behavior for an entire page file.
// app/posts/[id]/page.tsx — Force static generation (SSG equivalent)
export const dynamic = "force-static";
// app/posts/[id]/page.tsx — Force dynamic rendering (SSR equivalent)
export const dynamic = "force-dynamic";
// ISR equivalent: regenerate every 60 seconds
export const revalidate = 60;
32blog.com uses generateStaticParams to statically generate all article pages at build time.
// app/[locale]/[category]/[slug]/page.tsx
// Generate all article paths at build time
export async function generateStaticParams() {
const locales = ["ja", "en"];
const categories = ["nextjs", "react", "claude-code", "ffmpeg", "gaming"];
const paths = [];
for (const locale of locales) {
for (const category of categories) {
const articles = await getArticlesByCategory(category, locale);
for (const article of articles) {
paths.push({
locale,
category,
slug: article.slug,
});
}
}
}
return paths;
}
What Does Suspense + Streaming Change?
Pages Router SSR worked by sending the full HTML only after everything finished rendering. App Router supports streaming. Combined with Suspense, you can send HTML progressively as each part becomes ready.
// app/posts/[id]/page.tsx
import { Suspense } from "react";
import { PostContent } from "@/components/PostContent";
import { RelatedPosts } from "@/components/RelatedPosts";
import { CommentSection } from "@/components/CommentSection";
export default async function PostPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
return (
<main>
{/* Post content appears first (fast API) */}
<Suspense fallback={<PostContentSkeleton />}>
<PostContent id={id} />
</Suspense>
{/* Related posts come later (separate, slower API) */}
<Suspense fallback={<RelatedPostsSkeleton />}>
<RelatedPosts id={id} />
</Suspense>
{/* Comments load last (heaviest operation) */}
<Suspense fallback={<CommentsSkeleton />}>
<CommentSection id={id} />
</Suspense>
</main>
);
}
The browser receives the first HTML chunk as soon as PostContent is ready. Related posts and comments stream in afterward. Users don't wait for everything to load before seeing the main content.
The practical benefit is improved LCP (Largest Contentful Paint). Getting the most important content — the article itself — to users faster reduces the time they perceive as "nothing is loading."
When to Use SSG vs SSR
Here's a quick decision guide:
| Case | Recommendation | Reason |
|---|---|---|
| Blog posts (static content) | SSG (static generation) | Fast, CDN cacheable |
| Product listings (changing inventory) | ISR (revalidate) | Cache matches update frequency |
| User profile pages | SSR (dynamic) | Different per user |
| Real-time dashboards | CSR (Client Component) | Server rendering not needed |
32blog.com uses SSG for article pages and the homepage (article list generated at build time). Search is implemented as a Client Component.
// app/[locale]/page.tsx
// Homepage — statically generated
export async function generateStaticParams() {
return [{ locale: "ja" }, { locale: "en" }];
}
export default async function HomePage({
params,
}: {
params: Promise<{ locale: string }>;
}) {
const { locale } = await params;
// Read MDX files at build time to generate article list
const latestPosts = await getLatestPosts(locale, 6);
const categories = await getCategories(locale);
return (
<main>
<HeroSection />
<ArticleGrid posts={latestPosts} />
<CategoryList categories={categories} />
</main>
);
}
How to Handle Form Processing with Server Actions
App Router introduces Server Actions — server-side processing you can run directly from form submissions without creating an API route.
// app/contact/page.tsx
export default function ContactPage() {
async function submitContact(formData: FormData) {
"use server"; // This function runs on the server
const name = formData.get("name") as string;
const email = formData.get("email") as string;
const message = formData.get("message") as string;
// Server-side processing (send email, save to DB, etc.)
await sendEmail({ name, email, message });
}
return (
<form action={submitContact}>
<input name="name" type="text" required />
<input name="email" type="email" required />
<textarea name="message" required />
<button type="submit">Send</button>
</form>
);
}
The biggest benefit is keeping API keys and database connections off the client. For real-time validation feedback, combine with useActionState (React 19+) and useFormStatus.
Wrapping Up
The App Router's SSR approach takes getting used to — especially the idea that "components run on the server by default." Here's the summary:
- Server Component — the default. Write data fetching directly in the component.
async/awaitworks natively - Client Component — add
"use client". Hooks and browser APIs work here. Code is included in the browser bundle - Caching — Next.js 15+ defaults to no-cache. Explicitly set
force-cacheorrevalidatewhen needed - Streaming — wrap components in Suspense to send HTML progressively as each part becomes ready
- SSG vs SSR — decide based on how often content changes and whether it varies per user
32blog.com uses this design to statically generate all pages at build time, serving them from CDN after deployment. Response time stays consistent regardless of how many articles we add — that's the real strength of static generation.