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:
| Mode | Latency | Cost | Best for |
|---|---|---|---|
| Static (SSG) | Cheapest, fastest (CDN cache) | Per-request: ~0 | Marketing pages, blog, docs |
| ISR / Revalidate | First request slow, then cached | Periodic regeneration | Mostly static, occasional updates |
| SSR (server) | Each request renders | Function invocation per request | User-specific content, freshness critical |
| Edge SSR | Faster than serverless (edge POP) | Edge function pricing | Same as SSR but global |
| Streaming | Time-to-first-byte fast | Edge function | Long 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:
| Scope | Where | Use |
|---|---|---|
NEXT_PUBLIC_* | Browser bundle | Public config (analytics ID, public API URL) |
| Build-time | Available during build, not runtime | Embed values into static pages |
| Server-side / Runtime | Available to server / edge functions | Secrets, 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 browserNever 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
| Resource | Per-preview / Shared? |
|---|---|
| Database | Usually shared (preview = staging DB) |
| Object storage | Usually shared |
| Email sending | Suppress non-test addresses |
| Third-party APIs | Test mode where possible |
| Analytics | Tagged as env=preview |
Sharing the staging DB means a PR could mess up staging. Two options:
- Tolerate it — preview branches are short-lived.
- 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.
| Platform | Preview auth |
|---|---|
| Vercel Pro | Deployment Protection (password / SSO) |
| Netlify | Password protection on previews |
| Cloudflare Pages + Access | Cloudflare 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:
- Detects supported formats (
Acceptheader — avif, webp, jpeg). - Resizes on demand based on
sizesattribute. - Caches the transformed image at the edge.
- Serves the smallest acceptable variant.
Per-platform implementation:
- Vercel —
next/imageintegrated with their image service. - Cloudflare Pages — works with Cloudflare Images or
cf-imagetransformations. - Netlify — Netlify Image CDN.
- Standalone —
imgix,cloudinary, or your owncdn-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 302Cloudflare 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/webper 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:
| Approach | Pros | Cons |
|---|---|---|
| CORS at the API | Clean separation; API serves multiple clients | Configure correctly; preflight requests |
| Rewrite at the static host | No CORS issues; same-origin from browser perspective | Platform-dependent; only HTTP backends |
Rewrite example:
# Cloudflare Pages _redirects
/api/* https://api.example.com/:splat 200Now 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.