[2026] Technical SEO with Next.js App Router | SSR, SSG, ISR & Cache Boundaries

[2026] Technical SEO with Next.js App Router | SSR, SSG, ISR & Cache Boundaries

이 글의 핵심

Choose App Router rendering per route: SSG, SSR, and ISR with fetch cache, revalidate, tags, Route Segment Config, and SEO-safe patterns for metadata and personalization.

Introduction

In Next.js App Router (13+), how pages are built on the server and how aggressively they cache shapes performance, SEO, and operating cost at once. Knowing SSR vs SSG vs ISR only by name is not enough—one fetch option can change caching behavior in surprising ways. This article maps static generation, server rendering, and incremental revalidation onto server components, fetch cache semantics, and Route Segment Config. It assumes 2026-era App Router + fetch cache behavior.

After reading this post

  • Distinguish what SSG / SSR / ISR mean in App Router
  • Design fetch options and revalidate per data source
  • Split strategies for dynamic routes, personalization, and admin UIs

Table of contents

  1. Concepts
  2. Hands-on implementation
  3. Advanced: Route Segment Config
  4. Performance comparison
  5. Real-world cases
  6. Troubleshooting
  7. Conclusion

Concepts

Terminology (vs Pages Router intuition)

TermIntuitive meaningApp Router reality
SSGHTML at build (or regenerate) timePaths that can statically cache server component trees—often fetch(..., { cache: 'force-cache' }) patterns
SSRHTML per requestDynamic rendering—closer to cache: 'no-store' or dynamic = 'force-dynamic'
ISRPeriodic or on-demand static refreshfetch revalidate seconds or revalidatePath / revalidateTag
App Router creates cache boundaries from fetch + segment config, not a single page-level toggle.

React Server Components (RSC)

Server components run on the server by default; the client bundle receives serialized output. “SSR vs SSG” becomes when and how that output is cached.

Hands-on implementation

SSG-like: build-time cached data

아래 코드는 typescript를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// app/posts/page.tsx — cache at build (similar to default force-cache)
export default async function PostsPage() {
  const res = await fetch('https://api.example.com/posts', {
    cache: 'force-cache',
  });
  const posts = await res.json();
  return (
    <ul>
      {posts.map((p: { id: string; title: string }) => (
        <li key={p.id}>{p.title}</li>
      ))}
    </ul>
  );
}

SSR: fresh data every request

아래 코드는 typescript를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// app/dashboard/page.tsx
export default async function DashboardPage() {
  const res = await fetch('https://api.example.com/me', {
    cache: 'no-store',
  });
  const user = await res.json();
  return <div>{user.name}</div>;
}

ISR: time-based revalidation

아래 코드는 typescript를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// app/blog/[slug]/page.tsx
export default async function PostPage({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { slug } = await params;
  const res = await fetch(`https://api.example.com/posts/${slug}`, {
    next: { revalidate: 3600 }, // background revalidate every hour
  });
  if (!res.ok) notFound();
  const post = await res.json();
  return <article>{post.body}</article>;
}

Tag-based invalidation (great for operations)

아래 코드는 typescript를 사용한 구현 예제입니다. 필요한 모듈을 import하고, 비동기 처리를 통해 효율적으로 작업을 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// On fetch
await fetch('https://api.example.com/posts', {
  next: { tags: ['posts'] },
});
// In a Server Action or Route Handler
import { revalidateTag } from 'next/cache';
export async function POST() {
  revalidateTag('posts');
  return Response.json({ ok: true });
}

Summary: Mixed strategies inside one route are normal—define a per-source cache policy table for the team.

Advanced: Route Segment Config

Force dynamic segments

// app/admin/layout.tsx
export const dynamic = 'force-dynamic';
export const fetchCache = 'force-no-store';
  • dynamic: 'auto' | 'force-dynamic' | 'error' | 'force-static' — default behavior for the route tree
  • revalidate: segment-level default revalidation window (use alongside fetch-level settings)

generateStaticParams for SSG scope

아래 코드는 typescript를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 반복문으로 데이터를 처리합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
  const slugs = await getAllSlugs(); // list available at build time
  return slugs.map((slug) => ({ slug }));
}

If a CMS exports tens of thousands of slugs, prebuild only top pages and rely on on-demand ISR for the tail.

Performance comparison

SituationDirectionWhy
Marketing, docs, legalSSG + long revalidate or fully staticMax CDN hit ratio, stable TTFB
Dashboards, cartsSSR (no-store) or split client islandsPer-user data, avoid cache poisoning
Blogs, catalogsISR + revalidateTagBalance traffic vs freshness
Real-time inventory/pricingSSR + short TTL or edge + external cacheNext cache alone may be insufficient
Key idea: Prefer agreeing “how stale can this fetch be?” over labeling a page “SSG” in isolation.

Real-world cases

  • E-commerce listing: revalidate: 300 + products tag; call revalidateTag('products') on price updates.
  • Logged-in header: no-store for user info; keep shared nav fragments static to minimize personalized surface area.
  • Docs site: Mostly SSG; isolate search to client or a separate API—separate rendering from search indexing strategy.

Troubleshooting

“Build is fresh but production shows stale data”

  • Check fetch cache vs revalidate vs CDN/hosting data caches.

revalidatePath did not update”

  • Verify matching segment tree and cache keystag-based invalidation is often more reliable.

“Almost leaked user data across sessions”

  • Never force-cache per-user fetches—use no-store or route boundaries that separate auth contexts.

Conclusion

SSR vs SSG vs ISR in App Router is really about fetch + segment configuration telling a caching story. Document per-layer TTLs, tags, and invalidation triggers so performance and freshness debates shrink. For async load and server cost, pair with the Node.js performance guide.

... 996 lines not shown ... Token usage: 63706/1000000; 936294 remaining Start-Sleep -Seconds 3