The Build Ledger Search articles
Back to articles

Choosing Between Static Generation and Server Rendering in Content Products

The rendering choice should follow freshness, personalization, operational cost, preview needs, and failure behavior.

Content products often start static for good reasons: static pages are cheap to serve, easy to cache, easy to crawl, and simple to preview. The mistake is treating that first choice as an identity. A content product is not “a static app” or “a server-rendered app.” It is a set of routes with different freshness, privacy, and failure requirements.

The useful question is route-level: what does this page need at request time that it cannot know at build time?

Start with a route inventory

Before debating frameworks, list the routes and their constraints.

RouteFreshnessIdentityRecommended mode
/posts/[slug]/Changes only on publishPublicStatic generation
/tags/[tag]/Changes on publishPublicStatic generation
/account/billing/Must reflect current accountPrivateServer rendering
/search/Index can be stale by minutesPublicStatic shell + client/API search
/pricing/Mostly durable, checkout dynamicPublicStatic page + server checkout endpoint

This table usually resolves more arguments than a generic “SSR versus SSG” discussion.

Static generation is excellent for durable public content

Articles, documentation, policy pages, changelogs, and category archives are strong static candidates. The page can be built once, served from a CDN, previewed before deploy, and indexed without depending on a live origin for every request.

The failure mode is also clear. If the database is down after deployment, already-built article pages still load. For a publication, documentation site, or marketing archive, that resilience matters.

The tradeoff is publication timing. A static route changes when the build changes. If editors expect a scheduled post to appear at exactly 09:00, the build system needs to run then, or the app needs a runtime visibility check.

Server rendering should earn its operational cost

Server rendering is justified when request-time data is the point of the page: account state, entitlement checks, private dashboards, unread counts, inventory, localized pricing, or admin tools.

That flexibility carries operational work:

- runtime error monitoring
- cache headers per route
- database and API timeout behavior
- capacity during traffic spikes
- preview data and safe credentials
- fallback behavior when dependencies fail

A static build failure blocks publication. A server failure can affect every request. That difference should be explicit when choosing a rendering mode.

Isolate dynamic widgets instead of making whole pages dynamic

A public article may have a bookmark button. A docs page may have API-backed search. A pricing page may open checkout. None of those require the entire route to be server rendered.

Keep the durable content static and isolate the dynamic piece:

export default function ArticlePage({ article }) {
  return (
    <main>
      <ArticleBody article={article} />
      <BookmarkButton articleId={article.id} />
    </main>
  );
}

If the bookmark request fails, the article still exists. That is the boundary you want.

Decide what stale means

“Fresh” is too vague. Some content can be stale for a day. Search indexes may be fine with a few minutes of lag. Billing pages may need current data. Breaking news, inventory, and stock availability may need tighter rules.

Write it down:

Article page: stale until next content build is acceptable
Search index: stale up to 10 minutes is acceptable
Account entitlement: stale data is not acceptable
Pricing copy: static; checkout price verified server-side

Once the stale window is explicit, rendering choices become less emotional.

Document the mode beside the route

Future developers need to know why a route behaves the way it does. A short route note is enough:

/posts/[slug]/: static, because public content changes only through publishing workflow
/account/billing/: server-rendered, because plan and invoice state are private and current
/search/: static shell with API search, because index lag is acceptable and article pages remain crawlable

Good rendering decisions feel boring after launch. Static pages stay fast and resilient. Dynamic pages earn their complexity by serving data that truly belongs at request time.