| |

Angular 77 🅰️ Prerendering and Static Site Generation

In the previous chapters, you set up Angular Universal for server-side rendering and learned how hydration makes the server-rendered HTML interactive. But SSR is not the only way to deliver pre-rendered HTML. When your content is the same for every user — marketing pages, blog posts, documentation, product catalogs with stable data — rendering on every request is wasted work. The server does the same job over and over. Prerendering solves this by generating the HTML once, at build time, and serving it as static files.

Prerendering, also called Static Site Generation (SSG), is the strategy where pages are rendered to static HTML files during the build process . Instead of a server rendering the page when a user requests it, Angular renders it ahead of time and stores the result. The browser receives a complete HTML document immediately, with no server computation required. The performance benefits of SSR — fast first paint, full SEO content — are preserved, but Time to First Byte is reduced because the server does nothing but send a file .

This chapter covers when to use prerendering, how to configure it with the Angular CLI, how to handle parameterized routes with getPrerenderParams, and how to choose between outputMode: "static" for a fully static site versus the default hybrid approach that generates a server file as well.

Key point: Prerendering generates static HTML at build time. It is not a replacement for SSR — it is an alternative for content that does not change per request. If the data is the same for every user, prerender it. If the data is user-specific or changes frequently, use SSR or client-side rendering .


Why prerendering exists

SSR solved the blank page problem and the SEO problem, but it introduced a new cost: every request triggers a server render. For content that is identical for all users, that work is redundant.

The redundant computation problem. A blog post’s content is the same whether one person reads it or a thousand. With SSR, the server re-renders the same page for every visitor. With prerendering, the page is rendered once during the build, and the resulting HTML file is served to everyone . The server’s work is done before the first user arrives.

The server cost problem. SSR requires a running Node.js server to handle requests. That server consumes CPU and memory on every page view. Prerendered pages can be served from a CDN or a static file server, with no application runtime at all . This is the difference between renting a kitchen and preparing a meal for every customer, versus preparing all the meals in advance and putting them on a shelf.

The latency problem. A prerendered page is a static file. The CDN serves it from the edge location closest to the user. There is no server-side rendering, no database query, no Angular bootstrap on the server. The time to first byte is as low as it can be for any HTML document .

The build-time problem. Prerendering requires a build step that generates every page. If you have a thousand product pages, the build renders a thousand HTML files. This takes time. And when the content changes — a new blog post, a price update — you must rebuild and redeploy . The trade-off is clear: faster serving, slower updates.

The trade-off. Prerendering is not universally better than SSR. It is better for content that does not change per user. For dashboards, shopping carts, and personalized feeds, SSR or CSR is the right choice. The decision is about whether the content is static or dynamic .


a. Enabling Prerendering and Configuring Routes

Prerendering is part of Angular’s SSR infrastructure. To add it to a project, you run the same command that adds SSR:

ng add @angular/ssr

Or, for a new project:

ng new my-app --ssr

Once SSR is added, prerendering is configured through the prerender option in angular.json under the build target’s options . The option can be a boolean or an object. When true, Angular attempts to discover and prerender all unparameterized routes automatically. When false, no prerendering is done. When it is an object, you can configure discoverRoutes and routesFile .

The default behavior is prerender: true with discoverRoutes: true. Angular reads your router configuration, finds every route without parameters, and generates a static HTML file for each . This is convenient for small applications with simple routes, but it can cause problems when you have routes that should not be prerendered, such as authenticated dashboards or routes that depend on browser-only APIs.

The recommended configuration disables automatic discovery and specifies routes explicitly:

{
  "projects": {
    "my-app": {
      "architect": {
        "build": {
          "builder": "@angular-devkit/build-angular:application",
          "options": {
            "prerender": {
              "discoverRoutes": false,
              "routesFile": "routes.txt"
            }
          }
        }
      }
    }
  }
}

The routes.txt file contains one route per line. Each line is a path that should be prerendered :

/
/about
/blog
/contact

When you run ng build, Angular generates an index.html for each route. If you look in the dist/my-app/browser directory, you will find a folder structure that mirrors the routes. /about becomes about/index.html, /blog becomes blog/index.html, and so on .


b. Parameterized Routes and getPrerenderParams

Static route lists cannot cover parameterized routes. /products/1 and /products/555 are two different pages, but the route definition is /products/:id. Prerendering needs to know which specific IDs to generate.

The getPrerenderParams function solves this. It is defined per route in the server routing configuration, and it returns an array of parameter objects. Each object represents one specific path that should be prerendered .

// app.routes.server.ts
import { RenderMode, ServerRoute } from '@angular/ssr';

export const serverRoutes: ServerRoute[] = [
  {
    path: 'products/:id',
    renderMode: RenderMode.Prerender,
    async getPrerenderParams() {
      const productService = inject(ProductService);
      const ids = await productService.getAllIds();
      return ids.map(id => ({ id }));
    },
  },
  {
    path: '**',
    renderMode: RenderMode.Server,
  },
];

When the build runs, Angular calls getPrerenderParams, receives the list of IDs, and prerenders a page for each one . The inject function works inside getPrerenderParams because it runs in an injection context. The function can call services, fetch data from an API, or read from a database — anything needed to determine which paths to generate .

One important constraint: inject must be called synchronously. It cannot be used inside an asynchronous callback or after an await statement. The function itself can be async, but the injection of dependencies must happen before the first await .

For routes where some parameters are not prerendered, the fallback option determines what happens. PrerenderFallback.Server (the default) uses SSR for non-prerendered paths. PrerenderFallback.Client uses client-side rendering. PrerenderFallback.None returns a 404 .


c. Fully Static Sites with outputMode

By default, prerendering generates a server file along with the static HTML. This allows the application to fall back to SSR for routes that were not prerendered. If you want a fully static site with no server at all, you can set outputMode to "static" in angular.json .

{
  "projects": {
    "my-app": {
      "architect": {
        "build": {
          "options": {
            "outputMode": "static"
          }
        }
      }
    }
  }
}

With outputMode: "static", Angular generates only the prerendered HTML files. No server file is produced. The application can be deployed to any static hosting provider — Netlify, Vercel, GitHub Pages, S3, Cloudflare Pages — without a Node.js runtime . The static files are served directly, and hydration makes them interactive in the browser .

This is the simplest deployment model for an Angular application. There is no server to maintain, no runtime costs, no scaling concerns. The content is served from a CDN, and every user gets the same HTML. The trade-off is that no route can fall back to SSR. Every route that a user visits must either be prerendered or rendered client-side .

For applications with a mix of static and dynamic content, the default hybrid approach is better. Prerender the static pages, and let the server handle the dynamic ones. For applications that are entirely static — blogs, documentation, marketing sites — outputMode: "static" is the right choice.


Complete Example Session

This session builds a blog with prerendered posts, dynamic product pages with getPrerenderParams, and a static output configuration.

// ============================================
// PART 1: THE ROUTES
// ============================================

// app.routes.ts
import { Routes } from '@angular/router';
import { HomeComponent } from './pages/home.component';
import { AboutComponent } from './pages/about.component';
import { BlogPostComponent } from './pages/blog-post.component';
import { ProductComponent } from './pages/product.component';

export const routes: Routes = [
  { path: '', component: HomeComponent },
  { path: 'about', component: AboutComponent },
  { path: 'blog/:slug', component: BlogPostComponent },
  { path: 'products/:id', component: ProductComponent },
];
// ============================================
// PART 2: THE SERVER ROUTES
// ============================================

// app.routes.server.ts
import { RenderMode, ServerRoute, PrerenderFallback } from '@angular/ssr';
import { inject } from '@angular/core';
import { BlogService } from './services/blog.service';
import { ProductService } from './services/product.service';

export const serverRoutes: ServerRoute[] = [
  {
    path: '',
    renderMode: RenderMode.Prerender,
  },
  {
    path: 'about',
    renderMode: RenderMode.Prerender,
  },
  {
    path: 'blog/:slug',
    renderMode: RenderMode.Prerender,
    async getPrerenderParams() {
      const blogService = inject(BlogService);
      const posts = await blogService.getAllPosts();
      return posts.map(post => ({ slug: post.slug }));
    },
  },
  {
    path: 'products/:id',
    renderMode: RenderMode.Prerender,
    async getPrerenderParams() {
      const productService = inject(ProductService);
      const products = await productService.getFeaturedProducts();
      return products.map(product => ({ id: product.id }));
    },
    fallback: PrerenderFallback.Server,
  },
  {
    path: '**',
    renderMode: RenderMode.Server,
  },
];
// ============================================
// PART 3: THE BUILD CONFIGURATION
// ============================================

// angular.json
{
  "projects": {
    "blog-app": {
      "architect": {
        "build": {
          "builder": "@angular-devkit/build-angular:application",
          "options": {
            "outputPath": "dist/blog-app",
            "index": "src/index.html",
            "browser": "src/main.ts",
            "server": "src/main.server.ts",
            "prerender": {
              "discoverRoutes": false
            },
            "ssr": {
              "entry": "server.ts"
            }
          },
          "configurations": {
            "production": {
              "outputMode": "static"
            }
          }
        }
      }
    }
  }
}
// ============================================
// PART 4: THE BLOG SERVICE
// ============================================

// services/blog.service.ts
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { firstValueFrom } from 'rxjs';

export interface BlogPost {
  slug: string;
  title: string;
  content: string;
  publishedAt: string;
}

@Injectable({ providedIn: 'root' })
export class BlogService {
  private http = inject(HttpClient);

  async getAllPosts(): Promise<BlogPost[]> {
    return firstValueFrom(this.http.get<BlogPost[]>('/api/posts'));
  }

  async getPost(slug: string): Promise<BlogPost> {
    return firstValueFrom(this.http.get<BlogPost>(`/api/posts/${slug}`));
  }
}
// ============================================
// PART 5: THE PRODUCT SERVICE
// ============================================

// services/product.service.ts
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { firstValueFrom } from 'rxjs';

export interface Product {
  id: string;
  name: string;
  price: number;
}

@Injectable({ providedIn: 'root' })
export class ProductService {
  private http = inject(HttpClient);

  async getFeaturedProducts(): Promise<Product[]> {
    return firstValueFrom(this.http.get<Product[]>('/api/products/featured'));
  }
}
// ============================================
// PART 6: THE BLOG POST COMPONENT
// ============================================

// pages/blog-post.component.ts
import { Component, inject, OnInit, signal } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { BlogService, BlogPost } from '../services/blog.service';

@Component({
  selector: 'app-blog-post',
  template: `
    @if (post()) {
      <article>
        <h1>{{ post()!.title }}</h1>
        <p>{{ post()!.publishedAt | date }}</p>
        <div [innerHTML]="post()!.content"></div>
      </article>
    } @else {
      <p>Loading...</p>
    }
  `,
})
export class BlogPostComponent implements OnInit {
  private route = inject(ActivatedRoute);
  private blogService = inject(BlogService);
  post = signal<BlogPost | null>(null);

  ngOnInit() {
    const slug = this.route.snapshot.paramMap.get('slug');
    if (slug) {
      this.blogService.getPost(slug).then(p => this.post.set(p));
    }
  }
}
// ============================================
// PART 7: THE PRODUCT COMPONENT
// ============================================

// pages/product.component.ts
import { Component, inject, OnInit, signal } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { HttpClient } from '@angular/common/http';
import { firstValueFrom } from 'rxjs';
import { Product } from '../services/product.service';

@Component({
  selector: 'app-product',
  template: `
    @if (product()) {
      <h1>{{ product()!.name }}</h1>
      <p>{{ product()!.price | currency }}</p>
    }
  `,
})
export class ProductComponent implements OnInit {
  private route = inject(ActivatedRoute);
  private http = inject(HttpClient);
  product = signal<Product | null>(null);

  ngOnInit() {
    const id = this.route.snapshot.paramMap.get('id');
    if (id) {
      firstValueFrom(this.http.get<Product>(`/api/products/${id}`))
        .then(p => this.product.set(p));
    }
  }
}
// ============================================
// PART 8: THE PRERENDERED OUTPUT
// ============================================

// After running ng build, the dist folder contains:
// dist/blog-app/browser/
// ├── index.html                    (home)
// ├── about/index.html              (about)
// ├── blog/
// │   ├── first-post/index.html     (blog/first-post)
// │   ├── second-post/index.html    (blog/second-post)
// │   └── third-post/index.html     (blog/third-post)
// ├── products/
// │   ├── 1/index.html
// │   ├── 2/index.html
// │   └── 3/index.html
// └── ...
// ============================================
// PART 9: THE STATIC DEPLOYMENT
// ============================================

// With outputMode: "static", the dist folder contains no server file.
// Deploy the contents of dist/blog-app/browser to any static host.
//
// Netlify: drag the folder to app.netlify.com
// Vercel: vercel deploy dist/blog-app/browser
// GitHub Pages: push the folder to the gh-pages branch
// S3: aws s3 sync dist/blog-app/browser s3://bucket-name
// ============================================
// PART 10: THE VERIFICATION
// ============================================

// Check that the HTML is complete before hydration:
// curl https://example.com/blog/first-post
//
// The response contains:
// - The full article content
// - The title tag with the post title
// - The meta description
// - No blank body
//
// The page is fully rendered in HTML.
// Hydration makes it interactive in the browser.

The ten parts cover the routes, the server routes with getPrerenderParams, the build configuration, the blog service, the product service, the blog post component, the product component, the prerendered output, the static deployment, and the verification.


Quick Reference

The Prerender Configuration Options

OptionTypePurpose
discoverRoutesbooleanAuto-discover unparameterized routes
routesFilestringPath to file listing routes to prerender
outputMode"static" | "server"Generate only static files or include server

The Render Modes

ModeWhen HTML is GeneratedUse Case
RenderMode.PrerenderBuild timeStatic content, blogs, marketing
RenderMode.ServerPer requestUser-specific, frequently changing
RenderMode.ClientBrowserAuth-gated dashboards

The Fallback Strategies

FallbackBehavior
PrerenderFallback.ServerSSR for non-prerendered paths (default)
PrerenderFallback.ClientCSR for non-prerendered paths
PrerenderFallback.None404 for non-prerendered paths

The Build Output

ModeGenerated Files
Default (hybrid)Prerendered HTML + server file
outputMode: "static"Prerendered HTML only

Best Practices

✅ Do This:

// Disable automatic route discovery for explicit control
"prerender": { "discoverRoutes": false }                       // ✅
// Use getPrerenderParams for parameterized routes
async getPrerenderParams() { ... }                             // ✅
// Set fallback to Server for routes that might not be prerendered
fallback: PrerenderFallback.Server                             // ✅
// Use outputMode: "static" for fully static sites
"outputMode": "static"                                         // ✅
// Use Title and Meta services for SEO
title.setTitle('About Us - My Website');                       // ✅

❌ Don’t Do This:

// Don't rely on discoverRoutes for complex applications
"discoverRoutes": true                                         // ❌ unpredictable
// Don't forget getPrerenderParams for parameterized routes
{ path: 'products/:id', renderMode: RenderMode.Prerender }     // ❌ build error
// Don't prerender user-specific content
{ path: 'dashboard', renderMode: RenderMode.Prerender }        // ❌ same for all
// Don't expect prerendered content to update without rebuild
// Prices, stock, and real-time data require SSR or CSR.          // ❌

Common Pitfalls

PitfallWhy It HappensFix
Build error on parameterized routeMissing getPrerenderParamsDefine the function in server routes
inject fails in getPrerenderParamsUsed after awaitCall inject synchronously
No index.html after buildoutputMode: "server" with no serverUse outputMode: "static"
Route not prerendereddiscoverRoutes: false and not in listAdd to routes.txt or server routes
Content outdatedPrerendered at build timeRebuild and redeploy after changes

Real-World Examples

1. Static Route Prerendering

{ path: '', renderMode: RenderMode.Prerender }

2. Parameterized Route

{ path: 'blog/:slug', renderMode: RenderMode.Prerender, async getPrerenderParams() { ... } }

3. Fallback to SSR

fallback: PrerenderFallback.Server

4. Fallback to CSR

fallback: PrerenderFallback.Client

5. Fully Static Output

{ "outputMode": "static" }

6. Routes File

/about
/blog
/contact

7. Disable Auto-Discovery

{ "prerender": { "discoverRoutes": false } }

8. Title Service for SEO

title.setTitle('About Us - My Website');

9. Meta Service for SEO

meta.addTags([{ name: 'description', content: '...' }]);

10. Static Deployment

aws s3 sync dist/blog-app/browser s3://bucket-name

Visual

Prerendering vs SSR

┌──────────────────────────────────────────────┐
│  PRERENDERING (SSG)                          │
│                                              │
│  Build time:                                 │
│    ng build → render all pages → HTML files  │
│                                              │
│  Request time:                               │
│    User → CDN → static HTML → hydrate        │
│                                              │
│  No server rendering per request.            │
│  Content is identical for all users.         │
│                                              │
├──────────────────────────────────────────────┤
│  SERVER-SIDE RENDERING (SSR)                 │
│                                              │
│  Build time:                                 │
│    ng build → server bundle                  │
│                                              │
│  Request time:                               │
│    User → Server → render → HTML → hydrate   │
│                                              │
│  Server renders on every request.            │
│  Content can be user-specific.               │
│                                              │
└──────────────────────────────────────────────┘

The Prerender Decision Matrix

┌──────────────────────────────────────────────┐
│  WHEN TO PRERENDER                           │
│                                              │
│  ✅ Content is the same for all users        │
│  ✅ Content does not change frequently       │
│  ✅ SEO is important                         │
│  ✅ Static hosting is preferred              │
│                                              │
│  Examples:                                   │
│    Marketing pages, blogs, docs,             │
│    product catalogs with stable data         │
│                                              │
│  ❌ User-specific content                    │
│  ❌ Real-time data (prices, stock)           │
│  ❌ Frequently changing content              │
│  ❌ Auth-gated dashboards                    │
│                                              │
└──────────────────────────────────────────────┘

The getPrerenderParams Flow

┌──────────────────────────────────────────────┐
│  getPrerenderParams                          │
│                                              │
│  Build starts                                │
│       │                                      │
│       ▼                                      │
│  Angular reads server routes                 │
│       │                                      │
│       ▼                                      │
│  For each Prerender route with params:       │
│    └─ Call getPrerenderParams()              │
│    └─ Receive array of { id: value }         │
│    └─ Render one HTML file per entry         │
│                                              │
│  Result:                                     │
│    /products/1/index.html                    │
│    /products/2/index.html                    │
│    /products/3/index.html                    │
│                                              │
└──────────────────────────────────────────────┘

Static vs Hybrid Output

┌──────────────────────────────────────────────┐
│  HYBRID (default)                            │
│                                              │
│  dist/browser/    → static HTML files        │
│  dist/server/     → server bundle            │
│                                              │
│  Requires Node.js server for non-prerendered │
│  routes. Can fall back to SSR.               │
│                                              │
├──────────────────────────────────────────────┤
│  STATIC (outputMode: "static")               │
│                                              │
│  dist/browser/    → static HTML files        │
│  No server bundle.                           │
│                                              │
│  Deploy to any static host. No Node.js       │
│  runtime required.                           │
│                                              │
└──────────────────────────────────────────────┘

Summary

ItemValue
Prerendering definitionStatic HTML generated at build time
Also calledStatic Site Generation (SSG)
Enabled byng add @angular/ssr
Config optionprerender in angular.json
Route discoverydiscoverRoutes: true/false
Route listroutesFile: "routes.txt"
Parameterized routesgetPrerenderParams() in server routes
Fallback optionsPrerenderFallback.Server, .Client, .None
Fully static outputoutputMode: "static"
Best forBlogs, docs, marketing, stable catalogs

Key takeaways:

  • Prerendering generates static HTML at build time, not on each request. It preserves the SEO and performance benefits of SSR while reducing server load and time to first byte. The server serves files, not rendered pages .
  • Prerendering is best for content that is the same for every user. Marketing pages, blog posts, documentation, and product catalogs with stable data are ideal. User-specific content, real-time data, and frequently changing pages should use SSR or CSR .
  • Configure prerendering explicitly for production applications. Disable discoverRoutes and specify which routes to prerender, either through routes.txt or through the server routing configuration. Automatic discovery is convenient but unpredictable .
  • Parameterized routes need getPrerenderParams. The function returns an array of parameter objects, one for each path to generate. It runs at build time and can inject services to fetch the data it needs .
  • inject must be called synchronously inside getPrerenderParams. It cannot be used after an await statement. The function can be async, but dependency injection happens before the first await .
  • Fallback strategies handle routes that were not prerendered. PrerenderFallback.Server uses SSR for those paths, PrerenderFallback.Client uses CSR, and PrerenderFallback.None returns a 404 .
  • outputMode: "static" produces a fully static site. No server file is generated. The output can be deployed to any static hosting provider without a Node.js runtime. This is the simplest deployment model for entirely static applications .
  • Prerendered content requires a rebuild to update. The HTML is frozen at build time. When the content changes, you must rebuild and redeploy. For content that changes frequently, prerendering is not the right strategy .

Remember: Prerendering is the strategy for content that does not change per request. It renders the HTML once, at build time, and serves the same file to every user. The server does no work. The CDN serves the file from the edge. The browser paints it immediately. Hydration makes it interactive. Use prerendering for blogs, documentation, marketing pages, and any content that is the same for everyone. Use getPrerenderParams to enumerate the specific paths for parameterized routes. Use outputMode: "static" when you want no server at all. And remember that prerendered content is frozen — if it changes, you rebuild.


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!