"Description": "Learn how to configure Server-Side Rendering (SSR) for your Angular application in the ABP Framework to improve performance and SEO."
"Description": "Learn how to configure Server-Side Rendering (SSR) for your Angular application in the ABP Framework to improve performance and SEO."
}
```
# SSR Configuration
[Server-Side Rendering (SSR)](https://angular.io/guide/ssr) is a process that involves rendering pages on the server, resulting in initial HTML content that contains the page state. This allows the browser to show the page to the user immediately, before the JavaScript bundles are downloaded and executed.
[Server-Side Rendering (SSR)](https://angular.dev/guide/ssr) is a process that involves rendering pages on the server, resulting in initial HTML content that contains the page state. This allows the browser to show the page to the user immediately, before the JavaScript bundles are downloaded and executed.
SSR improves the **performance** (First Contentful Paint) and **SEO** (Search Engine Optimization) of your application.
@ -105,10 +105,10 @@ If your project uses the **Webpack Builder** (`@angular-devkit/build-angular:bro
```typescript
import {
AngularNodeAppEngine,
createNodeRequestHandler,
isMainModule,
writeResponseToNodeResponse,
AngularNodeAppEngine,
createNodeRequestHandler,
isMainModule,
writeResponseToNodeResponse,
} from '@angular/ssr/node';
import express from 'express';
import { dirname, resolve } from 'node:path';
@ -128,28 +128,28 @@ const angularApp = new AngularNodeAppEngine();
* Serve static files from /browser
*/
app.use(
express.static(browserDistFolder, {
maxAge: '1y',
index: false,
redirect: false,
}),
express.static(browserDistFolder, {
maxAge: '1y',
index: false,
redirect: false,
}),
);
/**
* Handle all other requests by rendering the Angular application.
Hydration is the process where Angular attaches to server-rendered HTML and makes it interactive. The ABP schematic automatically configures hydration for your application.
### 6.1. Common Hydration Issues
**Problem: Browser APIs on Server**
```typescript
// ❌ Bad - will fail on server
const width = window.innerWidth;
// ✅ Good - check platform
import { isPlatformBrowser } from '@angular/common';
import { PLATFORM_ID, inject } from '@angular/core';
export class MyComponent {
platformId = inject(PLATFORM_ID);
getWidth() {
if (isPlatformBrowser(this.platformId)) {
return window.innerWidth;
}
return 0;
}
}
```
**Problem: Random or Time-Based Values**
```typescript
// ❌ Bad - generates different values on server and client
id = Math.random();
currentTime = new Date();
// ✅ Good - use TransferState for consistent data
import { TransferState, makeStateKey } from '@angular/core';
this.transferState.set(TIME_KEY, new Date().toISOString());
} else {
this.time = this.transferState.get(TIME_KEY, new Date().toISOString());
}
}
```
**Enable Debug Tracing:**
```typescript
// app.config.ts
import { provideClientHydration, withDebugTracing } from '@angular/platform-browser';
export const appConfig: ApplicationConfig = {
providers: [
provideClientHydration(withDebugTracing()),
]
};
```
## 7. Environment Variables
Configure your SSR application using environment variables in `server.ts`:
```typescript
// server.ts
const PORT = process.env['PORT'] || 4000;
const HOST = process.env['HOST'] || 'localhost';
// Start the server
if (isMainModule(import.meta.url)) {
app.listen(PORT, () => {
console.log(`Server running on http://${HOST}:${PORT}`);
});
}
```
For production, set environment variables:
```bash
# .env file or environment configuration
NODE_ENV=production
PORT=4000
HOST=0.0.0.0
API_URL=https://api.yourdomain.com
```
## 8. Deployment
To deploy your Angular SSR application to a production server:
### 8.1. Build the Application
```shell
yarn build
@ -251,30 +415,152 @@ yarn build
yarn run build:ssr
```
### 5.2. Prepare Artifacts
### 8.2. Prepare Artifacts
After the build is complete, you will find the output in the `dist` folder.
For the **Application Builder**, the output structure typically looks like this:
Copy the `dist/MyProjectName` folder to your server:
```
dist/MyProjectName/
├── browser/ # Client-side bundles
└── server/ # Server-side bundles and entry point (server.mjs)
└── server/ # Server-side bundles (server.mjs)
```
You need to copy the entire `dist/MyProjectName` folder to your server.
### 8.3. Install Production Dependencies
On your server, install only the required dependencies (schematic already added them to package.json):
```shell
npm install --production
```
### 5.3. Run the Server
Required dependencies:
- `express`: Web server framework
- `openid-client`: Authentication support
On your server, navigate to the folder where you copied the artifacts and run the server using Node.js:
### 8.4. Run the Server
**Development/Testing:**
```shell
node server/server.mjs
```
> [!TIP]
> It is recommended to use a process manager like [PM2](https://pm2.keymetrics.io/) to keep your application alive and handle restarts.
**Production (with PM2):**
Use [PM2](https://pm2.keymetrics.io/) to keep your application alive and manage restarts:
```shell
npm install -g pm2
pm2 start server/server.mjs --name "my-app"
pm2 startup # Configure PM2 to start on boot
pm2 save # Save current process list
```
## 9. Troubleshooting
### 9.1. "Window/Document is not defined"
Browser APIs don't exist on the server. Always check the platform:
```typescript
import { isPlatformBrowser } from '@angular/common';
if (isPlatformBrowser(this.platformId)) {
// Safe to use window, document, localStorage, etc.
}
```
### 9.2. "LocalStorage is not defined"
ABP Core provides `AbpLocalStorageService` that implements the `Storage` interface and works safely on both server and client:
```typescript
import { AbpLocalStorageService } from '@abp/ng.core';
@Injectable({ providedIn: 'root' })
export class MyService {
private storage = inject(AbpLocalStorageService);
saveData(key: string, value: string): void {
// Safe on both server and client
this.storage.setItem(key, value);
}
getData(key: string): string | null {
// Returns null on server, actual value on client
return this.storage.getItem(key);
}
}
```
`AbpLocalStorageService` implements all `Storage` methods:
- `getItem(key: string): string | null`
- `setItem(key: string, value: string): void`
- `removeItem(key: string): void`
- `clear(): void`
- `key(index: number): string | null`
- `length: number`
### 9.3. Hydration Mismatch Errors
If you see "NG0500" errors in the console:
1. Enable debug tracing (see section 6.1)
2. Check for dynamic content (dates, random IDs)
3. Ensure server and client render the same HTML
4. Use `TransferState` for data consistency
### 9.4. Avoiding Duplicate API Calls
ABP Core provides a `transferStateInterceptor` that automatically prevents duplicate HTTP GET requests during hydration. When you use `provideAbpCore()`, this interceptor is already active.
**How it works:**
- Server: Stores HTTP GET responses in `TransferState`
- Client: Reuses stored responses during hydration
- Automatically cleans up stored data after use
```typescript
// app.config.ts
import { provideAbpCore } from '@abp/ng.core';
export const appConfig: ApplicationConfig = {
providers: [
provideAbpCore(),
// transferStateInterceptor is automatically included
]
};
```
The interceptor works with all HTTP GET requests made through `HttpClient`:
```typescript
// This service automatically benefits from the interceptor
@Injectable({ providedIn: 'root' })
export class UserService {
private http = inject(HttpClient);
getUsers() {
// On server: Response is cached in TransferState
// On client: Cached response is used (no duplicate request)
return this.http.get<User[]>('/api/users');
}
}
```
> [!NOTE]
> The interceptor only works with GET requests. POST, PUT, DELETE, and PATCH requests are not cached.
## Additional Resources
- [Angular SSR Official Guide](https://angular.dev/guide/ssr)