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
| Option | Type | Purpose |
|---|---|---|
discoverRoutes | boolean | Auto-discover unparameterized routes |
routesFile | string | Path to file listing routes to prerender |
outputMode | "static" | "server" | Generate only static files or include server |
The Render Modes
| Mode | When HTML is Generated | Use Case |
|---|---|---|
RenderMode.Prerender | Build time | Static content, blogs, marketing |
RenderMode.Server | Per request | User-specific, frequently changing |
RenderMode.Client | Browser | Auth-gated dashboards |
The Fallback Strategies
| Fallback | Behavior |
|---|---|
PrerenderFallback.Server | SSR for non-prerendered paths (default) |
PrerenderFallback.Client | CSR for non-prerendered paths |
PrerenderFallback.None | 404 for non-prerendered paths |
The Build Output
| Mode | Generated 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
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Build error on parameterized route | Missing getPrerenderParams | Define the function in server routes |
inject fails in getPrerenderParams | Used after await | Call inject synchronously |
| No index.html after build | outputMode: "server" with no server | Use outputMode: "static" |
| Route not prerendered | discoverRoutes: false and not in list | Add to routes.txt or server routes |
| Content outdated | Prerendered at build time | Rebuild 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
| Item | Value |
|---|---|
| Prerendering definition | Static HTML generated at build time |
| Also called | Static Site Generation (SSG) |
| Enabled by | ng add @angular/ssr |
| Config option | prerender in angular.json |
| Route discovery | discoverRoutes: true/false |
| Route list | routesFile: "routes.txt" |
| Parameterized routes | getPrerenderParams() in server routes |
| Fallback options | PrerenderFallback.Server, .Client, .None |
| Fully static output | outputMode: "static" |
| Best for | Blogs, 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
discoverRoutesand specify which routes to prerender, either throughroutes.txtor 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 . injectmust be called synchronously insidegetPrerenderParams. It cannot be used after anawaitstatement. The function can be async, but dependency injection happens before the first await .- Fallback strategies handle routes that were not prerendered.
PrerenderFallback.Serveruses SSR for those paths,PrerenderFallback.Clientuses CSR, andPrerenderFallback.Nonereturns 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!