---
title: "24 — Static Site Generators — Render Once, Serve Forever"
uid: static-site-generators
tags: ["react-router", "ssg", "build", "sveltekit", "cdn", "astro", "roadmap:frontend"]
excerpt: "SSG's whole distinction is build-time vs request-time: render once at build, then serve plain HTML from a CDN."
date: 2026-08-12T18:35:07+0000
source: https://www.aveshina.my.id/en/blog/static-site-generators
---

"Just uploading HTML files" was how I wrote off static site generators, which hid the interesting half of the story. The one sentence that separated them: **render once, at build time, then serve plain files.** Once I had that frame, the whole category made sense — and so did the trade-off it's built around.

There are really only three moments a page's HTML can come into existence, and SSG picks the third:

```figure
<svg viewBox="0 0 740 250" xmlns="http://www.w3.org/2000/svg" class="my-6 w-full max-w-2xl" role="img" aria-label="Three rendering models compared by when the HTML is produced. Left box: CSR — browser runs JS and builds the DOM at click time. Middle box: SSR — server runs code and builds HTML at request time, every request. Right box: SSG — build step runs once, produces static HTML files served from a CDN. CSR and SSR rebuild repeatedly; SSG renders once and serves the cached file.">
  <defs>
    <marker id="sarrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto">
      <path d="M0,0 L10,5 L0,10 z" fill="#64748b"/>
    </marker>
  </defs>
  <g font-family="ui-sans-serif, system-ui, sans-serif" text-rendering="geometricPrecision">

    <!-- CSR -->
    <rect x="30" y="30" width="200" height="170" rx="10" fill="#fce7f3" stroke="#db2777" stroke-width="1.5"/>
    <text x="130" y="58" font-size="13" font-weight="700" fill="#500724" text-anchor="middle">CSR</text>
    <text x="130" y="76" font-size="10.5" fill="#475569" text-anchor="middle">Client-Side Rendering</text>
    <rect x="48" y="92" width="164" height="40" rx="6" fill="#fff" stroke="#db2777" stroke-width="1.2"/>
    <text x="130" y="116" font-size="11" font-weight="700" fill="#500724" text-anchor="middle">browser runs JS</text>
    <text x="130" y="148" font-size="10.5" fill="#db2777" text-anchor="middle" font-style="italic">HTML built every click</text>
    <text x="130" y="182" font-size="10" fill="#64748b" text-anchor="middle">empty shell + bundle</text>

    <!-- SSR -->
    <rect x="270" y="30" width="200" height="170" rx="10" fill="#fef9c3" stroke="#ca8a04" stroke-width="1.5"/>
    <text x="370" y="58" font-size="13" font-weight="700" fill="#422006" text-anchor="middle">SSR</text>
    <text x="370" y="76" font-size="10.5" fill="#475569" text-anchor="middle">Server-Side Rendering</text>
    <rect x="288" y="92" width="164" height="40" rx="6" fill="#fff" stroke="#ca8a04" stroke-width="1.2"/>
    <text x="370" y="116" font-size="11" font-weight="700" fill="#422006" text-anchor="middle">server runs code</text>
    <text x="370" y="148" font-size="10.5" fill="#ca8a04" text-anchor="middle" font-style="italic">HTML built every request</text>
    <text x="370" y="182" font-size="10" fill="#64748b" text-anchor="middle">needs a running server</text>

    <!-- SSG (highlighted) -->
    <rect x="510" y="30" width="200" height="170" rx="10" fill="#dcfce7" stroke="#16a34a" stroke-width="2"/>
    <text x="610" y="58" font-size="13" font-weight="700" fill="#052e16" text-anchor="middle">SSG</text>
    <text x="610" y="76" font-size="10.5" fill="#475569" text-anchor="middle">Static Site Generation</text>
    <rect x="528" y="92" width="164" height="40" rx="6" fill="#fff" stroke="#16a34a" stroke-width="1.4"/>
    <text x="610" y="116" font-size="11" font-weight="700" fill="#052e16" text-anchor="middle">build step runs once</text>
    <text x="610" y="148" font-size="10.5" fill="#16a34a" text-anchor="middle" font-style="italic">HTML built at deploy</text>
    <text x="610" y="182" font-size="10" fill="#64748b" text-anchor="middle">plain files on a CDN</text>

  </g>
</svg>
```

The thing I had to see clearly is *when* the work happens. With CSR the browser does the rendering, every time, after shipping a mostly-empty page and a JavaScript bundle. With SSR the server does the rendering, every request, which is correct but means a machine is on the clock for every visitor. SSG refuses both — it does the rendering **once**, when content or code changes, and hands the result off as flat HTML that a CDN (content delivery network — servers spread around the world) can copy to the edge (the server nearest each user) and serve with no computation at all [1]. Visitors never touch the build; they touch a file.

## The trade-off, stated plainly

SSG front-loads the cost. The build re-runs only when content or code changes — a new blog post, a deploy, a content edit — and the output is a set of static .html files plus assets. Between builds, nothing computes. That gives you four properties that follow directly from "it's just files" [1][2]:

- **Fast** — the server returns a pre-built file; there's no template rendering on the hot path (the code that runs on every request), and a CDN serves it from wherever's nearest the user.
- **Cheap** — static files need no always-on application server. Free or near-free hosting is the norm.
- **Secure** — there's no running process, database, or template engine to attack at request time. The attack surface shrinks to the file server.
- **Cacheable** — identical bytes for every visitor cache perfectly at every layer (browser, CDN, origin).

The cost you pay is the flip side: content only updates when you rebuild. A page that must reflect per-user, real-time, or frequently-changing data is a poor fit — you'd rebuild constantly, or fake freshness with client-side fetches, which is really CSR bolted onto an SSG shell. **SSG wins when content changes infrequently relative to reads** [1]. That's why the canonical SSG use cases are all read-heavy, update-rare: blogs, documentation, marketing sites, landing pages.

## Astro — zero JS by default, islands when you need it

Astro is the SSG I kept hearing about, and its angle is a single design decision: **ship zero JavaScript by default** [3]. Components (React, Vue, Svelte, or Astro's own) render to static HTML at build time, and the JS that made them interactive is *dropped* on the floor unless you explicitly opt in.

The opt-in mechanism is **partial hydration**, or "islands" [3]. You mark a component as an interactive island — a search box, a carousel, a like button — and *only that component* ships its JavaScript. Everything around it stays as inert HTML. So a 50-component marketing page with one interactive form ships the form's JS and nothing else. The mental shift for me: the page isn't "a React app"; it's HTML, with a few tiny apps embedded where interaction is actually needed.

```figure
<svg viewBox="0 0 600 220" xmlns="http://www.w3.org/2000/svg" class="my-6 w-full max-w-xl" role="img" aria-label="Astro islands architecture. A large page rendered as static HTML with zero JS. Three small interactive components are highlighted as islands — a search box, a carousel, a like button — each shipping its own small JS bundle. The rest of the page ships no JavaScript.">
  <g font-family="ui-sans-serif, system-ui, sans-serif" text-rendering="geometricPrecision">

    <!-- page body: static, no JS -->
    <rect x="30" y="30" width="540" height="160" rx="10" fill="#dcfce7" stroke="#16a34a" stroke-width="1.5"/>
    <text x="300" y="54" font-size="12" font-weight="700" fill="#052e16" text-anchor="middle">Astro page — static HTML, 0 KB JS by default</text>

    <!-- islands -->
    <rect x="60" y="80" width="130" height="44" rx="6" fill="#fef9c3" stroke="#ca8a04" stroke-width="1.5"/>
    <text x="125" y="100" font-size="11" font-weight="700" fill="#422006" text-anchor="middle">search box</text>
    <text x="125" y="115" font-size="9.5" fill="#ca8a04" text-anchor="middle">island · ships JS</text>

    <rect x="235" y="80" width="130" height="44" rx="6" fill="#fef9c3" stroke="#ca8a04" stroke-width="1.5"/>
    <text x="300" y="100" font-size="11" font-weight="700" fill="#422006" text-anchor="middle">carousel</text>
    <text x="300" y="115" font-size="9.5" fill="#ca8a04" text-anchor="middle">island · ships JS</text>

    <rect x="410" y="80" width="130" height="44" rx="6" fill="#fef9c3" stroke="#ca8a04" stroke-width="1.5"/>
    <text x="475" y="100" font-size="11" font-weight="700" fill="#422006" text-anchor="middle">like button</text>
    <text x="475" y="115" font-size="9.5" fill="#ca8a04" text-anchor="middle">island · ships JS</text>

    <text x="300" y="162" font-size="10.5" fill="#052e16" text-anchor="middle" font-style="italic">everything else — pure HTML, no runtime JS</text>

  </g>
</svg>
```

Astro also gives file-based routing and first-class markdown/mds support, which is exactly what a content site wants — but the islands idea is the part worth remembering. It's SSG's "render once, serve files" principle applied *inside* the page itself.

## SvelteKit — SSG as one mode among several

SvelteKit is a full-stack framework on top of Svelte, and it treats SSG not as its identity but as **one rendering mode you can flip on** [4]. A SvelteKit app can be SSR, SSG, or a mix, configured per page with a single adapter and page option. The same codebase, different output: prerender a marketing page to static HTML at build, SSR a dashboard at request time, CSR a tiny widget. The framework picks per route.

This is the pattern that made SSG click for me as a *strategy* rather than a *product*. Astro *is* an SSG. SvelteKit *has* an SSG mode. Next.js, Nuxt, and Remix sit in the same camp — they're hybrid renderers that can emit static output where it pays off and fall back to server or client rendering where it doesn't [2]. The build-time-vs-request-time choice isn't global; it's per page.

The Svelte half matters too. Svelte is a compile-time framework — it turns components into optimized vanilla JS (plain, framework-free JavaScript) at build time, with no virtual DOM (no in-memory draft of the page) and a tiny runtime (very little framework code shipped to the browser) [5]. That pairs naturally with SSG: if you're already compiling away the framework at build time, prerendering the whole page to static HTML is the same idea taken one step further. The bundle you ship is small, and the page is already HTML before deploy.

## react-router — client routing, not a renderer

The roadmap groups react-router under this node, and the thing I had to untangle is that **react-router isn't an SSG at all** — it's a client-side router [6]. It solves a different problem: once a React app is in the browser, how does it change "pages" without a full reload? react-router manages the URL, swaps the view, handles lazy loading (loading a route's code only when it's visited) and route guards (checks that block a route unless a condition is met), and turns a single-page app into something that *feels* like a multi-page site.

Why it sits next to SSGs: in an SSG output, or any static deployment, there's no server to handle routes — every URL has to resolve to a static file. react-router is the client-side answer to "how do I have many logical routes backed by a static bundle." The two ideas compose: SSG produces the initial HTML cheaply; react-router (or a framework's built-in router) takes over navigation in the browser afterward. Modern react-router also supports data loading and framework-style modes, but at its core it's the navigation layer, and a static file can be the entry point it loads.

## How I use this

The habit these notes left me with is one question before I reach for a framework: **how often does this content change, relative to how often it's read?** If reads vastly outnumber updates — a blog, docs, a portfolio — SSG is almost always right: build once, serve from a CDN, sleep well. If every request needs fresh or per-user data, SSG is the wrong tool and SSR or CSR is honest about it. And if most of the page is static but a corner needs life, that corner is an island, not a reason to ship a full app.

That single ratio — update frequency to read frequency — is the whole decision. Everything else (Astro vs SvelteKit, islands vs hybrid, which router) is implementation once the rendering moment is settled.

## References

[1] Cloudflare, "What is a static site generator?," Cloudflare Learning Center, 2024. [Online]. Available: [https://www.cloudflare.com/learning/performance/static-site-generator/](https://www.cloudflare.com/learning/performance/static-site-generator/)

[2] The New Stack, "Get back to basics with static website generators," 2023. [Online]. Available: [https://thenewstack.io/get-back-to-basics-static-website-generators/](https://thenewstack.io/get-back-to-basics-static-website-generators/)

[3] Astro, "Astro — The web framework for content-driven websites," 2024. [Online]. Available: [https://astro.build/](https://astro.build/)

[4] Svelte, "SvelteKit documentation," 2024. [Online]. Available: [https://kit.svelte.dev/docs/introduction](https://kit.svelte.dev/docs/introduction)

[5] The New Stack, "All about Svelte, the much-loved, state-driven web framework," 2023. [Online]. Available: [https://thenewstack.io/all-about-svelte-the-much-loved-state-driven-web-framework/](https://thenewstack.io/all-about-svelte-the-much-loved-state-driven-web-framework/)

[6] Remix Software, "React Router," 2024. [Online]. Available: [https://reactrouter.com/](https://reactrouter.com/)

```quiz
Q: What distinguishes SSG from SSR and CSR?
- It renders HTML once at build time, then serves static files; SSR and CSR render at request/click time
- It renders HTML in the browser using JavaScript, like CSR
- It renders HTML on the server for every request, like SSR
correct: 0
explain: SSG front-loads the render to the build step. The output is flat HTML served from a CDN; no rendering happens per request or per click.

Q: SSG is the best fit when content…
- changes on every request and must be personalized per user
- changes infrequently but is read very often — blogs, docs, marketing
correct: 1
explain: SSG wins when update frequency is low relative to reads. You rebuild on content change and serve the cached file to every visitor in between.

Q: What does "partial hydration" mean in Astro?
- The whole page ships a full JavaScript application bundle
- Only specific interactive components ("islands") ship JS; the rest is inert HTML
correct: 1
explain: Astro renders everything to static HTML and drops the JS by default. You opt individual components in as islands, so only those ship their runtime.

Q: How does SvelteKit relate to SSG?
- It only does SSG — nothing else
- SSG is one of several rendering modes it supports, configurable per page
correct: 1
explain: SvelteKit is a hybrid framework: the same codebase can prerender some routes to static HTML, SSR others, and CSR the rest. SSG is a mode, not its whole identity.

Q: What problem does react-router actually solve?
- It renders pages to static HTML at build time
- It provides client-side routing so a SPA can change views without a full reload
correct: 1
explain: react-router is a navigation layer, not a renderer. It manages URLs and view swapping in the browser; a static file can still be the entry point it loads.
```
