Static-first business sites: why your marketing pages don't need a server
This site renders once at build time and serves from memory. SSG, ISR, and the short list of things that genuinely need a request-time server — most marketing pages aren't on it.
Ask a client why their marketing site needs a server and the honest answer is almost always "it doesn't, but the framework made it easy to add one." Next.js ships a request-time server by default, so a request-time server is what people build, even when every page on the site is the same for every visitor. A home page, a services page, an about page, a blog post — none of that changes per request. It changes when someone edits it. That's a build-time problem, not a runtime one, and treating it as runtime is how a five-page brochure site ends up with the same operational surface as a checkout flow: a process to keep alive, a health check to monitor, a pod to restart when it wedges.
Three modes, not two
The framework gives you more granularity than "static site" versus "server," and conflating them is where the confusion starts.
const nextConfig = {
// Pure static export. No Node process, no server component render
// at request time — every route is prerendered HTML in a folder.
output: "export",
}output: "export" produces exactly what it sounds like: a folder of HTML, CSS and JS you can drop on any static host or CDN. There is no server to run, which also means no server-only features — no Route Handlers, no revalidate, no per-request headers. If your entire site is content that's the same for every visitor, this is the whole solution and you're done reading.
const nextConfig = {
// A real, small Node process. Static pages stay static; specific
// routes opt into ISR or full SSR without changing everything else.
output: "standalone",
}output: "standalone" — what we run this very site on — is the middle ground people skip past. It's still a server, but a thin one: pages that don't opt into anything dynamic get prerendered and served as static files, and only the routes that declare a cache window or read a request actually touch the Node process. You get the operational simplicity of static output for most of the site, and a real server available for the handful of pages that need one.
Full SSR — no static generation, every route rendered on every request — is the mode people reach for by default and the one almost no marketing page needs. It's the right tool for something like a logged-in dashboard where the response genuinely depends on who's asking. It's the wrong tool for a services page that renders identically for every visitor and gets rebuilt, at most, when someone edits the copy.
What actually forces a request-time server
There's a short, honest list:
- Personalization. The response depends on who's logged in — an account dashboard, a cart, anything keyed to a session.
- Auth-gated content. The page shouldn't render at all for an unauthenticated request, so you can't prerender it once and serve it to everyone.
- Live pricing or inventory. The number on the page has to be correct right now, not "correct as of the last rebuild or revalidation window."
- Per-request context you can't bake into a build — geolocation-based routing, A/B assignment that has to be consistent within a session, anything reading a header that varies by request.
Notice what's not on that list: "the content comes from a CMS," "the content changes sometimes," or "we want SEO." Those three, on their own, are exactly what static generation and ISR already solve — they just don't require the page to be recomputed on every single request.
What people wrongly think needs one
Almost every page on a typical business site: the home page, the services pages, the about page, case studies, the blog itself. All of it is the same HTML for every visitor, and all of it changes on an editorial cadence — someone publishes a post, someone updates a case study — not on a per-request basis. That's the textbook case for prerendering it once at build time and serving the result from memory or a CDN edge, with zero server-side compute per visit.
The mistake is assuming that because some pages on the site are backed by live data, the whole app needs to run as a server on every request. It doesn't. It needs the pages that are actually dynamic to opt into dynamism, and everything else to stay static by default.
How this site actually splits it
We don't theorize this — we run it. Every post you're reading, every service page, the whole custom web application portfolio: prerendered at build time, served as static files, rebuilt and redeployed on push through our own CI. No database read happens to serve you this page. Two routes on this site are the exception, because they're the two routes actually backed by live data in Odoo:
// The Odoo reads are cached, not the routes. Every page using a reader
// inherits its window, and server actions keep calling the live ones.
export async function fetchJobFacts(ids: number[]) {
"use cache"
cacheLife("odooJobs")
return readJobFacts(ids)
}cacheLife: {
// 60s: closing a role in Odoo should show up quickly.
odooJobs: { stale: 60, revalidate: 60, expire: 86400 },
// 300s: accepting or answering a question shows up within minutes
// without a deploy, while Odoo sees at most one read per window.
odooAma: { stale: 300, revalidate: 300, expire: 86400 },
},/jobs and /ama opt into caching with a window because their content is genuinely live — a role closes, a question gets answered — and everything else on the site opts into nothing, because it doesn't need to. Note where the window is declared: on the Odoo reads, not on the routes. Next 16.3 moved us off route-level export const revalidate to "use cache" + a named cacheLife profile, which is a better fit for this argument anyway — the page isn't dynamic, one function it calls is, and only that function needs a staleness budget. Neither route needed full SSR either: a 60-second or 300-second window is fine for "a job closed" or "a question got answered," so we buy correctness on a timer instead of paying for a fresh render on every visit. When a page needs tighter correctness than a timer allows — a product page that has to reflect an Odoo change immediately — that's where revalidateTag and a webhook from the source system earn their keep instead of a blanket short window.
The rule of thumb
Default every page to static. Move a specific route off that default only when you can name which item from the "actually forces a server" list applies to it — and even then, reach for ISR's revalidation window before reaching for full SSR, because most "it needs to be live" requirements really mean "it needs to be live within a minute," not "on this exact request." A marketing site that follows this ends up looking like ours: a folder of prerendered pages, a couple of narrow exceptions with a number attached to how stale they're allowed to be, and a pod that's mostly a file server with opinions.
Want us to publish something specific?
Tell us what you'd like to read and we'll add it to our writing queue.
Get the next one in your inbox
New articles, videos and the occasional engineering note — a short mail when there’s something worth reading, nothing else.