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:
- Improved SEO: Search engines can better index your content because they receive fully rendered HTML pages.
- Enhanced Performance: Users see content much faster because they don't have to wait for all JavaScript to download, parse and execute.
- Better User Experience: Visitors see a fully-rendered page quickly, rather than a loading spinner.
- 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:
- When a user or bot requests your application, the request goes to a server
- The server runs your Angular application in a Node.js environment
- Angular renders the requested page to HTML on the server
- The server sends the HTML to the client
- The client displays the HTML immediately while downloading the Angular application
- 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:
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
:
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
:
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:
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
// 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:
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:
// 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 {}
// 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 {}
// 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
- Official Angular Universal Documentation
- Angular Universal GitHub Repository
- Server-side rendering (SSR) with Angular Universal
Exercise
- Add Angular Universal to an existing Angular project of your choice.
- Create a component that displays dynamic content (like a blog post or product details) and implement proper SEO metadata.
- Implement TransferState to avoid duplicate data fetching between server and client.
- 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! :)