Skip to main content

Angular Universal Introduction

What is Angular Universal?

Angular Universal is the official solution for implementing server-side rendering (SSR) in Angular applications. Traditionally, Angular applications run in the browser, rendering the application on the client-side. With Angular Universal, your application can render on the server, sending fully rendered HTML to the client.

This approach combines the best of both worlds: the SEO benefits and faster initial loading of server-rendered content, with the rich interactivity of client-side Angular applications.

Why Use Angular Universal?

Angular Universal offers several key advantages:

  1. Improved SEO: Search engines can better index your content because they receive fully rendered HTML pages.
  2. Enhanced Performance: Users see content much faster because they don't have to wait for all JavaScript to download, parse and execute.
  3. Better User Experience: Visitors see a fully-rendered page quickly, rather than a loading spinner.
  4. Social Media Optimization: Content sharing on social platforms displays proper previews when links are shared.

How Angular Universal Works

At a high level, Angular Universal works as follows:

  1. When a user or bot requests your application, the request goes to a server
  2. The server runs your Angular application in a Node.js environment
  3. Angular renders the requested page to HTML on the server
  4. The server sends the HTML to the client
  5. The client displays the HTML immediately while downloading the Angular application
  6. Once loaded, Angular takes over the page in a process called "hydration"

Setting Up Angular Universal

Let's walk through the basic setup for adding Angular Universal to an existing Angular project:

Step 1: Add Universal to your Project

The Angular CLI makes it easy to add Universal to your project:

bash
ng add @nguniversal/express-engine

This command does several things:

  • Installs required dependencies
  • Creates server-side application module (app.server.module.ts)
  • Creates a server TypeScript file (server.ts)
  • Updates your Angular configuration

Step 2: Understanding the Generated Files

After running the command above, let's examine some of the key files that are generated:

app.server.module.ts:

typescript
import { NgModule } from '@angular/core';
import { ServerModule } from '@angular/platform-server';

import { AppModule } from './app.module';
import { AppComponent } from './app.component';

@NgModule({
imports: [
AppModule,
ServerModule,
],
bootstrap: [AppComponent],
})
export class AppServerModule {}

This is the server-side application module that wraps your main application.

server.ts:

typescript
import 'zone.js/dist/zone-node';

import { ngExpressEngine } from '@nguniversal/express-engine';
import * as express from 'express';
import { join } from 'path';

import { AppServerModule } from './src/main.server';
import { APP_BASE_HREF } from '@angular/common';

// The Express app is exported so that it can be used by serverless Functions
export function app(): express.Express {
const server = express();
const distFolder = join(process.cwd(), 'dist/your-app-name/browser');

// Our Universal engine
server.engine('html', ngExpressEngine({
bootstrap: AppServerModule,
}));

server.set('view engine', 'html');
server.set('views', distFolder);

// Serve static files from /browser
server.get('*.*', express.static(distFolder, {
maxAge: '1y'
}));

// All regular routes use the Universal engine
server.get('*', (req, res) => {
res.render('index', { req, providers: [{ provide: APP_BASE_HREF, useValue: req.baseUrl }] });
});

return server;
}

function run(): void {
const port = process.env.PORT || 4000;

// Start up the Node server
const server = app();
server.listen(port, () => {
console.log(`Node Express server listening on http://localhost:${port}`);
});
}

// Webpack will replace 'require' with '__webpack_require__'
// '__non_webpack_require__' is a proxy to Node 'require'
// The below code is to ensure that the server is run only when not requiring the bundle
declare const __non_webpack_require__: NodeRequire;
const mainModule = __non_webpack_require__.main;
const moduleFilename = mainModule && mainModule.filename || '';
if (moduleFilename === __filename || moduleFilename.includes('iisnode')) {
run();
}

export * from './src/main.server';

This file sets up an Express server that uses Angular Universal to render your application.

Step 3: Running Your Universal App

To build and run your Universal application:

bash
npm run build:ssr
npm run serve:ssr

The first command builds both the client and server versions of your app, and the second command starts the Universal server.

Real-World Example: Building a Blog with Angular Universal

Let's see how Angular Universal can enhance a simple blog application:

Blog Post Component

typescript
// blog-post.component.ts
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { BlogService } from '../services/blog.service';
import { Meta, Title } from '@angular/platform-browser';

@Component({
selector: 'app-blog-post',
template: `
<article *ngIf="post">
<h1>{{post.title}}</h1>
<p class="meta">By {{post.author}} on {{post.date | date}}</p>
<img [src]="post.featuredImage" [alt]="post.title">
<div [innerHTML]="post.content"></div>
</article>
`
})
export class BlogPostComponent implements OnInit {
post: any;

constructor(
private route: ActivatedRoute,
private blogService: BlogService,
private title: Title,
private meta: Meta
) {}

ngOnInit() {
const slug = this.route.snapshot.paramMap.get('slug');
this.blogService.getPost(slug).subscribe(post => {
this.post = post;

// Update page title and meta tags (great for SEO!)
this.title.setTitle(post.title);
this.meta.updateTag({ name: 'description', content: post.excerpt });
this.meta.updateTag({ property: 'og:title', content: post.title });
this.meta.updateTag({ property: 'og:description', content: post.excerpt });
this.meta.updateTag({ property: 'og:image', content: post.featuredImage });
});
}
}

The benefit here is that search engines and social media platforms will see the fully rendered HTML with proper meta tags, making your content discoverable and shareable.

Common Challenges and Solutions

1. Browser-Specific APIs

In Universal apps, your code runs in both browser and Node.js environments. Browser-specific APIs (like window, document, localStorage) don't exist on the server.

Solution: Use Angular's platform detection:

typescript
import { isPlatformBrowser, isPlatformServer } from '@angular/common';
import { Component, Inject, PLATFORM_ID } from '@angular/core';

@Component({/* ... */})
export class MyComponent {
constructor(@Inject(PLATFORM_ID) private platformId: Object) {
if (isPlatformBrowser(this.platformId)) {
// Browser-only code
console.log('Running in the browser');
const windowWidth = window.innerWidth;
}

if (isPlatformServer(this.platformId)) {
// Server-only code
console.log('Running on the server');
}
}
}

2. Transfer State

Sometimes you need to fetch data on the server and then reuse it on the client without fetching it again.

Solution: Use Angular's TransferState:

typescript
// In your server module (app.server.module.ts)
import { ServerTransferStateModule } from '@angular/platform-server';

@NgModule({
imports: [
AppModule,
ServerModule,
ServerTransferStateModule // Add this
],
bootstrap: [AppComponent],
})
export class AppServerModule {}
typescript
// In your browser module (app.module.ts)
import { BrowserTransferStateModule } from '@angular/platform-browser';

@NgModule({
imports: [
BrowserModule.withServerTransition({ appId: 'your-app' }),
BrowserTransferStateModule, // Add this
// other imports...
],
declarations: [AppComponent],
bootstrap: [AppComponent]
})
export class AppModule {}
typescript
// In your service
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { tap, catchError } from 'rxjs/operators';
import { makeStateKey, TransferState } from '@angular/platform-browser';
import { isPlatformServer } from '@angular/common';
import { Inject, PLATFORM_ID } from '@angular/core';

const DATA_KEY = makeStateKey<any>('my-data');

@Injectable()
export class DataService {
constructor(
private http: HttpClient,
private transferState: TransferState,
@Inject(PLATFORM_ID) private platformId: Object
) {}

getData(): Observable<any> {
// Check if we have data from transfer state
if (this.transferState.hasKey(DATA_KEY)) {
const data = this.transferState.get(DATA_KEY, null);
this.transferState.remove(DATA_KEY);
return of(data);
}

// If not, make the API call
return this.http.get('/api/data').pipe(
tap(data => {
if (isPlatformServer(this.platformId)) {
this.transferState.set(DATA_KEY, data);
}
}),
catchError(error => {
console.error('Error fetching data', error);
return of(null);
})
);
}
}

Summary

Angular Universal brings server-side rendering capabilities to Angular applications, resulting in improved SEO, faster initial load times, and better user experiences. In this introduction, we've covered:

  • What Angular Universal is and its benefits
  • How to set up Angular Universal in an existing Angular project
  • How to handle common challenges like browser APIs and transferring state
  • A real-world example showcasing SEO benefits

By implementing Angular Universal, you're taking a significant step toward making your Angular applications more performant, accessible, and search engine friendly.

Additional Resources

Exercise

  1. Add Angular Universal to an existing Angular project of your choice.
  2. Create a component that displays dynamic content (like a blog post or product details) and implement proper SEO metadata.
  3. Implement TransferState to avoid duplicate data fetching between server and client.
  4. Test your application with JavaScript disabled to see how it renders with just server-side content.


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)