# Server-Side Rendering

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

> Rendering pages on a Node.js server: the SSR architecture, pre-rendered HTML, hydration and event replay, data fetching and transfer, and a Blog Portal capstone project.

## Introduction to Server-Side Rendering

In the previous lesson, the browser received an empty page and created all the HTML itself. The user saw nothing until the JavaScript had downloaded and run.

Server-Side Rendering solves this problem: the user gets a page that already contains the content.

## What is Server-Side Rendering (SSR)?

In **Server-Side Rendering (SSR)**, a Node.js server runs your Angular app for each request. It creates the full HTML of the page, with the data, and sends it to the browser. The browser shows this HTML at once.

Then the browser downloads the JavaScript, and Angular makes the page interactive.

### Real-World Example

A news website publishes new articles every hour. A reader opens an article from a search engine. The server renders the article with its text and sends the finished HTML, so the text appears immediately, and search engines can read it.

A few moments later, Angular starts in the browser, and the comment button and menus start to work.

<figure className="ngc-fig">
<svg viewBox="0 0 640 140" role="img" aria-label="Server-side rendering: the request goes to the server, which renders the page with data, sends full HTML that is visible at once, and Angular hydrates it.">
<defs><marker id="ngc-c2" 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"}}>Request</text><text className="ngc-sub" x="77.0" y="83" textAnchor="middle">GET /posts/hydration</text></g></g><line className="ngc-link ngc-flow c-slate" x1="150" y1="68" x2="162" y2="68" markerEnd="url(#ngc-c2)" /><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"}}>Server</text><text className="ngc-sub" x="235.0" y="83" textAnchor="middle">renders + gets data</text></g></g><line className="ngc-link ngc-flow c-slate" x1="308" y1="68" x2="320" y2="68" markerEnd="url(#ngc-c2)" /><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"}}>Full HTML</text><text className="ngc-sub" x="393.0" y="83" textAnchor="middle">visible at once</text></g></g><line className="ngc-link ngc-flow c-slate" x1="466" y1="68" x2="478" y2="68" markerEnd="url(#ngc-c2)" /><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"}}>Hydration</text><text className="ngc-sub" x="551.0" y="83" textAnchor="middle">buttons start to work</text></g></g><text className="ngc-note" x="320" y="124" textAnchor="middle">The user sees the content first. Then Angular makes it interactive.</text>
</svg>
<figcaption>Server-Side Rendering: the server creates the HTML for each request.</figcaption>
</figure>

## Benefits of SSR

- **Fast first view.** The user sees the content before the JavaScript has downloaded.
- **Better for search engines.** Search engines receive the full text in the HTML.
- **Better link previews.** Chat apps and social networks show the page title and text when someone shares a link.
- **Works on slow devices.** The server does the first rendering, not the user's phone.
- **Always current.** The page is rendered for each request, so it always shows the newest data.

## SSR Architecture

### Angular SSR Overview

Angular SSR is in the package `@angular/ssr`. Add it when you create a project:

```bash
ng new my-blog --ssr
```

Or add it to an existing project:

```bash
ng add @angular/ssr
```

The CLI adds these files:

| File | Job |
|---|---|
| `src/server.ts` | The Node.js server (Express). It receives requests and calls Angular |
| `src/main.server.ts` | Starts the app on the server |
| `src/app/app.config.server.ts` | Providers used only on the server |
| `src/app/app.routes.server.ts` | The render mode of each route |

In `app.routes.server.ts`, `RenderMode.Server` means "render this route on the server for each request":

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

export const serverRoutes: ServerRoute[] = [
  { path: '', renderMode: RenderMode.Server },
  { path: 'posts/:slug', renderMode: RenderMode.Server },
  { path: '**', renderMode: RenderMode.Server },
];
```

`ng serve` also renders the pages on the server in an SSR project. So you see SSR problems, such as code that uses `window`, already during development.

### Server Rendering Process

For each request, the server does these steps:

1. It finds the route that matches the URL.
2. It creates the components of that route, the same way as in the browser.
3. It waits until the components have their data, for example from `HttpClient`.
4. It turns the rendered components into an HTML text.
5. It sends this HTML to the browser.

### Request-Response Lifecycle

1. The user opens `https://myblog.com/posts/hydration`.
2. The **request** reaches `server.ts`.
3. Angular renders the post page on the server.
4. The **response**, the full HTML, goes back to the browser.
5. The browser shows the HTML and downloads the JavaScript.
6. Angular starts in the browser and connects to the existing HTML.

The practice blocks on this page run in your browser, so they use CSR. They show the code that you write in an SSR app. The downloadable project at the end runs real SSR with a Node.js server.

## SSR Rendering Fundamentals

### Initial Page Render

On the server, the **initial page render** creates the HTML with the data. Compare the first HTML that the browser receives:

```html
<!-- CSR: the browser receives -->
<app-root></app-root>

<!-- SSR: the browser receives -->
<app-root>
  <h1>Latest posts</h1>
  <article><h2>What is SSR?</h2><p>Asha Rao</p></article>
</app-root>
```

### Pre-rendered HTML

The HTML from the server is **pre-rendered**: it was created before it reached the browser. The browser shows it at once, but its buttons do not work yet, because Angular has not started in the browser.

### Interactive UI Initialization

Your components run **two times**: once on the server and once in the browser. The server has no browser, so these do not exist there:

- `window`, `document` and `localStorage`
- The size of the screen
- Clicks and key presses

To check where the code runs, inject `PLATFORM_ID` and use `isPlatformBrowser()`:

```angular runnable title="Where is the code running?"
import { Component, PLATFORM_ID, inject } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';

@Component({
  selector: 'app-root',
  template: `
    <p>This component is running in the {{ isBrowser ? 'browser' : 'server' }}.</p>
    <p>Saved theme: {{ theme }}</p>
  `,
})
export class App {
  isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
  theme = 'light';

  constructor() {
    // localStorage exists only in the browser
    if (this.isBrowser) {
      this.theme = this.readTheme();
    }
  }

  readTheme() {
    try {
      return localStorage.getItem('theme') ?? 'light';
    } catch {
      return 'light';
    }
  }
}
```

On the server, this component shows "server" and does not read `localStorage`.

A simpler way for browser-only code is **`afterNextRender()`**. Angular runs its function **only in the browser**, after the component is rendered. On the server it never runs.

```angular runnable title="Browser-only code with afterNextRender()"
import { Component, afterNextRender, signal } from '@angular/core';

@Component({
  selector: 'app-root',
  template: `
    <p>Screen width: {{ width() }}</p>
    <p>Page title: {{ title() }}</p>
  `,
})
export class App {
  width = signal('unknown');
  title = signal('unknown');

  constructor() {
    // Runs only in the browser, after the first render
    afterNextRender(() => {
      this.width.set(window.innerWidth > 600 ? 'wide' : 'narrow');
      this.title.set(document.title || 'no title');
    });
  }
}
```

Like `inject()`, call `afterNextRender()` in the constructor.

## Hydration Process

### What is Hydration?

When the JavaScript has downloaded, Angular starts in the browser. Without hydration, it would delete the HTML from the server and create it again. The screen would flash.

<figure className="ngc-fig">
<svg viewBox="0 0 640 234" role="img" aria-label="Without hydration the browser deletes and rebuilds the HTML; with hydration Angular reuses the server HTML.">
<defs><marker id="ngc-s2" 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><text className="ngc-title c-red" x="160" y="22" textAnchor="middle">Without hydration</text><text className="ngc-title c-green" x="480" y="22" textAnchor="middle">With hydration</text><g className="ngc-step ngc-t1"><g className="ngc-node ngc-soft c-red"><rect x="30" y="36" width="260" height="46" rx="9" /><text x="160.0" y="64" textAnchor="middle" style={{fontSize:"13px"}}>Server HTML shown</text></g></g><line className="ngc-link ngc-flow c-slate" x1="160" y1="83" x2="160" y2="99" markerEnd="url(#ngc-s2)" /><g className="ngc-step ngc-t2"><g className="ngc-node ngc-soft c-red"><rect x="30" y="100" width="260" height="46" rx="9" /><text x="160.0" y="120" textAnchor="middle" style={{fontSize:"13px"}}>Angular deletes it</text><text className="ngc-sub" x="160.0" y="138" textAnchor="middle">and renders again</text></g></g><line className="ngc-link ngc-flow c-slate" x1="160" y1="147" x2="160" y2="163" markerEnd="url(#ngc-s2)" /><g className="ngc-step ngc-t3"><g className="ngc-node ngc-soft c-red"><rect x="30" y="164" width="260" height="46" rx="9" /><text x="160.0" y="184" textAnchor="middle" style={{fontSize:"13px"}}>Screen flashes</text><text className="ngc-sub" x="160.0" y="202" textAnchor="middle">work done twice</text></g></g><g className="ngc-step ngc-t1"><g className="ngc-node ngc-soft c-green"><rect x="350" y="36" width="260" height="46" rx="9" /><text x="480.0" y="64" textAnchor="middle" style={{fontSize:"13px"}}>Server HTML shown</text></g></g><line className="ngc-link ngc-flow c-slate" x1="480" y1="83" x2="480" y2="99" markerEnd="url(#ngc-s2)" /><g className="ngc-step ngc-t2"><g className="ngc-node ngc-soft c-green"><rect x="350" y="100" width="260" height="46" rx="9" /><text x="480.0" y="120" textAnchor="middle" style={{fontSize:"13px"}}>Angular reuses the elements</text><text className="ngc-sub" x="480.0" y="138" textAnchor="middle">connects components</text></g></g><line className="ngc-link ngc-flow c-slate" x1="480" y1="147" x2="480" y2="163" markerEnd="url(#ngc-s2)" /><g className="ngc-step ngc-t3"><g className="ngc-node ngc-soft c-green"><rect x="350" y="164" width="260" height="46" rx="9" /><text x="480.0" y="184" textAnchor="middle" style={{fontSize:"13px"}}>Buttons start to work</text><text className="ngc-sub" x="480.0" y="202" textAnchor="middle">no flash</text></g></g>
</svg>
<figcaption>Hydration keeps the server HTML and makes it interactive.</figcaption>
</figure>

**Hydration** means that Angular **reuses** the HTML from the server. It walks through the existing elements and connects them to the components, instead of creating new elements.

Turn it on in `app.config.ts`. The CLI adds it for you with `--ssr`:

```ts
import { provideClientHydration, withEventReplay } from '@angular/platform-browser';

export const appConfig: ApplicationConfig = {
  providers: [provideClientHydration(withEventReplay())],
};
```

### Event Binding Restoration

During hydration, Angular connects every `(click)` and other event binding to its element. After that, the buttons work.

A user can click before hydration is finished. **`withEventReplay()`** records these early clicks and runs them again (replays them) when hydration finishes, so no click is lost.

### Hydrated Angular Applications

After hydration, the app is a normal Angular app running in the browser. Navigation with `routerLink` happens in the browser, like CSR. Only the **first** page of a visit comes from the server.

The server HTML and the first browser rendering must be the same. If a template shows different content in the browser, for example a random number or the current time in seconds, hydration finds a difference. Angular then reports a hydration error in the browser console.

## Data Fetching in SSR

### Server-Side Data Fetching

You use `HttpClient` in the same way as in CSR. On the server, the request runs on the server, and Angular **waits** for the answer before it sends the HTML.

```angular runnable title="Loading posts with loading and error states"
import { ApplicationConfig, Component, inject, signal } from '@angular/core';
import {
  HttpClient, HttpErrorResponse, HttpHandlerFn, HttpRequest, HttpResponse,
  provideHttpClient, withInterceptors,
} from '@angular/common/http';
import { delay, mergeMap, of, throwError, timer } from 'rxjs';

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

@Component({
  selector: 'app-root',
  template: `
    <h4>Latest posts</h4>
    @if (loading()) {
      <p>Loading posts...</p>
    } @else if (error()) {
      <p>⚠ {{ error() }}</p>
    } @else {
      @for (post of posts(); track post.slug) {
        <p><b>{{ post.title }}</b> by {{ post.author }}</p>
      }
    }
  `,
})
export class App {
  posts = signal<any[]>([]);
  loading = signal(true);
  error = signal('');

  constructor() {
    inject(HttpClient).get<any[]>('/api/posts').subscribe({
      next: list => {
        this.posts.set(list);
        this.loading.set(false);
      },
      error: () => {
        this.error.set('Could not load the posts.');
        this.loading.set(false);
      },
    });
  }
}
// ─── Practice server ──────────────────────────────────────────────
// Answers the blog API inside the browser, like in the API Integration lesson.
const POSTS = [
  { slug: 'what-is-ssr', title: 'What is SSR?', author: 'Asha Rao', body: 'The server builds the HTML of the page.' },
  { slug: 'hydration', title: 'Hydration Explained', author: 'Ravi Kumar', body: 'Angular reuses the HTML from the server.' },
];

function practiceServer(req: HttpRequest<unknown>, next: HttpHandlerFn) {
  if (req.url === '/api/posts') {
    return of(new HttpResponse({ status: 200, body: POSTS })).pipe(delay(300));
  }
  const post = POSTS.find(p => req.url === `/api/posts/${p.slug}`);
  if (post) {
    return of(new HttpResponse({ status: 200, body: post })).pipe(delay(300));
  }
  return timer(300).pipe(mergeMap(() => throwError(() => new HttpErrorResponse({ status: 404, statusText: 'Not Found' }))));
}

```

With SSR, the user never sees "Loading posts..." on the first page. The server waited for the posts, so the HTML already contains them.

### Rendering Dynamic Content

The same request code works for a details page. The route parameter `:slug` comes into an `@Input()`. Here it is written as a **setter**, `@Input() set slug(value)`, a method that runs each time the router gives a new value.

```angular runnable title="A details page with a route parameter"
import { ApplicationConfig, Component, Input, inject, signal } from '@angular/core';
import {
  HttpClient, HttpErrorResponse, HttpHandlerFn, HttpRequest, HttpResponse,
  provideHttpClient, withInterceptors,
} from '@angular/common/http';
import { RouterLink, RouterOutlet, Routes, provideRouter, withComponentInputBinding } from '@angular/router';
import { delay, mergeMap, of, throwError, timer } from 'rxjs';

@Component({
  selector: 'app-post-detail',
  template: `
    @if (error()) {
      <p>⚠ {{ error() }}</p>
    } @else {
      <p><b>{{ title() }}</b></p>
    }
  `,
})
export class PostDetail {
  private http = inject(HttpClient);
  title = signal('Loading...');
  error = signal('');

  @Input() set slug(value: string) {
    this.error.set('');
    this.title.set('Loading...');
    this.http.get<any>(`/api/posts/${value}`).subscribe({
      next: post => this.title.set(post.title),
      error: () => this.error.set('This post does not exist.'),
    });
  }
}

const routes: Routes = [
  { path: '', redirectTo: 'posts/hydration', pathMatch: 'full' },
  { path: 'posts/:slug', component: PostDetail },
];

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

@Component({
  selector: 'app-root',
  imports: [RouterOutlet, RouterLink],
  template: `
    <a routerLink="/posts/what-is-ssr">Post 1</a> |
    <a routerLink="/posts/hydration">Post 2</a> |
    <a routerLink="/posts/old-post">Old post</a>
    <router-outlet />
  `,
})
export class App {}
// ─── Practice server ──────────────────────────────────────────────
// Answers the blog API inside the browser, like in the API Integration lesson.
const POSTS = [
  { slug: 'what-is-ssr', title: 'What is SSR?', author: 'Asha Rao', body: 'The server builds the HTML of the page.' },
  { slug: 'hydration', title: 'Hydration Explained', author: 'Ravi Kumar', body: 'Angular reuses the HTML from the server.' },
];

function practiceServer(req: HttpRequest<unknown>, next: HttpHandlerFn) {
  if (req.url === '/api/posts') {
    return of(new HttpResponse({ status: 200, body: POSTS })).pipe(delay(300));
  }
  const post = POSTS.find(p => req.url === `/api/posts/${p.slug}`);
  if (post) {
    return of(new HttpResponse({ status: 200, body: post })).pipe(delay(300));
  }
  return timer(300).pipe(mergeMap(() => throwError(() => new HttpErrorResponse({ status: 404, statusText: 'Not Found' }))));
}

```

Each request is rendered again, so the page always shows the current data. A post published one minute ago appears on the next request, without a new build. This is the main difference from SSG, which you learn in the next lesson.

### Data Transfer to Client

Without help, the browser would send the same `HttpClient` request again after hydration. `provideClientHydration()` prevents this:

<figure className="ngc-fig">
<svg viewBox="0 0 640 134" role="img" aria-label="The server stores the HttpClient answer in an ng-state script; the browser HttpClient reads it instead of sending the request again.">
<defs><marker id="ngc-s3" 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="6" y="30" width="145" height="64" rx="9" /><text x="78.5" y="59" textAnchor="middle" style={{fontSize:"14px"}}>Server</text><text className="ngc-sub" x="78.5" y="77" textAnchor="middle">HttpClient gets posts</text></g></g><line className="ngc-link ngc-flow c-slate" x1="152" y1="62" x2="166" y2="62" markerEnd="url(#ngc-s3)" /><g className="ngc-step ngc-t2"><g className="ngc-node ngc-soft c-orange"><rect x="167" y="30" width="145" height="64" rx="9" /><text x="239.5" y="59" textAnchor="middle" style={{fontSize:"14px"}}>ng-state</text><text className="ngc-sub" x="239.5" y="77" textAnchor="middle">answer stored in the HTML</text></g></g><line className="ngc-link ngc-flow c-slate" x1="313" y1="62" x2="327" y2="62" markerEnd="url(#ngc-s3)" /><g className="ngc-step ngc-t3"><g className="ngc-node ngc-soft c-blue"><rect x="328" y="30" width="145" height="64" rx="9" /><text x="400.5" y="59" textAnchor="middle" style={{fontSize:"14px"}}>Browser</text><text className="ngc-sub" x="400.5" y="77" textAnchor="middle">HttpClient checks ng-state</text></g></g><line className="ngc-link ngc-flow c-slate" x1="474" y1="62" x2="488" y2="62" markerEnd="url(#ngc-s3)" /><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"}}>No 2nd request</text><text className="ngc-sub" x="561.5" y="77" textAnchor="middle">answer reused</text></g></g><circle className="ngc-packet c-orange" 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 same GET request is sent only once: on the server.</text>
</svg>
<figcaption>HttpClient answers move from the server to the browser inside the page.</figcaption>
</figure>

1. On the server, Angular stores every `HttpClient` answer in the HTML, inside a `<script id="ng-state">` element.
2. In the browser, `HttpClient` finds the stored answer and uses it. No second request is sent.

```html
<script id="ng-state" type="application/json">
  {"…": {"b": [{"slug": "what-is-ssr", "title": "What is SSR?"}]}}
</script>
```

This works automatically for GET requests. Requests with other methods are always sent again.

## Advantages and Limitations of SSR

**Advantages:**

- Fast first view, also on slow devices.
- Full HTML for search engines and link previews.
- Always current data, rendered for each request.

**Limitations:**

- **Needs a Node.js server.** You cannot use a simple file hosting service.
- **Server cost.** The server works for every request.
- **More rules for your code.** Browser-only code needs `afterNextRender()` or `isPlatformBrowser()`.
- **Slower response than a ready file.** The server needs time to render each page.

| Point | CSR | SSR |
|---|---|---|
| First HTML | Empty | Full content |
| First view | Slow | Fast |
| Search engines | Weak | Strong |
| Hosting | Any file host | A Node.js server |
| Browser-only code | Works everywhere | Needs `afterNextRender()` |

## Capstone Project: Blog Portal

You now build a **Blog Portal**: a list of posts and a page for each post, rendered on the server.

### Build Blog Listing Pages

`PostList` requests `/api/posts` and shows each post with a link to its page.

### Build Blog Details Pages

`PostDetail` shows one post. The route `posts/:slug` gives the `slug` to its `@Input()`. The input uses a **setter**: a method written as `@Input() set slug(value)` that runs each time the router gives a new value. It requests the post with this slug.

### Implement Server Rendering

In the downloadable project, every route uses `RenderMode.Server` in `app.routes.server.ts`, and `server.ts` runs the app.

### Fetch Content on the Server

`BlogApiService` uses `HttpClient`. On the server, Angular waits for the answers, and the HTML contains the posts. The answers are transferred to the browser.

### Configure Dynamic Routes

`posts/:slug` is a **dynamic route**: one route for every post. With SSR, a new post works at once, because the server renders the page for each request.

### Handle Loading and Error States

Each page has a `loading` and an `error` signal. An unknown slug returns 404 from the API, and the page shows "This post does not exist."

```angular runnable title="Capstone: Blog Portal"
import { ApplicationConfig, Component, Injectable, Input, inject, signal } from '@angular/core';
import {
  HttpClient, HttpErrorResponse, HttpHandlerFn, HttpRequest, HttpResponse,
  provideHttpClient, withInterceptors,
} from '@angular/common/http';
import { RouterLink, RouterOutlet, Routes, provideRouter, withComponentInputBinding } from '@angular/router';
import { delay, mergeMap, of, throwError, timer } from 'rxjs';

export interface Post {
  slug: string;
  title: string;
  author: string;
  body: string;
}

// ── API service ──
@Injectable({ providedIn: 'root' })
export class BlogApiService {
  private http = inject(HttpClient);
  getPosts() {
    return this.http.get<Post[]>('/api/posts');
  }
  getPost(slug: string) {
    return this.http.get<Post>(`/api/posts/${slug}`);
  }
}

// ── Listing page ──
@Component({
  selector: 'app-post-list',
  imports: [RouterLink],
  template: `
    <h4>Latest posts</h4>
    @if (loading()) {
      <p>Loading posts...</p>
    } @else {
      @for (post of posts(); track post.slug) {
        <p><a [routerLink]="['/posts', post.slug]">{{ post.title }}</a> by {{ post.author }}</p>
      }
      <p><a routerLink="/posts/missing-post">A broken link</a></p>
    }
  `,
})
export class PostList {
  posts = signal<Post[]>([]);
  loading = signal(true);

  constructor() {
    inject(BlogApiService).getPosts().subscribe(list => {
      this.posts.set(list);
      this.loading.set(false);
    });
  }
}

// ── Details page ──
@Component({
  selector: 'app-post-detail',
  imports: [RouterLink],
  template: `
    <p><a routerLink="/">← All posts</a></p>
    @if (loading()) {
      <p>Loading post...</p>
    } @else if (error()) {
      <p>⚠ {{ error() }}</p>
    } @else if (post(); as post) {
      <h4>{{ post.title }}</h4>
      <p>{{ post.body }}</p>
    }
  `,
})
export class PostDetail {
  private api = inject(BlogApiService);
  post = signal<Post | null>(null);
  loading = signal(true);
  error = signal('');

  @Input() set slug(value: string) {
    this.loading.set(true);
    this.error.set('');
    this.api.getPost(value).subscribe({
      next: post => {
        this.post.set(post);
        this.loading.set(false);
      },
      error: (err: HttpErrorResponse) => {
        this.error.set(err.status === 404 ? 'This post does not exist.' : 'Could not load the post.');
        this.loading.set(false);
      },
    });
  }
}

// ── Routes and configuration ──
const routes: Routes = [
  { path: '', component: PostList },
  { path: 'posts/:slug', component: PostDetail },
];

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

@Component({
  selector: 'app-root',
  imports: [RouterOutlet],
  template: `<b>📝 Blog Portal</b><router-outlet />`,
})
export class App {}
// ─── Practice server ──────────────────────────────────────────────
// Answers the blog API inside the browser, like in the API Integration lesson.
const POSTS = [
  { slug: 'what-is-ssr', title: 'What is SSR?', author: 'Asha Rao', body: 'The server builds the HTML of the page.' },
  { slug: 'hydration', title: 'Hydration Explained', author: 'Ravi Kumar', body: 'Angular reuses the HTML from the server.' },
];

function practiceServer(req: HttpRequest<unknown>, next: HttpHandlerFn) {
  if (req.url === '/api/posts') {
    return of(new HttpResponse({ status: 200, body: POSTS })).pipe(delay(300));
  }
  const post = POSTS.find(p => req.url === `/api/posts/${p.slug}`);
  if (post) {
    return of(new HttpResponse({ status: 200, body: post })).pipe(delay(300));
  }
  return timer(300).pipe(mergeMap(() => throwError(() => new HttpErrorResponse({ status: 404, statusText: 'Not Found' }))));
}

```

Open a post, go back, and open the broken link to see the error state.

## Download Project Files

### Download SSR Project Source Code

<a href="/downloads/angular-blog-portal-ssr.zip" download>Download angular-blog-portal-ssr.zip</a>

The ZIP file contains the Blog Portal as a complete Angular SSR project, with the blog API in `server.ts`.

| File | What it contains |
|---|---|
| `src/server.ts` | The Express server: the blog API and the Angular rendering |
| `src/server-data/posts.ts` | The blog posts |
| `src/app/app.routes.server.ts` | `RenderMode.Server` for every route |
| `src/app/app.config.ts` | Router, `HttpClient` and hydration with event replay |
| `src/app/blog-api.service.ts` | The requests to the blog API |
| `src/app/pages/` | The listing page, the details page and the "not found" page |
| `src/app/absolute-url.interceptor.ts` | Adds the server address to `/api` URLs, which the server needs |

### Project Setup Instructions

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

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

   ```bash
   npm install
   ```

3. Start the development server. It runs SSR and the API together:

   ```bash
   npm start
   ```

4. Open `http://localhost:4200`.

To run the production version:

```bash
npm run build
npm run serve:ssr:blog-portal
```

Then open `http://localhost:4000`. Open the page source (right-click → **View Page Source**): the HTML already contains the posts and a `<script id="ng-state">` element with the transferred data.

## Common Mistakes

<div className="ngc-mistakes">

| Mistake | What you see | Fix |
|---|---|---|
| Using `window` or `localStorage` in a constructor | The server crashes: `window is not defined` | Use `afterNextRender()` or `isPlatformBrowser()` |
| Different content on the server and in the browser | Hydration error in the console, and the page flashes | Render the same content; change it later in `afterNextRender()` |
| No `provideClientHydration()` | The page flashes, and data is requested twice | Add `provideClientHydration()` to `app.config.ts` |
| Relative URLs such as `/api/posts` on the server | The request fails on the server, because there is no current page | Add the server address with an interceptor |
| Deploying an SSR app to a file host | The server part does not run | Use a host that runs Node.js |

</div>

## Check Yourself

## Exercises

## What to Remember

- In SSR, a Node.js server renders each request into full HTML, so the first view is fast and search engines see the content. `RenderMode.Server` in `app.routes.server.ts` turns it on for a route.
- Hydration reuses the server HTML in the browser; `withEventReplay()` keeps early clicks. Browser-only code goes into `afterNextRender()` or behind `isPlatformBrowser()`.
- `HttpClient` requests run on the server, and their answers are transferred to the browser, so they are not sent twice.

Next: **Static Site Generation**, how to create the HTML of every page once, at build time.
