# Static Site Generation

Course: [Angular](https://www.devtutorials.ai/learn/angular) · Section: Render Modes
Source: https://www.devtutorials.ai/learn/angular/rendering/static-site-generation

> Generating HTML files at build time: prerendering static and parameterized routes, build-time data, deployment, and a Documentation Website capstone project.

## Introduction to Static Site Generation

SSR renders a page for **every** request. But many pages are the same for every visitor and change rarely, for example documentation, a company's "About" page or a blog post that never changes.

For these pages, rendering on every request is wasted work. You can render them **once** and send the same finished file to every visitor.

## What is Static Site Generation (SSG)?

In **Static Site Generation (SSG)**, Angular renders your pages when you **build** the app with `ng build`. For each route, it saves the HTML in a file, for example `docs/routing/index.html`.

A **static** file is a file that does not change between requests. A file hosting service sends these files to the browser, and no server code runs. In the browser, the page is hydrated, the same as with SSR.

SSG is also called **prerendering**.

### Real-World Example

A software product has 300 documentation pages that change only when a new version is released. When the team runs `ng build`, Angular creates one HTML file for each page.

A file hosting service sends the files to readers around the world. Each page opens immediately, and no server needs to render anything.

<figure className="ngc-fig">
<svg viewBox="0 0 640 140" role="img" aria-label="Static site generation: ng build prerenders every route into HTML files, and a file host sends them to visitors.">
<defs><marker id="ngc-c3" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0 0L10 5L0 10z" className="ngc-arrowhead" /></marker></defs><g className="ngc-step ngc-t1"><g className="ngc-node ngc-soft c-blue"><rect x="6" y="36" width="142" height="64" rx="9" /><text x="77.0" y="65" textAnchor="middle" style={{fontSize:"14px"}}>ng build</text><text className="ngc-sub" x="77.0" y="83" textAnchor="middle">once</text></g></g><line className="ngc-link ngc-flow c-slate" x1="150" y1="68" x2="162" y2="68" markerEnd="url(#ngc-c3)" /><g className="ngc-step ngc-t2"><g className="ngc-node ngc-soft c-purple"><rect x="164" y="36" width="142" height="64" rx="9" /><text x="235.0" y="65" textAnchor="middle" style={{fontSize:"14px"}}>Prerender</text><text className="ngc-sub" x="235.0" y="83" textAnchor="middle">every route</text></g></g><line className="ngc-link ngc-flow c-slate" x1="308" y1="68" x2="320" y2="68" markerEnd="url(#ngc-c3)" /><g className="ngc-step ngc-t3"><g className="ngc-node ngc-soft c-green"><rect x="322" y="36" width="142" height="64" rx="9" /><text x="393.0" y="65" textAnchor="middle" style={{fontSize:"14px"}}>HTML files</text><text className="ngc-sub" x="393.0" y="83" textAnchor="middle">docs/…/index.html</text></g></g><line className="ngc-link ngc-flow c-slate" x1="466" y1="68" x2="478" y2="68" markerEnd="url(#ngc-c3)" /><g className="ngc-step ngc-t4"><g className="ngc-node ngc-soft c-orange"><rect x="480" y="36" width="142" height="64" rx="9" /><text x="551.0" y="65" textAnchor="middle" style={{fontSize:"14px"}}>File host</text><text className="ngc-sub" x="551.0" y="83" textAnchor="middle">sends the files</text></g></g><text className="ngc-note" x="320" y="124" textAnchor="middle">The pages are created once. Every visitor gets the same ready file.</text>
</svg>
<figcaption>Static Site Generation: the HTML is created at build time.</figcaption>
</figure>

## Benefits of SSG

- **Fastest response.** The HTML is ready, so the host only sends a file.
- **Cheap and simple hosting.** Any file hosting service works. No Node.js server is needed.
- **Full HTML for search engines**, the same as SSR.
- **Very reliable.** No server code runs, so there is nothing that can crash during a request.

## SSG Architecture

### Build-Time Rendering

**Build time** is the moment when you run `ng build`. **Request time** is the moment when a user opens a page. SSR renders at request time. SSG renders at build time.

### Static HTML Generation

During the build, Angular runs your app once for each route. It renders the components, including data from `HttpClient`, and saves the result as an HTML file.

### Pre-rendered Pages

The result is a folder of ready files in `dist/<project-name>/browser`:

```text
dist/docs-site/browser/
├── index.html                    ← the home page
├── docs/
│   ├── installation/index.html   ← /docs/installation
│   ├── components/index.html     ← /docs/components
│   └── routing/index.html        ← /docs/routing
├── main-XYZ.js
└── styles-XYZ.css
```

| Point | CSR | SSR | SSG |
|---|---|---|---|
| HTML created | In the browser | On the server, for each request | Once, at build time |
| First view | Slow | Fast | Fastest |
| Data | Always current | Always current | From the last build |
| Hosting | File host | Node.js server | File host |

## SSG Rendering Fundamentals

### Prerendering

You turn on SSG with the same `@angular/ssr` package as SSR (`ng new --ssr` or `ng add @angular/ssr`). Then you choose `RenderMode.Prerender` for the routes in `app.routes.server.ts`:

```ts
import { RenderMode, ServerRoute } from '@angular/ssr';

export const serverRoutes: ServerRoute[] = [
  { path: '**', renderMode: RenderMode.Prerender },
];
```

When **all** routes are prerendered, you do not need a server. Set `"outputMode": "static"` in `angular.json`, and `ng build` creates only static files:

```json
"build": {
  "options": {
    "outputMode": "static"
  }
}
```

### Static Route Prerendering

A **static route** has a fixed path, such as `''` or `about`. Angular knows its URL, so it prerenders it automatically.

### Parameterized Route Prerendering

In the app itself, a parameterized route is a normal route with `:slug`. One component displays every page:

<figure className="ngc-fig">
<svg viewBox="0 0 640 206" role="img" aria-label="getPrerenderParams returns three slugs, and the build creates one HTML file for each.">
<defs><marker id="ngc-x2" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0 0L10 5L0 10z" className="ngc-arrowhead" /></marker></defs><g className="ngc-step ngc-t1"><g className="ngc-node ngc-solid c-purple"><rect x="10" y="70" width="180" height="64" rx="9" /><text x="100.0" y="99" textAnchor="middle" style={{fontSize:"14px"}}>getPrerenderParams()</text><text className="ngc-sub" x="100.0" y="117" textAnchor="middle">returns the slugs</text></g></g><line className="ngc-link ngc-flow c-slate" x1="192" y1="102" x2="226" y2="35" markerEnd="url(#ngc-x2)" /><g className="ngc-step ngc-t2"><g className="ngc-node ngc-soft c-orange"><rect x="228" y="10" width="170" height="50" rx="9" /><text x="313.0" y="40" textAnchor="middle" style={{fontSize:"13px"}}>&#123; slug: "installation" &#125;</text></g></g><line className="ngc-link ngc-flow c-slate" x1="400" y1="35" x2="420" y2="35" markerEnd="url(#ngc-x2)" /><g className="ngc-step ngc-t2"><g className="ngc-node ngc-soft c-green"><rect x="422" y="10" width="212" height="50" rx="9" /><text x="528.0" y="40" textAnchor="middle" style={{fontSize:"13px"}}>docs/installation/index.html</text></g></g><line className="ngc-link ngc-flow c-slate" x1="192" y1="102" x2="226" y2="101" markerEnd="url(#ngc-x2)" /><g className="ngc-step ngc-t3"><g className="ngc-node ngc-soft c-orange"><rect x="228" y="76" width="170" height="50" rx="9" /><text x="313.0" y="106" textAnchor="middle" style={{fontSize:"13px"}}>&#123; slug: "components" &#125;</text></g></g><line className="ngc-link ngc-flow c-slate" x1="400" y1="101" x2="420" y2="101" markerEnd="url(#ngc-x2)" /><g className="ngc-step ngc-t3"><g className="ngc-node ngc-soft c-green"><rect x="422" y="76" width="212" height="50" rx="9" /><text x="528.0" y="106" textAnchor="middle" style={{fontSize:"13px"}}>docs/components/index.html</text></g></g><line className="ngc-link ngc-flow c-slate" x1="192" y1="102" x2="226" y2="167" markerEnd="url(#ngc-x2)" /><g className="ngc-step ngc-t4"><g className="ngc-node ngc-soft c-orange"><rect x="228" y="142" width="170" height="50" rx="9" /><text x="313.0" y="172" textAnchor="middle" style={{fontSize:"13px"}}>&#123; slug: "routing" &#125;</text></g></g><line className="ngc-link ngc-flow c-slate" x1="400" y1="167" x2="420" y2="167" markerEnd="url(#ngc-x2)" /><g className="ngc-step ngc-t4"><g className="ngc-node ngc-soft c-green"><rect x="422" y="142" width="212" height="50" rx="9" /><text x="528.0" y="172" textAnchor="middle" style={{fontSize:"13px"}}>docs/routing/index.html</text></g></g>
</svg>
<figcaption>One object from getPrerenderParams() becomes one generated page.</figcaption>
</figure>

```angular runnable title="One route for many pages"
import { Component, Input } from '@angular/core';
import { RouterLink, RouterOutlet, Routes } from '@angular/router';

const TITLES: Record<string, string> = {
  installation: 'Installation',
  components: 'Components',
  routing: 'Routing',
};

@Component({
  selector: 'app-doc-page',
  template: `<p>Page: <b>{{ title }}</b> (slug: {{ slugText }})</p>`,
})
export class DocPage {
  title = '';
  slugText = '';

  @Input() set slug(value: string) {
    this.slugText = value;
    this.title = TITLES[value] ?? 'Not found';
  }
}

export const routes: Routes = [
  { path: '', redirectTo: 'docs/installation', pathMatch: 'full' },
  { path: 'docs/:slug', component: DocPage },
];

@Component({
  selector: 'app-root',
  imports: [RouterOutlet, RouterLink],
  template: `
    <a routerLink="/docs/installation">Installation</a> |
    <a routerLink="/docs/components">Components</a> |
    <a routerLink="/docs/routing">Routing</a>
    <router-outlet />
  `,
})
export class App {}
```

For SSG, Angular must know the three slugs before the build. That is the job of `getPrerenderParams()`.

A **parameterized route** has a route parameter, such as `docs/:slug`. Angular does not know which slugs exist, so you must tell it with **`getPrerenderParams()`**. This function returns one object for each page to generate:

```ts
export const serverRoutes: ServerRoute[] = [
  {
    path: 'docs/:slug',
    renderMode: RenderMode.Prerender,
    async getPrerenderParams() {
      return [{ slug: 'installation' }, { slug: 'components' }, { slug: 'routing' }];
    },
  },
  { path: '**', renderMode: RenderMode.Prerender },
];
```

Angular then generates `/docs/installation`, `/docs/components` and `/docs/routing`.

### Build-Time Route Generation

Writing the slugs by hand is easy to forget. Generate them from your content instead. The function runs at build time, so it can read your content list or request an API:

```ts
async getPrerenderParams() {
  return DOCS.map(doc => ({ slug: doc.slug }));
}
```

The next example shows the same idea in the browser: it creates the list of paths from the content.

```angular runnable title="Generating the list of pages from the content"
import { Component } from '@angular/core';

const DOCS = [
  { slug: 'installation', title: 'Installation' },
  { slug: 'components', title: 'Components' },
  { slug: 'routing', title: 'Routing' },
];

// Like getPrerenderParams(): one object for each page
function prerenderParams() {
  return DOCS.map(doc => ({ slug: doc.slug }));
}

@Component({
  selector: 'app-root',
  template: `
    <p>ng build will generate these pages:</p>
    @for (params of pages; track params.slug) {
      <p>/docs/{{ params.slug }} → docs/{{ params.slug }}/index.html</p>
    }
  `,
})
export class App {
  pages = prerenderParams();
}
```

Add an object to `DOCS` and click **Run**: the new page appears in the list.

A parameterized route without `getPrerenderParams()` is not prerendered. Users who open its URL directly get no generated page.

## Content Generation

### Build-Time Data Fetching

`HttpClient` requests in your components run during the build, and their answers are saved in the HTML. So the page contains the data that the API had **at build time**.

The next example loads the list of pages with `HttpClient`. With SSG, this request runs during `ng build`, and the list is saved in the HTML.

```angular runnable title="Data loaded for the generated page"
import { ApplicationConfig, Component, inject, signal } from '@angular/core';
import { HttpClient, HttpHandlerFn, HttpRequest, HttpResponse, provideHttpClient, withInterceptors } from '@angular/common/http';
import { delay, of } from 'rxjs';

export const appConfig: ApplicationConfig = {
  providers: [provideHttpClient(withInterceptors([practiceServer]))],
};

@Component({
  selector: 'app-root',
  template: `
    <h4>What's new in version 2.0</h4>
    @for (item of items(); track $index) {
      <p>• {{ item }}</p>
    }
  `,
})
export class App {
  items = signal<string[]>([]);

  constructor() {
    inject(HttpClient)
      .get<string[]>('/api/release-notes')
      .subscribe(list => this.items.set(list));
  }
}

// ─── Practice server ──────────────────────────────────────────────
// Answers the request inside the browser, like in the API Integration lesson.
function practiceServer(req: HttpRequest<unknown>, next: HttpHandlerFn) {
  const notes = ['A new search page', 'Faster builds', 'Dark mode'];
  return of(new HttpResponse({ status: 200, body: notes })).pipe(delay(300));
}
```

If the release notes change after the build, the generated page still shows the old list until the next build.
### Static Content Management

A simple way to manage the content of a static site is a TypeScript file with the content as data. Components read this data, and the build turns it into pages.

```ts
// content/docs.ts
export const DOCS: DocPage[] = [
  { slug: 'installation', category: 'Getting Started', title: 'Installation', paragraphs: ['…'] },
  { slug: 'routing', category: 'Guides', title: 'Routing', paragraphs: ['…'] },
];
```

To change a page, you edit the data and build again. Larger sites keep the content in Markdown files or a content management system (an app where editors write the content), and read it in the same way during the build.

```angular runnable title="Content grouped by category"
import { Component } from '@angular/core';

const CATEGORIES = ['Getting Started', 'Guides', 'Reference'];

const DOCS = [
  { slug: 'installation', category: 'Getting Started', title: 'Installation' },
  { slug: 'first-project', category: 'Getting Started', title: 'Your First Project' },
  { slug: 'components', category: 'Guides', title: 'Components' },
  { slug: 'routing', category: 'Guides', title: 'Routing' },
  { slug: 'cli-commands', category: 'Reference', title: 'CLI Commands' },
];

@Component({
  selector: 'app-root',
  template: `
    @for (category of categories; track category) {
      <p><b>{{ category }}</b></p>
      @for (doc of docsIn(category); track doc.slug) {
        <p>• {{ doc.title }}</p>
      }
    }
  `,
})
export class App {
  categories = CATEGORIES;

  docsIn(category: string) {
    return DOCS.filter(doc => doc.category === category);
  }
}
```

## Deployment and Delivery

### Static Assets

**Static assets** are all the files in `dist/<project-name>/browser`: the HTML pages, the JavaScript, the CSS, images and fonts. To publish the site, you upload this folder. Nothing else is needed.

Before you upload, test the generated files on your computer with a simple static server, not with `ng serve`. `ng serve` renders pages during development, so it does not show you the files that the build created.

### Content Distribution

Static files can be copied to many servers around the world. Such a network is called a **CDN** (Content Delivery Network). Each reader gets the files from a server near them, so pages open fast everywhere. Most file hosting services use a CDN automatically.

### Updating Generated Pages

The generated pages do not change by themselves. To publish new or changed content:

<figure className="ngc-fig">
<svg viewBox="0 0 640 134" role="img" aria-label="Change the content, run ng build, upload the new files, and users see the new pages.">
<defs><marker id="ngc-x3" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0 0L10 5L0 10z" className="ngc-arrowhead" /></marker></defs><g className="ngc-step ngc-t1"><g className="ngc-node ngc-solid c-blue"><rect x="6" y="30" width="145" height="64" rx="9" /><text x="78.5" y="59" textAnchor="middle" style={{fontSize:"14px"}}>Edit content</text><text className="ngc-sub" x="78.5" y="77" textAnchor="middle">docs.ts</text></g></g><line className="ngc-link ngc-flow c-slate" x1="152" y1="62" x2="166" y2="62" markerEnd="url(#ngc-x3)" /><g className="ngc-step ngc-t2"><g className="ngc-node ngc-soft c-purple"><rect x="167" y="30" width="145" height="64" rx="9" /><text x="239.5" y="59" textAnchor="middle" style={{fontSize:"14px"}}>ng build</text><text className="ngc-sub" x="239.5" y="77" textAnchor="middle">renders all pages</text></g></g><line className="ngc-link ngc-flow c-slate" x1="313" y1="62" x2="327" y2="62" markerEnd="url(#ngc-x3)" /><g className="ngc-step ngc-t3"><g className="ngc-node ngc-soft c-orange"><rect x="328" y="30" width="145" height="64" rx="9" /><text x="400.5" y="59" textAnchor="middle" style={{fontSize:"14px"}}>Upload</text><text className="ngc-sub" x="400.5" y="77" textAnchor="middle">dist/.../browser</text></g></g><line className="ngc-link ngc-flow c-slate" x1="474" y1="62" x2="488" y2="62" markerEnd="url(#ngc-x3)" /><g className="ngc-step ngc-t4"><g className="ngc-node ngc-soft c-green"><rect x="489" y="30" width="145" height="64" rx="9" /><text x="561.5" y="59" textAnchor="middle" style={{fontSize:"14px"}}>Users see it</text><text className="ngc-sub" x="561.5" y="77" textAnchor="middle">new HTML files</text></g></g><circle className="ngc-packet c-purple" r="5"><animateMotion dur="3.6s" repeatCount="indefinite" path="M78 106 L562 106" /></circle><text className="ngc-note" x="320" y="128" textAnchor="middle">The pages change only after a new build and upload.</text>
</svg>
<figcaption>Updating a static site: edit, build, upload.</figcaption>
</figure>

1. Change the content, for example in `docs.ts`.
2. Run `ng build` again.
3. Upload the new `browser` folder.

Many teams let a build service do these steps automatically each time the content changes.

You can mix render modes in one app. For example, use `RenderMode.Prerender` for documentation pages, `RenderMode.Server` for a search page, and `RenderMode.Client` (CSR) for a page behind a login. This needs a Node.js server again, because some routes are rendered at request time.

## Advantages and Limitations of SSG

**Advantages:**

- The fastest pages, because the HTML is ready.
- Cheap, simple and reliable hosting on any file host.
- Full HTML for search engines and link previews.

**Limitations:**

- **Content is only as new as the last build.** Changes need a new build.
- **Long builds for very large sites.** Every page is rendered during the build.
- **Not for personal pages.** A page that is different for each user, such as "My orders", cannot be generated in advance.

| Use SSG for | Use SSR or CSR for |
|---|---|
| Documentation, blogs, marketing pages | Pages that change every minute |
| Content that changes with releases | Pages that are different for each user |

## Capstone Project: Documentation Website

You now build a **Documentation Website** that is generated at build time.

### Create Documentation Pages

Each page is an object in `DOCS` with a `slug`, a `category`, a `title` and its paragraphs. `DocPage` displays one page.

### Generate Pages at Build Time

In the downloadable project, `getPrerenderParams()` returns one slug for each object in `DOCS`, and `outputMode` is `static`.

### Implement Navigation

A side menu shows a `routerLink` for each page. `routerLinkActive` highlights the open page.

### Organize Content Categories

The menu groups the pages by category with `docsIn(category)`.

### Build Reusable Layouts

`DocsLayout` contains the side menu and a `<router-outlet />`. Every documentation page is a child route, so all pages share the same layout.

### Configure Static Routes

The routes are `''` (home), `docs` with the child route `:slug`, and a redirect from `docs` to the first page.

```angular runnable title="Capstone: Documentation Website"
import { Component, Input } from '@angular/core';
import { RouterLink, RouterLinkActive, RouterOutlet, Routes } from '@angular/router';

// ── Content ──
export interface Doc {
  slug: string;
  category: string;
  title: string;
  text: string;
}

const CATEGORIES = ['Getting Started', 'Guides'];

const DOCS: Doc[] = [
  { slug: 'installation', category: 'Getting Started', title: 'Installation', text: 'Install Node.js and run npm install.' },
  { slug: 'first-project', category: 'Getting Started', title: 'First Project', text: 'Run ng new my-app and ng serve.' },
  { slug: 'components', category: 'Guides', title: 'Components', text: 'A component controls one part of the screen.' },
  { slug: 'routing', category: 'Guides', title: 'Routing', text: 'Routing connects each URL to one screen.' },
];

// ── One documentation page ──
@Component({
  selector: 'app-doc-page',
  template: `
    @if (doc) {
      <h4>{{ doc.title }}</h4>
      <p>{{ doc.text }}</p>
    } @else {
      <p>Page not found.</p>
    }
  `,
})
export class DocPage {
  doc: Doc | undefined;

  @Input() set slug(value: string) {
    this.doc = DOCS.find(d => d.slug === value);
  }
}

// ── Reusable layout ──
@Component({
  selector: 'app-docs-layout',
  imports: [RouterOutlet, RouterLink, RouterLinkActive],
  template: `
    <div style="display: flex; gap: 24px">
      <nav>
        @for (category of categories; track category) {
          <p><b>{{ category }}</b></p>
          @for (doc of docsIn(category); track doc.slug) {
            <p><a [routerLink]="['/docs', doc.slug]" routerLinkActive="active">{{ doc.title }}</a></p>
          }
        }
      </nav>
      <section><router-outlet /></section>
    </div>
  `,
  styles: `.active { font-weight: bold; color: #c3002f; }`,
})
export class DocsLayout {
  categories = CATEGORIES;
  docsIn(category: string) {
    return DOCS.filter(d => d.category === category);
  }
}

// ── Routes ──
export const routes: Routes = [
  { path: '', redirectTo: 'docs', pathMatch: 'full' },
  {
    path: 'docs',
    component: DocsLayout,
    children: [
      { path: '', redirectTo: 'installation', pathMatch: 'full' },
      { path: ':slug', component: DocPage },
    ],
  },
];

@Component({
  selector: 'app-root',
  imports: [RouterOutlet],
  template: `<b>📘 Docs Site</b><router-outlet />`,
})
export class App {}
```

Click the pages in the menu. In the downloadable project, each of these pages is a separate HTML file.

## Download Project Files

### Download SSG Project Source Code

<a href="/downloads/angular-docs-site-ssg.zip" download>Download angular-docs-site-ssg.zip</a>

The ZIP file contains the Documentation Website as a complete Angular project with `outputMode: "static"`.

| File | What it contains |
|---|---|
| `src/app/content/docs.ts` | All pages and categories |
| `src/app/app.routes.server.ts` | `RenderMode.Prerender` and `getPrerenderParams()` |
| `src/app/layout/docs-layout.ts` | The reusable layout with the side menu |
| `src/app/pages/doc-page.ts` | One documentation page |
| `scripts/serve-static.mjs` | A small Node.js server that sends the generated files |

### Project Setup Instructions

You need **Node.js 20.19 or newer**.

1. Unzip the file and open a terminal in the `docs-site` folder.
2. Install the packages:

   ```bash
   npm install
   ```

3. Generate the pages:

   ```bash
   npm run build
   ```

   The output ends with "Prerendered 7 static routes". The pages are in `dist/docs-site/browser`.

4. Send the generated files with the included static server:

   ```bash
   npm run serve:static
   ```

5. Open `http://localhost:8080`.

To check SSG, open `dist/docs-site/browser/docs/routing/index.html` in an editor: it already contains the text of the page. During development, you can also use `npm start`.

## Common Mistakes

<div className="ngc-mistakes">

| Mistake | What you see | Fix |
|---|---|---|
| A parameterized route without `getPrerenderParams()` | Its pages are missing from the build | Return one object per page from `getPrerenderParams()` |
| Writing the slugs by hand | New pages are forgotten | Generate them from the content with `map()` |
| Expecting new content without a new build | The site still shows the old text | Run `ng build` again and upload the files |
| Using SSG for pages that are different for each user | Every user sees the same generated page | Use CSR or SSR for those routes |
| Browser-only code in components | The build fails, because it renders on the build machine | Use `afterNextRender()`, as with SSR |

</div>

## Check Yourself

## Exercises

## What to Remember

- SSG renders each route once, at build time, and saves the HTML as files. Turn it on with `RenderMode.Prerender`, and use `outputMode: "static"` when no route needs a server.
- Parameterized routes need `getPrerenderParams()`. Generate the parameters from your content, so new pages are included automatically.
- SSG gives the fastest pages and the simplest hosting, but content changes need a new build, and personal pages need CSR or SSR.

Next: **User Access Control**, how to log users in and decide which pages each user may open.
