Steven's Knowledge

Patterns

Static vs SSR vs ISR, environment variables, preview environments, edge functions, image optimization, monorepos

Patterns

The patterns that come up once "I can push and deploy" is no longer impressive.

Render Mode Per Page

Modern frameworks let you mix render modes per page. The trade-offs:

ModeLatencyCostBest for
Static (SSG)Cheapest, fastest (CDN cache)Per-request: ~0Marketing pages, blog, docs
ISR / RevalidateFirst request slow, then cachedPeriodic regenerationMostly static, occasional updates
SSR (server)Each request rendersFunction invocation per requestUser-specific content, freshness critical
Edge SSRFaster than serverless (edge POP)Edge function pricingSame as SSR but global
StreamingTime-to-first-byte fastEdge functionLong pages, LLM responses
Client-only (CSR)First load slower (JS download)Cheap (static delivery)Highly interactive, post-login dashboards

The default in modern Next.js / Astro / SvelteKit: static where possible, dynamic where necessary. Frameworks make this configurable per-route.

Next.js Examples

// app/blog/[slug]/page.tsx — static; generated at build
export async function generateStaticParams() {
  const posts = await getPosts();
  return posts.map(p => ({ slug: p.slug }));
}

// app/products/page.tsx — ISR
export const revalidate = 300;  // re-generate every 5 minutes

// app/dashboard/page.tsx — SSR (per-request)
export const dynamic = 'force-dynamic';

// app/api/chat/route.ts — edge runtime
export const runtime = 'edge';

The platform compiles each route to the right runtime and tier.

Environment Variables

Three scopes:

ScopeWhereUse
NEXT_PUBLIC_*Browser bundlePublic config (analytics ID, public API URL)
Build-timeAvailable during build, not runtimeEmbed values into static pages
Server-side / RuntimeAvailable to server / edge functionsSecrets, DB connection strings
// pages/api/data.ts (server route)
const apiKey = process.env.API_KEY;          // server-only
const publicUrl = process.env.NEXT_PUBLIC_API_URL;   // also exists in browser

Never put secrets in NEXT_PUBLIC_* — they end up in the JavaScript bundle that ships to every user.

Most platforms support per-environment values:

DATABASE_URL = postgres://prod...     (production)
DATABASE_URL = postgres://staging...  (preview)

Plus per-branch overrides on some platforms — useful for staging vs preview-of-staging.

Preview Environments Done Right

Preview deploys are powerful but have gotchas:

What Backs Them

ResourcePer-preview / Shared?
DatabaseUsually shared (preview = staging DB)
Object storageUsually shared
Email sendingSuppress non-test addresses
Third-party APIsTest mode where possible
AnalyticsTagged as env=preview

Sharing the staging DB means a PR could mess up staging. Two options:

  1. Tolerate it — preview branches are short-lived.
  2. Per-PR ephemeral databases — e.g., Neon's branching (Postgres branches per PR), PlanetScale's branches.

Per-PR DB branches are powerful but add complexity. Most teams tolerate shared previews.

Auth on Previews

Public preview URLs are convenient and discoverable. Don't deploy unfinished pricing pages or PII-bearing flows to public preview URLs without thinking.

PlatformPreview auth
Vercel ProDeployment Protection (password / SSO)
NetlifyPassword protection on previews
Cloudflare Pages + AccessCloudflare Access policy on *.pages.dev

For sensitive products: lock preview URLs behind SSO via Cloudflare Access or platform-native auth.

Edge Functions

Static hosts often include an edge runtime. Useful for:

  • Auth checks before serving cached pages.
  • A/B testing at the edge.
  • API endpoints that don't need a full backend.
  • Image transformation.
  • Webhook handlers.

See Edge Functions for the full pattern set.

In static-host context, the integration is closer:

// app/api/auth/check/route.ts — Next.js Route Handler
export const runtime = 'edge';

export async function GET(request: Request) {
  const cookie = request.headers.get('cookie');
  // ...
  return Response.json({ authenticated: true });
}

One repo, one deploy, mixed static + edge functions. The platform routes traffic per URL.

Image Optimization

Modern frameworks ship image components that work with platform image services:

import Image from 'next/image';

<Image
  src="/photo.jpg"
  alt="A photo"
  width={400}
  height={300}
  sizes="(max-width: 768px) 100vw, 400px"
/>

The framework + platform combination:

  1. Detects supported formats (Accept header — avif, webp, jpeg).
  2. Resizes on demand based on sizes attribute.
  3. Caches the transformed image at the edge.
  4. Serves the smallest acceptable variant.

Per-platform implementation:

  • Vercelnext/image integrated with their image service.
  • Cloudflare Pages — works with Cloudflare Images or cf-image transformations.
  • Netlify — Netlify Image CDN.
  • Standaloneimgix, cloudinary, or your own cdn-cgi/image/* URLs.

This single feature can drop page weight 50-80%.

Form Handling Without a Backend

Some platforms accept forms with no backend code:

<!-- Netlify Forms — just add the data-netlify attribute -->
<form name="contact" method="POST" data-netlify="true">
  <input name="email" />
  <textarea name="message"></textarea>
  <button type="submit">Send</button>
</form>

Netlify accepts the submission and stores it; you read in the dashboard or via API. Similar features in Vercel (Forms via Form Action) and Cloudflare (Workers).

For more sophisticated forms (validation, integrations), still better to call a real backend — but for "contact us" pages, the platform form is the fastest path.

Headers, Redirects, Rewrites

Configure at the platform level:

# Netlify: _headers file
/*
  X-Frame-Options: DENY
  X-Content-Type-Options: nosniff

/admin/*
  Cache-Control: no-store
# Netlify: _redirects file
/old-page    /new-page    301
/api/*       https://api.example.com/:splat    200    # rewrite (proxy)
/blog/*      /posts/:splat    302

Cloudflare Pages: _headers and _redirects files at the project root. Vercel: vercel.json.

Use them for:

  • Security headers (CSP, X-Frame-Options, HSTS).
  • Redirects after URL changes (old paths → new paths).
  • Proxying API calls to avoid CORS.

Monorepo Setup

For monorepos (one repo, multiple sites):

my-monorepo/
├── apps/
│   ├── web/        ← public site
│   ├── docs/       ← docs site
│   └── admin/      ← admin app
└── packages/
    └── shared/

Each platform handles this:

  • Vercel — set "Root Directory" to apps/web per project; create three projects on the same repo.
  • Cloudflare Pages — same idea, configure the build directory per project.
  • Netlify — base directory setting; package detection in netlify.toml.

Tools like Turborepo and Nx help build only what changed across the monorepo.

Cross-Origin Concerns

Static sites often need to call your APIs on a different domain. Two paths:

ApproachProsCons
CORS at the APIClean separation; API serves multiple clientsConfigure correctly; preflight requests
Rewrite at the static hostNo CORS issues; same-origin from browser perspectivePlatform-dependent; only HTTP backends

Rewrite example:

# Cloudflare Pages _redirects
/api/*    https://api.example.com/:splat    200

Now https://my-site.example.com/api/users is proxied to https://api.example.com/users — same origin from the browser's point of view.

What's Next

You know the patterns. Best Practices covers operations — build optimization, performance, observability, cost, common pitfalls.

On this page