Static Site Generation in Angular

By Syncfusion®, Inc. · Last updated

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.

ng buildoncePrerenderevery routeHTML filesdocs/…/index.htmlFile hostsends the filesThe pages are created once. Every visitor gets the same ready file.

Static Site Generation: the HTML is created at build time.

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:

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
PointCSRSSRSSG
HTML createdIn the browserOn the server, for each requestOnce, at build time
First viewSlowFastFastest
DataAlways currentAlways currentFrom the last build
HostingFile hostNode.js serverFile 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:

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:

"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:

getPrerenderParams()returns the slugs{ slug: "installation" }docs/installation/index.html{ slug: "components" }docs/components/index.html{ slug: "routing" }docs/routing/index.html

One object from getPrerenderParams() becomes one generated page.

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:

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:

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.

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.

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.

// 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.

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:

Edit contentdocs.tsng buildrenders all pagesUploaddist/.../browserUsers see itnew HTML filesThe pages change only after a new build and upload.

Updating a static site: edit, build, upload.

  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 forUse SSR or CSR for
Documentation, blogs, marketing pagesPages that change every minute
Content that changes with releasesPages 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.

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

Download angular-docs-site-ssg.zip

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

FileWhat it contains
src/app/content/docs.tsAll pages and categories
src/app/app.routes.server.tsRenderMode.Prerender and getPrerenderParams()
src/app/layout/docs-layout.tsThe reusable layout with the side menu
src/app/pages/doc-page.tsOne documentation page
scripts/serve-static.mjsA 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:

    npm install
  3. Generate the pages:

    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:

    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

MistakeWhat you seeFix
A parameterized route without getPrerenderParams()Its pages are missing from the buildReturn one object per page from getPrerenderParams()
Writing the slugs by handNew pages are forgottenGenerate them from the content with map()
Expecting new content without a new buildThe site still shows the old textRun ng build again and upload the files
Using SSG for pages that are different for each userEvery user sees the same generated pageUse CSR or SSR for those routes
Browser-only code in componentsThe build fails, because it renders on the build machineUse afterNextRender(), as with SSR

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.