Static or SSR: How to Pick for a Content Site

Static and SSR look interchangeable on a Next.js project until you scale, ship, or pay the bill. Here is how to pick correctly for a content site in 2026.

Static vs SSR is the decision that quietly determines your hosting cost, your time-to-first-byte, your Core Web Vitals, and how often you wake up to a 500 error. For a content site the answer is almost always “static, with a sprinkle of dynamic where it matters”. This walks through how to actually decide.

Background

Modern Next.js (App Router) blurs the lines: a single route can be static, ISR-cached, dynamic SSR, or streamed. That flexibility is a feature, but it lets people accidentally opt into SSR-by-default because they forgot a generateStaticParams or used cookies() somewhere. For a content site that is a self-inflicted regression.

What “static” / “ISR” / “SSR” look like in code

Next.js App Router, forced static — what you almost always want for an article page:

// app/articles/[slug]/page.tsx
export const dynamic = 'force-static';
export const revalidate = false;

export async function generateStaticParams() {
  return getAllSlugs().map((slug) => ({ slug }));
}

export default async function Article({ params }: { params: { slug: string } }) {
  const post = await getPost(params.slug);
  return <Markdown source={post.body} />;
}

ISR — same shape but content refreshes every N seconds without redeploying:

export const revalidate = 300;  // re-render at most once every 5 minutes

SSR — only when the page genuinely depends on the request:

export const dynamic = 'force-dynamic';
// implies cookies()/headers() use; every request hits the server

Astro is static by default; opt in to SSR per-route only when needed:

// astro.config.mjs
import { defineConfig } from 'astro/config';
export default defineConfig({
  output: 'static',                                  // global default
  // output: 'hybrid',                               // per-route opt-in to SSR
  // adapter: import('@astrojs/node').default(),     // only if you actually need it
});

How to tell

  • Content rarely changes per request — same article served to everyone? Static.
  • Content is per-user or per-region — login-gated dashboard, geo offers? SSR.
  • You want CDN-cheap hosting with predictable latency? Static.
  • You need to fetch a freshly updated price / inventory / score on every render? SSR or ISR with short revalidate.
  • You are deploying to Cloudflare / Firebase / S3 / GitHub Pages? Static (these do not run Node per request).

Quick verdict

Default to static for content. Reach for SSR only where the page genuinely depends on per-request data. ISR (revalidate every N seconds) is the middle ground for “mostly static, updates sometimes” pages and is almost always what people actually want when they reach for SSR.

Step by step

  1. List every route. Mark each as: static, ISR (with revalidate window), SSR, or client-rendered.
  2. For each SSR mark, ask “what changes between two anonymous visitors in the same minute?” If nothing — it should be static.
  3. For each ISR mark, pick a revalidate window. 60s is fine for most blogs. Pricing pages may want 300s. Almost nothing needs 1s.
  4. In Next.js App Router, make sure routes are not accidentally opting into dynamic — set dynamic = 'force-static' where you want guarantees.
  5. For RSC fetches, set cache: 'force-cache' (default) or next: { revalidate: N } deliberately rather than leaving it implicit.
  6. Deploy and check the build output — Next.js logs which routes are Static, SSG, or Dynamic. Audit it.
  7. Run Lighthouse against the deployed static pages. If TTFB and LCP are clean, you have made the right call.

Common pitfalls

  • Accidentally going dynamic — one cookies() or headers() call in a layout will mark the whole subtree dynamic.
  • Setting ISR revalidate too aggressively (every 10s) and burning function invocations for content that changes daily.
  • Picking SSR “just in case I need it later” — premature flexibility is the same as premature optimization, you pay for it now in cold starts and bills.
  • Forgetting to set dynamic = 'force-static' on routes that must be CDN-cacheable for performance commitments.
  • Trying to do SSR on a host that does not run Node (Cloudflare Pages without Workers, plain S3, GitHub Pages) and not understanding why it fails.

Who this is for

Indie content sites — blogs, docs, niche directories, course sites — where 95%+ of traffic is the same HTML for everyone.

When to skip this

Apps with login walls, personalized feeds, or content tied to the requester (auth, geo, A/B) — those genuinely need SSR or client-rendered.

FAQ

  • Is ISR just cached SSR?: Effectively yes — Next.js builds the page on the first request after the revalidate window expires, then serves the cached HTML to everyone else until the next expiry.
  • Does static break if I have a CMS?: No. Build-time fetches from Contentful, Sanity, or Notion are still static. Trigger a rebuild on publish (via webhook) and you have the best of both worlds.
  • What about search?: Client-side search against a static JSON index handles most content sites up to thousands of posts. Only reach for SSR search if you need server-side filtering at scale.
  • Can I mix static and SSR in one Next.js project?: Yes, that is the whole point of App Router. Each route picks its own strategy.

Tags: #Indie dev #Next.js #Website planning #Comparison #Core Web Vitals