Angular Universal
Be SEO, CDN & user friendly!
Maciej Treder
AKAMAI TECHNOLOGIES
@maciejtreder
@maciejtreder
• Kraków, Poland
• Senior Software Development Engineer in Test 

Akamai Technologies
• Angular passionate
• Open source contributor (founder of @ng-toolkit)
• Articles writer
Outline
• Why SPAs are not SEO?
• Server-side rendering concept
• Working with server-side and browser-side code
• API calls performance
• What’s more? & Deployment
• Prerendering & Summary
ng build
• Ahead of Time compilation
We are ready for —prod!
• Ahead of Time compilation
• Minified
• Tree-shaked
ng build vs. —prod
Common SPA problem
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteRule ^index.html$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . index.html [L]
</IfModule>
.htaccess
Common SPA problem
GET /
GET /anotherPage
index.html
G
ET
/subpage
G
ET
/contact
GET /home
Common SPA problem
GET / GET /anotherPage
Common SPA problem
Outline
• Why SPAs are not SEO?
• Server-side rendering concept
• Working with server-side and browser-side code
• API calls performance
• What’s more? & Deployment
• Prerendering & Summary
Server Side Rendering
GET /
GET /anotherPage
Is it worth?
curl localhost:8080
<!DOCTYPE html><html lang="en"><head>
<meta charset="utf-8">
<title>SomeProject</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<link rel="stylesheet" href="styles.3ff695c00d717f2d2a11.css"><style ng-transition="app-root">
/*# sourceMappingURL=data:application/
json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IiIsImZpbGUiOiJzcmMvYXBwL2FwcC5jb21wb25lbnQuY3NzIn0= */</
style></head>
<body>
<script type="text/javascript" src="runtime.26209474bfa8dc87a77c.js"></script><script type="text/javascript" src="es2015-
polyfills.c5dd28b362270c767b34.js" nomodule=""></script><script type="text/javascript" src="polyfills.8bbb231b43165d65d357.js"></
script><script type="text/javascript" src="main.8a9128130a3a38dd7ee5.js"></script>
<script id="app-root-state" type="application/json">{}</script></body></html>
<app-root _nghost-sc0="" ng-version="7.2.9"><div _ngcontent-sc0="" style="text-align:center"><h1 _ngcontent-sc0=""> Welcome
to someProject! </h1><img _ngcontent-sc0="" alt="Angular Logo" src="data:image/
svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTAgMjUwIj4KICAgIDxwYXRoIGZpbGw9IiNERD
AwMzEiIGQ9Ik0xMjUgMzBMMzEuOSA2My4ybDE0LjIgMTIzLjFMMTI1IDIzMGw3OC45LTQzLjcgMTQuMi0xMjMuMXoiIC8+CiAgICA8cGF0aCBmaWxsPSIjQzMwMDJ
GIiBkPSJNMTI1IDMwdjIyLjItLjFWMjMwbDc4LjktNDMuNyAxNC4yLTEyMy4xTDEyNSAzMHoiIC8+CiAgICA8cGF0aCAgZmlsbD0iI0ZGRkZGRiIgZD0iTTEyNSA1
Mi4xTDY2LjggMTgyLjZoMjEuN2wxMS43LTI5LjJoNDkuNGwxMS43IDI5LjJIMTgzTDEyNSA1Mi4xem0xNyA4My4zaC0zNGwxNy00MC45IDE3IDQwLjl6IiAvPgogI
Dwvc3ZnPg==" width="300"></div><h2 _ngcontent-sc0="">Here are some links to help you start: </h2><ul _ngcontent-sc0=""><li
_ngcontent-sc0=""><h2 _ngcontent-sc0=""><a _ngcontent-sc0="" href="https://angular.io/tutorial" rel="noopener"
target="_blank">Tour of Heroes</a></h2></li><li _ngcontent-sc0=""><h2 _ngcontent-sc0=""><a _ngcontent-sc0="" href="https://
angular.io/cli" rel="noopener" target="_blank">CLI Documentation</a></h2></li><li _ngcontent-sc0=""><h2 _ngcontent-sc0=""><a
_ngcontent-sc0="" href="https://blog.angular.io/" rel="noopener" target="_blank">Angular blog</a></h2></li></ul></app-root>
Is it worth?
—prod vs. universal
Angular Universal
Load HTML Bootstrap
Load HTML Bootstrap
SSR
NO SSR
First meaningful paint
First meaningful paint
How to start?
Official guide
https://angular.io/guide/universal
ng-toolkit
https://github.com/maciejtreder/ng-toolkit
ng add @nguniversal/express-engine —clientProject myProject
CREATE src/main.server.ts (220 bytes)
CREATE src/app/app.server.module.ts (318 bytes)
CREATE src/tsconfig.server.json (219 bytes)
CREATE webpack.server.config.js (1360 bytes)
CREATE server.ts (1500 bytes)
UPDATE package.json (1876 bytes)
UPDATE angular.json (4411 bytes)
UPDATE src/main.ts (432 bytes)
UPDATE src/app/app.module.ts (359 bytes)
Adjust your modules
app.module.tsapp.server.module.ts
@NgModule({
bootstrap: [AppComponent],
imports: [
BrowserModule.withServerTransition({appId: 'my-app'}),
//other imports
],
})
export class AppModule {}
import {NgModule} from '@angular/core';
import {ServerModule} from '@angular/platform-server';
import {ModuleMapLoaderModule} from
‘@nguniversal/module-map-ngfactory-loader';
import {AppModule} from './app.module';
import {AppComponent} from './app.component';
@NgModule({
imports: [
AppModule,
ServerModule,
ModuleMapLoaderModule
],
bootstrap: [AppComponent],
})
export class AppServerModule {}
Adjust your modules
Official guide
app.module.tsapp.server.module.ts
@NgModule({
declarations: [AppComponent],
imports: [
//common imports
]
})
export class AppModule {}
import {NgModule} from '@angular/core';
import {ServerModule} from '@angular/platform-server';
import {ModuleMapLoaderModule} from
‘@nguniversal/module-map-ngfactory-loader';
import {AppModule} from './app.module';
import {AppComponent} from './app.component';
@NgModule({
imports: [
AppModule,
ServerModule,
ModuleMapLoaderModule,
//server specific imports
],
bootstrap: [AppComponent],
})
export class AppServerModule {}
app.browser.module.ts
@NgModule({
bootstrap: [AppComponent],
imports: [
AppModule,
BrowserModule.withServerTransition({appId: 'my-app'}),
//browser specific imports
]
})
export class AppModule {}
//browser specific imports
//server specific imports
ng add @ng-toolkit/universal
CREATE local.js (248 bytes)
CREATE server.ts (1546 bytes)
CREATE webpack.server.config.js (1214 bytes)
CREATE src/main.server.ts (249 bytes)
CREATE src/tsconfig.server.json (485 bytes)
CREATE src/app/app.browser.module.ts (395 bytes)
CREATE src/app/app.server.module.ts (788 bytes)
CREATE ng-toolkit.json (95 bytes)
UPDATE package.json (1840 bytes)
UPDATE angular.json (4022 bytes)
UPDATE src/app/app.module.ts (417 bytes)
UPDATE src/main.ts (447 bytes)
And let’s go!
• npm run build:prod
• npm run server
Date: 2018-11-21T13:04:33.302Z
Hash: 1a82cb687d2e22b5d12b
Time: 10752ms
chunk {0} runtime.ec2944dd8b20ec099bf3.js (runtime) 1.41 kB [entry] [rendered]
chunk {1} main.09093ffa4ad7f66bc6ff.js (main) 169 kB [initial] [rendered]
chunk {2} polyfills.c6871e56cb80756a5498.js (polyfills) 37.5 kB [initial] [rendered]
chunk {3} styles.3bb2a9d4949b7dc120a9.css (styles) 0 bytes [initial] [rendered]
> my-app@0.0.0 server /Users/mtreder/myApp
> node local.js
Listening on: http://localhost:8080Listening on: http://localhost:8080
Under the hood
export const app = express();
app.use(compression());
app.use(cors());
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
const {AppServerModuleNgFactory, LAZY_MODULE_MAP} = require('./dist/server/main');
app.engine('html', ngExpressEngine({
bootstrap: AppServerModuleNgFactory,
providers: [
provideModuleMap(LAZY_MODULE_MAP)
]
}));
server.ts
Under the hood
app.get('/*', (req, res) => {
res.render('index', {req, res}, (err, html) => {
if (html) {
res.send(html);
} else {
console.error(err);
res.send(err);
}
});
});
server.ts
Under the hood
app.set('view engine', 'html');
app.set('views', './dist/browser');
app.get('*.*', express.static('./dist/browser', {
maxAge: '1y'
}));
server.ts
Server Side Rendering
GET /
GET /anotherPage
Outline
• Why SPAs are not SEO?
• Server-side rendering concept
• Working with server-side and browser-side code
• API calls performance
• What’s more? & Deployment
• Prerendering & Summary
Browser vs Server• document
• window
• navigator
• file system
• request
Browser vs Server
public ngOnInit(): void {
console.log(window.navigator.language);
}
Listening on: http://localhost:8080
ERROR ReferenceError: window is not defined
at AppComponent.module.exports../src/app/app.component.ts.AppComponent.ngOnInit
(/Users/mtreder/myApp/dist/server.js:118857:21)
at checkAndUpdateDirectiveInline (/Users/mtreder/myApp/dist/server.js:19504:19)
at checkAndUpdateNodeInline (/Users/mtreder/myApp/dist/server.js:20768:20)
at checkAndUpdateNode (/Users/mtreder/myApp/dist/server.js:20730:16)
at prodCheckAndUpdateNode (/Users/mtreder/myApp/dist/server.js:21271:5)
at Object.updateDirectives (/Users/mtreder/myApp/dist/server.js:118833:264)
at Object.updateDirectives (/Users/mtreder/myApp/dist/server.js:21059:72)
at Object.checkAndUpdateView (/Users/mtreder/myApp/dist/server.js:20712:14)
at ViewRef_.module.exports.ViewRef_.detectChanges (/Users/mtreder/myApp/dist/
server.js:19093:22)
at /Users/mtreder/myApp/dist/server.js:15755:63
ERROR ReferenceError: window is not defined
is server? is browser?
import { Component, Inject, PLATFORM_ID, OnInit } from '@angular/core';
import { isPlatformBrowser, isPlatformServer } from '@angular/common';
@Component({
selector: 'home-view',
templateUrl: './home.component.html'
})
export class HomeComponent implements OnInit {
constructor( private platformId) {}
public ngOnInit(): void {
if ( ) {
console.log('I am executed in the browser!’);
// window.url can be reached here
}
if (isPlatformServer(this.platformId)) {
console.log('I am executed in the server!’);
// window.url CAN’T be reached here
}
}
}
@Inject(PLATFORM_ID)
isPlatformBrowser(this.platformId)
isPlatformServer(this.platformId)
Wrapper services
• Determine if we are in the browser or server
• Retrieve window or request object
• Create ‘mock’ window from request object if necessary
REQUEST
import { Component, OnInit, Inject, PLATFORM_ID, Optional } from ‘@angular/core’;
import { REQUEST } from '@nguniversal/express-engine/tokens';
import { isPlatformServer } from '@angular/common';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
})
export class AppComponent implements OnInit {
constructor(
@Inject(REQUEST) private request: any,
@Inject(PLATFORM_ID) private platformId: any) {}
public ngOnInit(): void {
if (isPlatformServer(this.platformId)) {
console.log(this.request.headers);
}
}
}
import { REQUEST } from '@nguniversal/express-engine/tokens';
@Optional @Inject(REQUEST) private request: any,
console.log(this.request.headers);
REQUEST
Listening on: http://localhost:8080
{ host: 'localhost:8080',
connection: 'keep-alive',
'cache-control': 'max-age=0',
'upgrade-insecure-requests': '1',
'user-agent':
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36
(KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36',
accept:
'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8',
'accept-encoding': 'gzip, deflate, br',
'accept-language': 'en-US,en;q=0.9,ru;q=0.8',
'if-none-match': 'W/"40e-JviTST4QyiABJz2Lg+QxzZtiXv8"' }
'accept-language': 'en-US,en;q=0.9,ru;q=0.8',
Wrapper Service
@Injectable()
export class WindowService {
private _window: Window;
constructor(@Inject(PLATFORM_ID) platformId: any, @Optional @Inject(REQUEST) private request: any ) {
if (isPlatformServer(platformId)) {
this._window = {
navigator: {
language: this.request.headers['accept-language']
},
URL: this.request.headers.host + '' + this.request.url
};
} else {
this._window = window;
}
}
get nativeWindow(): any {
return this._window;
}
}
Wrapper Service
import { Component , OnInit, Inject} from '@angular/core';
import { WINDOW } from '@ng-toolkit/universal';
export class AppComponent implements OnInit {
constructor(@Inject(WINDOW) private window: Window) {}
public ngOnInit(): void {
console.log(window.navigator.language);
}
}
app.component.ts
console.log(this.window.navigator.language);
Wrapper Service
import { NgtUniversalModule } from '@ng-toolkit/universal';
import { NgModule } from '@angular/core';
@NgModule({
imports:[
NgtUniversalModule
]
})
export class AppModule { }
app.module.ts
Server/Browser specific code
@ngx-translate
• i18n module
• multiple ways of usage
{{‘Welcome to' | translate}}
<div [innerHTML]="'HELLO' | translate"></div>
{
"Welcome to": "Ласкаво просимо в"
}
uk.json
Server/Browser specific elements
import { TranslateHttpLoader } from '@ngx-translate/http-loader';
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
export function HttpLoaderFactory(http: HttpClient) {
return new TranslateHttpLoader(http);
}
@NgModule({
imports:[
TranslateModule.forRoot({
loader: {provide: TranslateLoader, useFactory: HttpLoaderFactory,
deps: [httpClient]}
})
]
})
export class AppBrowserModule {}
export function httpLoaderFactory(http: HttpClient): TranslateLoader {
return new TranslateHttpLoader(http);
}
HttpLoaderFactory,
app.browser.module.ts
Server/Browser specific elements
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { Observable, Observer } from 'rxjs';
import * as fs from 'fs';
export function universalLoader(): TranslateLoader {
return {
getTranslation: (lang: string) => {
return Observable.create((observer: Observer<any>) => {
observer.next(JSON.parse(fs.readFileSync(`./dist/assets/i18n/${lang}.json`, 'utf8')));
observer.complete();
});
}
} as TranslateLoader;
}
@NgModule({
imports:[
TranslateModule.forRoot({
loader: {provide: TranslateLoader, useFactory: universalLoader}
})
]
})
export class AppServerModule {}
export function universalLoader(): TranslateLoader {
app.server.module.ts
universalLoader }
Server/Browser specific elements
import { Component, OnInit, Inject } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { WINDOW } from '@ng-toolkit/universal';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
})
export class AppComponent implements OnInit {
constructor(
@Inject(WINDOW) private window: Window,
private translateService: TranslateService
) {}
public ngOnInit(): void {
this.translateService.use(this.window.navigator.language);
}
}
app.component.ts
this.translateService.use(this.window.navigator.language);
Server/Browser specific elements
https://www.twilio.com/blog/create-search-engine-friendly-internationalized-web-apps-
angular-universal-ngx-translate
Outline
• Why SPAs are not SEO?
• Server-side rendering concept
• Working with server-side and browser-side code
• API calls performance
• What’s more? & Deployment
• Prerendering & Summary
DRY(c)
Don’t repeat your calls
export class AppComponent implements OnInit {
public post: Observable<any>;
constructor(private httpClient: HttpClient) {}
public ngOnInit(): void {
this.post = this.httpClient.get('https://jsonplaceholder.typicode.com/posts/1');
}
}
2
1
3
4
5 6
external.api.com
external.api.com
my-website.com
DRY(c)
Don’t repeat your calls
1
2
3
HttpCacheModule
npm install @nguniversal/common
import { NgtUniversalModule } from '@ng-toolkit/universal';
import { CommonModule } from '@angular/common';
import { HttpClientModule } from '@angular/common/http';
import { TransferHttpCacheModule } from '@nguniversal/common';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
@NgModule({
declarations: [
AppComponent
],
imports:[
CommonModule,
NgtUniversalModule,
TransferHttpCacheModule,
HttpClientModule
]
})
export class AppModule { }
TransferHttpCacheModule
TransferState
• ServerTransferStateModule (@angular/platform-server)
• BrowserTransferStateModule (@angular/platform-browser)
• get(key, fallbackValue)
• set(key, value)
• has(key)
• remove(key)
HTTP_INTERCEPTOR
• Provided in the AppModule
• Every http request made with HttpClient goes threw it
• Used to transform request or response ie:
• Adding authentication headers
HTTP_INTERCEPTOR
@Injectable()
export class ServerStateInterceptor implements HttpInterceptor {
    constructor(private _transferState: TransferState) {}
    intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        return next.handle(req).pipe(tap(event => {
            if (event instanceof HttpResponse) {
                this._transferState.set(makeStateKey(req.url), event.body);
            }
        }));
    }
}
HTTP_INTERCEPTOR
@Injectable()
export class BrowserStateInterceptor implements HttpInterceptor {
    constructor(private _transferState: TransferState) { }
    intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        if (req.method !== 'GET') {
            return next.handle(req);
        }
        const storedResponse: string = this._transferState.get(makeStateKey(req.url), null);
        if (storedResponse) {
            const response = new HttpResponse({ body: storedResponse, status: 200 });
this._transferState.remove(makeStateKey(req.url));
            return of(response);
        }
        return next.handle(req);
    }
}
HTTP_INTERCEPTOR
import {HTTP_INTERCEPTORS } from '@angular/common/http';
providers: [
{
provide: HTTP_INTERCEPTORS,
useClass: BrowserStateInterceptor,
multi: true,
}
]
import {HTTP_INTERCEPTORS } from '@angular/common/http';
providers: [
{
provide: HTTP_INTERCEPTORS,
useClass: ServerStateInterceptor,
multi: true,
}
]
Performance
export class RouteResolverService implements Resolve<any> {
constructor(
private httpClient: HttpClient,
@Inject(PLATFORM_ID) private platformId: any
) {}
public resolve(): Observable<any> {
}
}
const watchdog: Observable<number> = timer(500);
if (isPlatformBrowser(this.platformId)) {
return this.httpClient.get<any>('https://jsonplaceholder.typicode.com/posts/1');
}
return Observable.create(subject => {
this.httpClient.get<any>('https://jsonplaceholder.typicode.com/posts/1')
.subscribe(response => {
subject.next(response);
subject.complete();
});
})
.pipe(takeUntil(watchdog))
watchdog.subscribe(() => {
subject.next('timeout');
subject.complete()
})
0.5sec
0.5sec
DRY(c) & Performance
https://www.twilio.com/blog/faster-javascript-web-apps-angular-universal-transferstate-api-watchdog
Outline
• Why SPAs are not SEO?
• Server-side rendering concept
• Working with server-side and browser-side code
• API calls performance
• What’s more? & Deployment
• Prerendering & Summary
It’s not only SSR
const listFiles = (callBack) => {
return fs.readdir('./user_upload', callBack);
};
app.engine('html', ngExpressEngine({
bootstrap: AppServerModuleNgFactory,
providers: [
provideModuleMap(LAZY_MODULE_MAP),
{provide: 'LIST_FILES', useValue: listFiles}
]
}));
constructor(
private http: HttpClient,
@Optional() @Inject('LIST_FILES') private listFiles: (callback) => void,
@Inject(PLATFORM_ID) private platformId: any,
private transferState: TransferState
) {
const transferKey: StateKey<string> = makeStateKey<string>('fileList');
if (isPlatformServer(this.platformId)) {
this.listFiles((err, files) => {
this.fileList = files;
this.transferState.set(transferKey, this.fileList);
});
} else {
this.fileList = this.transferState.get<string[]>(transferKey, []);
}
}
server.ts
someService.ts
It’s not only SSR
https://www.twilio.com/blog/transfer-files-data-javascript-applications-angular-node-js
Let’s go Serverless!
• Function as a Service
• Event-driven
• Scalable
• Pay for the up-time
Let’s go Serverless!
https://www.twilio.com/blog/angular-universal-javascript-node-js-aws-lambda
Outline
• Why SPAs are not SEO?
• Server-side rendering concept
• Working with server-side and browser-side code
• API calls performance
• What’s more? & Deployment
• Prerendering & Summary
Prerender
• Generating HTML files at a build time
• Can be hosted from traditional hosting (ie. AWS S3)
• Doesn’t perform dynamic request
• https://github.com/maciejtreder/angular-ssr-prerender
Alternative
Summary
server-side renderingprerenderng build —prod
SEO
Performance
Difficulty
SEO + external calls
Additional back-end logic
@maciejtreder

Maciej Treder "Server-side rendering with Angular—be faster and more SEO, CDN and user-friendly!"

  • 1.
    Angular Universal Be SEO,CDN & user friendly! Maciej Treder AKAMAI TECHNOLOGIES
  • 2.
  • 3.
    @maciejtreder • Kraków, Poland •Senior Software Development Engineer in Test 
 Akamai Technologies • Angular passionate • Open source contributor (founder of @ng-toolkit) • Articles writer
  • 4.
    Outline • Why SPAsare not SEO? • Server-side rendering concept • Working with server-side and browser-side code • API calls performance • What’s more? & Deployment • Prerendering & Summary
  • 5.
    ng build • Aheadof Time compilation
  • 6.
    We are readyfor —prod! • Ahead of Time compilation • Minified • Tree-shaked
  • 7.
    ng build vs.—prod
  • 8.
    Common SPA problem <IfModulemod_rewrite.c> RewriteEngine On RewriteBase / RewriteRule ^index.html$ - [L] RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule . index.html [L] </IfModule> .htaccess
  • 9.
    Common SPA problem GET/ GET /anotherPage index.html G ET /subpage G ET /contact GET /home
  • 10.
    Common SPA problem GET/ GET /anotherPage
  • 11.
  • 12.
    Outline • Why SPAsare not SEO? • Server-side rendering concept • Working with server-side and browser-side code • API calls performance • What’s more? & Deployment • Prerendering & Summary
  • 13.
    Server Side Rendering GET/ GET /anotherPage
  • 14.
    Is it worth? curllocalhost:8080 <!DOCTYPE html><html lang="en"><head> <meta charset="utf-8"> <title>SomeProject</title> <base href="/"> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="icon" type="image/x-icon" href="favicon.ico"> <link rel="stylesheet" href="styles.3ff695c00d717f2d2a11.css"><style ng-transition="app-root"> /*# sourceMappingURL=data:application/ json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IiIsImZpbGUiOiJzcmMvYXBwL2FwcC5jb21wb25lbnQuY3NzIn0= */</ style></head> <body> <script type="text/javascript" src="runtime.26209474bfa8dc87a77c.js"></script><script type="text/javascript" src="es2015- polyfills.c5dd28b362270c767b34.js" nomodule=""></script><script type="text/javascript" src="polyfills.8bbb231b43165d65d357.js"></ script><script type="text/javascript" src="main.8a9128130a3a38dd7ee5.js"></script> <script id="app-root-state" type="application/json">{}</script></body></html> <app-root _nghost-sc0="" ng-version="7.2.9"><div _ngcontent-sc0="" style="text-align:center"><h1 _ngcontent-sc0=""> Welcome to someProject! </h1><img _ngcontent-sc0="" alt="Angular Logo" src="data:image/ svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTAgMjUwIj4KICAgIDxwYXRoIGZpbGw9IiNERD AwMzEiIGQ9Ik0xMjUgMzBMMzEuOSA2My4ybDE0LjIgMTIzLjFMMTI1IDIzMGw3OC45LTQzLjcgMTQuMi0xMjMuMXoiIC8+CiAgICA8cGF0aCBmaWxsPSIjQzMwMDJ GIiBkPSJNMTI1IDMwdjIyLjItLjFWMjMwbDc4LjktNDMuNyAxNC4yLTEyMy4xTDEyNSAzMHoiIC8+CiAgICA8cGF0aCAgZmlsbD0iI0ZGRkZGRiIgZD0iTTEyNSA1 Mi4xTDY2LjggMTgyLjZoMjEuN2wxMS43LTI5LjJoNDkuNGwxMS43IDI5LjJIMTgzTDEyNSA1Mi4xem0xNyA4My4zaC0zNGwxNy00MC45IDE3IDQwLjl6IiAvPgogI Dwvc3ZnPg==" width="300"></div><h2 _ngcontent-sc0="">Here are some links to help you start: </h2><ul _ngcontent-sc0=""><li _ngcontent-sc0=""><h2 _ngcontent-sc0=""><a _ngcontent-sc0="" href="https://angular.io/tutorial" rel="noopener" target="_blank">Tour of Heroes</a></h2></li><li _ngcontent-sc0=""><h2 _ngcontent-sc0=""><a _ngcontent-sc0="" href="https:// angular.io/cli" rel="noopener" target="_blank">CLI Documentation</a></h2></li><li _ngcontent-sc0=""><h2 _ngcontent-sc0=""><a _ngcontent-sc0="" href="https://blog.angular.io/" rel="noopener" target="_blank">Angular blog</a></h2></li></ul></app-root>
  • 15.
  • 16.
  • 17.
    Angular Universal Load HTMLBootstrap Load HTML Bootstrap SSR NO SSR First meaningful paint First meaningful paint
  • 18.
    How to start? Officialguide https://angular.io/guide/universal ng-toolkit https://github.com/maciejtreder/ng-toolkit
  • 19.
    ng add @nguniversal/express-engine—clientProject myProject CREATE src/main.server.ts (220 bytes) CREATE src/app/app.server.module.ts (318 bytes) CREATE src/tsconfig.server.json (219 bytes) CREATE webpack.server.config.js (1360 bytes) CREATE server.ts (1500 bytes) UPDATE package.json (1876 bytes) UPDATE angular.json (4411 bytes) UPDATE src/main.ts (432 bytes) UPDATE src/app/app.module.ts (359 bytes)
  • 20.
    Adjust your modules app.module.tsapp.server.module.ts @NgModule({ bootstrap:[AppComponent], imports: [ BrowserModule.withServerTransition({appId: 'my-app'}), //other imports ], }) export class AppModule {} import {NgModule} from '@angular/core'; import {ServerModule} from '@angular/platform-server'; import {ModuleMapLoaderModule} from ‘@nguniversal/module-map-ngfactory-loader'; import {AppModule} from './app.module'; import {AppComponent} from './app.component'; @NgModule({ imports: [ AppModule, ServerModule, ModuleMapLoaderModule ], bootstrap: [AppComponent], }) export class AppServerModule {}
  • 21.
    Adjust your modules Officialguide app.module.tsapp.server.module.ts @NgModule({ declarations: [AppComponent], imports: [ //common imports ] }) export class AppModule {} import {NgModule} from '@angular/core'; import {ServerModule} from '@angular/platform-server'; import {ModuleMapLoaderModule} from ‘@nguniversal/module-map-ngfactory-loader'; import {AppModule} from './app.module'; import {AppComponent} from './app.component'; @NgModule({ imports: [ AppModule, ServerModule, ModuleMapLoaderModule, //server specific imports ], bootstrap: [AppComponent], }) export class AppServerModule {} app.browser.module.ts @NgModule({ bootstrap: [AppComponent], imports: [ AppModule, BrowserModule.withServerTransition({appId: 'my-app'}), //browser specific imports ] }) export class AppModule {} //browser specific imports //server specific imports
  • 22.
    ng add @ng-toolkit/universal CREATElocal.js (248 bytes) CREATE server.ts (1546 bytes) CREATE webpack.server.config.js (1214 bytes) CREATE src/main.server.ts (249 bytes) CREATE src/tsconfig.server.json (485 bytes) CREATE src/app/app.browser.module.ts (395 bytes) CREATE src/app/app.server.module.ts (788 bytes) CREATE ng-toolkit.json (95 bytes) UPDATE package.json (1840 bytes) UPDATE angular.json (4022 bytes) UPDATE src/app/app.module.ts (417 bytes) UPDATE src/main.ts (447 bytes)
  • 23.
    And let’s go! •npm run build:prod • npm run server Date: 2018-11-21T13:04:33.302Z Hash: 1a82cb687d2e22b5d12b Time: 10752ms chunk {0} runtime.ec2944dd8b20ec099bf3.js (runtime) 1.41 kB [entry] [rendered] chunk {1} main.09093ffa4ad7f66bc6ff.js (main) 169 kB [initial] [rendered] chunk {2} polyfills.c6871e56cb80756a5498.js (polyfills) 37.5 kB [initial] [rendered] chunk {3} styles.3bb2a9d4949b7dc120a9.css (styles) 0 bytes [initial] [rendered] > my-app@0.0.0 server /Users/mtreder/myApp > node local.js Listening on: http://localhost:8080Listening on: http://localhost:8080
  • 24.
    Under the hood exportconst app = express(); app.use(compression()); app.use(cors()); app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: true })); const {AppServerModuleNgFactory, LAZY_MODULE_MAP} = require('./dist/server/main'); app.engine('html', ngExpressEngine({ bootstrap: AppServerModuleNgFactory, providers: [ provideModuleMap(LAZY_MODULE_MAP) ] })); server.ts
  • 25.
    Under the hood app.get('/*',(req, res) => { res.render('index', {req, res}, (err, html) => { if (html) { res.send(html); } else { console.error(err); res.send(err); } }); }); server.ts
  • 26.
    Under the hood app.set('viewengine', 'html'); app.set('views', './dist/browser'); app.get('*.*', express.static('./dist/browser', { maxAge: '1y' })); server.ts
  • 27.
    Server Side Rendering GET/ GET /anotherPage
  • 28.
    Outline • Why SPAsare not SEO? • Server-side rendering concept • Working with server-side and browser-side code • API calls performance • What’s more? & Deployment • Prerendering & Summary
  • 29.
    Browser vs Server•document • window • navigator • file system • request
  • 30.
    Browser vs Server publicngOnInit(): void { console.log(window.navigator.language); } Listening on: http://localhost:8080 ERROR ReferenceError: window is not defined at AppComponent.module.exports../src/app/app.component.ts.AppComponent.ngOnInit (/Users/mtreder/myApp/dist/server.js:118857:21) at checkAndUpdateDirectiveInline (/Users/mtreder/myApp/dist/server.js:19504:19) at checkAndUpdateNodeInline (/Users/mtreder/myApp/dist/server.js:20768:20) at checkAndUpdateNode (/Users/mtreder/myApp/dist/server.js:20730:16) at prodCheckAndUpdateNode (/Users/mtreder/myApp/dist/server.js:21271:5) at Object.updateDirectives (/Users/mtreder/myApp/dist/server.js:118833:264) at Object.updateDirectives (/Users/mtreder/myApp/dist/server.js:21059:72) at Object.checkAndUpdateView (/Users/mtreder/myApp/dist/server.js:20712:14) at ViewRef_.module.exports.ViewRef_.detectChanges (/Users/mtreder/myApp/dist/ server.js:19093:22) at /Users/mtreder/myApp/dist/server.js:15755:63 ERROR ReferenceError: window is not defined
  • 31.
    is server? isbrowser? import { Component, Inject, PLATFORM_ID, OnInit } from '@angular/core'; import { isPlatformBrowser, isPlatformServer } from '@angular/common'; @Component({ selector: 'home-view', templateUrl: './home.component.html' }) export class HomeComponent implements OnInit { constructor( private platformId) {} public ngOnInit(): void { if ( ) { console.log('I am executed in the browser!’); // window.url can be reached here } if (isPlatformServer(this.platformId)) { console.log('I am executed in the server!’); // window.url CAN’T be reached here } } } @Inject(PLATFORM_ID) isPlatformBrowser(this.platformId) isPlatformServer(this.platformId)
  • 32.
    Wrapper services • Determineif we are in the browser or server • Retrieve window or request object • Create ‘mock’ window from request object if necessary
  • 33.
    REQUEST import { Component,OnInit, Inject, PLATFORM_ID, Optional } from ‘@angular/core’; import { REQUEST } from '@nguniversal/express-engine/tokens'; import { isPlatformServer } from '@angular/common'; @Component({ selector: 'app-root', templateUrl: './app.component.html', }) export class AppComponent implements OnInit { constructor( @Inject(REQUEST) private request: any, @Inject(PLATFORM_ID) private platformId: any) {} public ngOnInit(): void { if (isPlatformServer(this.platformId)) { console.log(this.request.headers); } } } import { REQUEST } from '@nguniversal/express-engine/tokens'; @Optional @Inject(REQUEST) private request: any, console.log(this.request.headers);
  • 34.
    REQUEST Listening on: http://localhost:8080 {host: 'localhost:8080', connection: 'keep-alive', 'cache-control': 'max-age=0', 'upgrade-insecure-requests': '1', 'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36', accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8', 'accept-encoding': 'gzip, deflate, br', 'accept-language': 'en-US,en;q=0.9,ru;q=0.8', 'if-none-match': 'W/"40e-JviTST4QyiABJz2Lg+QxzZtiXv8"' } 'accept-language': 'en-US,en;q=0.9,ru;q=0.8',
  • 35.
    Wrapper Service @Injectable() export classWindowService { private _window: Window; constructor(@Inject(PLATFORM_ID) platformId: any, @Optional @Inject(REQUEST) private request: any ) { if (isPlatformServer(platformId)) { this._window = { navigator: { language: this.request.headers['accept-language'] }, URL: this.request.headers.host + '' + this.request.url }; } else { this._window = window; } } get nativeWindow(): any { return this._window; } }
  • 36.
    Wrapper Service import {Component , OnInit, Inject} from '@angular/core'; import { WINDOW } from '@ng-toolkit/universal'; export class AppComponent implements OnInit { constructor(@Inject(WINDOW) private window: Window) {} public ngOnInit(): void { console.log(window.navigator.language); } } app.component.ts console.log(this.window.navigator.language);
  • 37.
    Wrapper Service import {NgtUniversalModule } from '@ng-toolkit/universal'; import { NgModule } from '@angular/core'; @NgModule({ imports:[ NgtUniversalModule ] }) export class AppModule { } app.module.ts
  • 38.
    Server/Browser specific code @ngx-translate •i18n module • multiple ways of usage {{‘Welcome to' | translate}} <div [innerHTML]="'HELLO' | translate"></div> { "Welcome to": "Ласкаво просимо в" } uk.json
  • 39.
    Server/Browser specific elements import{ TranslateHttpLoader } from '@ngx-translate/http-loader'; import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; export function HttpLoaderFactory(http: HttpClient) { return new TranslateHttpLoader(http); } @NgModule({ imports:[ TranslateModule.forRoot({ loader: {provide: TranslateLoader, useFactory: HttpLoaderFactory, deps: [httpClient]} }) ] }) export class AppBrowserModule {} export function httpLoaderFactory(http: HttpClient): TranslateLoader { return new TranslateHttpLoader(http); } HttpLoaderFactory, app.browser.module.ts
  • 40.
    Server/Browser specific elements import{ TranslateLoader, TranslateModule } from '@ngx-translate/core'; import { Observable, Observer } from 'rxjs'; import * as fs from 'fs'; export function universalLoader(): TranslateLoader { return { getTranslation: (lang: string) => { return Observable.create((observer: Observer<any>) => { observer.next(JSON.parse(fs.readFileSync(`./dist/assets/i18n/${lang}.json`, 'utf8'))); observer.complete(); }); } } as TranslateLoader; } @NgModule({ imports:[ TranslateModule.forRoot({ loader: {provide: TranslateLoader, useFactory: universalLoader} }) ] }) export class AppServerModule {} export function universalLoader(): TranslateLoader { app.server.module.ts universalLoader }
  • 41.
    Server/Browser specific elements import{ Component, OnInit, Inject } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import { WINDOW } from '@ng-toolkit/universal'; @Component({ selector: 'app-root', templateUrl: './app.component.html', }) export class AppComponent implements OnInit { constructor( @Inject(WINDOW) private window: Window, private translateService: TranslateService ) {} public ngOnInit(): void { this.translateService.use(this.window.navigator.language); } } app.component.ts this.translateService.use(this.window.navigator.language);
  • 42.
  • 43.
    Outline • Why SPAsare not SEO? • Server-side rendering concept • Working with server-side and browser-side code • API calls performance • What’s more? & Deployment • Prerendering & Summary
  • 44.
    DRY(c) Don’t repeat yourcalls export class AppComponent implements OnInit { public post: Observable<any>; constructor(private httpClient: HttpClient) {} public ngOnInit(): void { this.post = this.httpClient.get('https://jsonplaceholder.typicode.com/posts/1'); } }
  • 45.
  • 46.
  • 47.
    HttpCacheModule npm install @nguniversal/common import{ NgtUniversalModule } from '@ng-toolkit/universal'; import { CommonModule } from '@angular/common'; import { HttpClientModule } from '@angular/common/http'; import { TransferHttpCacheModule } from '@nguniversal/common'; import { NgModule } from '@angular/core'; import { AppComponent } from './app.component'; @NgModule({ declarations: [ AppComponent ], imports:[ CommonModule, NgtUniversalModule, TransferHttpCacheModule, HttpClientModule ] }) export class AppModule { } TransferHttpCacheModule
  • 48.
    TransferState • ServerTransferStateModule (@angular/platform-server) •BrowserTransferStateModule (@angular/platform-browser) • get(key, fallbackValue) • set(key, value) • has(key) • remove(key)
  • 49.
    HTTP_INTERCEPTOR • Provided inthe AppModule • Every http request made with HttpClient goes threw it • Used to transform request or response ie: • Adding authentication headers
  • 50.
    HTTP_INTERCEPTOR @Injectable() export class ServerStateInterceptorimplements HttpInterceptor {     constructor(private _transferState: TransferState) {}     intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {         return next.handle(req).pipe(tap(event => {             if (event instanceof HttpResponse) {                 this._transferState.set(makeStateKey(req.url), event.body);             }         }));     } }
  • 51.
    HTTP_INTERCEPTOR @Injectable() export class BrowserStateInterceptorimplements HttpInterceptor {     constructor(private _transferState: TransferState) { }     intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {         if (req.method !== 'GET') {             return next.handle(req);         }         const storedResponse: string = this._transferState.get(makeStateKey(req.url), null);         if (storedResponse) {             const response = new HttpResponse({ body: storedResponse, status: 200 }); this._transferState.remove(makeStateKey(req.url));             return of(response);         }         return next.handle(req);     } }
  • 52.
    HTTP_INTERCEPTOR import {HTTP_INTERCEPTORS }from '@angular/common/http'; providers: [ { provide: HTTP_INTERCEPTORS, useClass: BrowserStateInterceptor, multi: true, } ] import {HTTP_INTERCEPTORS } from '@angular/common/http'; providers: [ { provide: HTTP_INTERCEPTORS, useClass: ServerStateInterceptor, multi: true, } ]
  • 53.
    Performance export class RouteResolverServiceimplements Resolve<any> { constructor( private httpClient: HttpClient, @Inject(PLATFORM_ID) private platformId: any ) {} public resolve(): Observable<any> { } } const watchdog: Observable<number> = timer(500); if (isPlatformBrowser(this.platformId)) { return this.httpClient.get<any>('https://jsonplaceholder.typicode.com/posts/1'); } return Observable.create(subject => { this.httpClient.get<any>('https://jsonplaceholder.typicode.com/posts/1') .subscribe(response => { subject.next(response); subject.complete(); }); }) .pipe(takeUntil(watchdog)) watchdog.subscribe(() => { subject.next('timeout'); subject.complete() })
  • 54.
  • 55.
  • 56.
  • 57.
    Outline • Why SPAsare not SEO? • Server-side rendering concept • Working with server-side and browser-side code • API calls performance • What’s more? & Deployment • Prerendering & Summary
  • 58.
    It’s not onlySSR const listFiles = (callBack) => { return fs.readdir('./user_upload', callBack); }; app.engine('html', ngExpressEngine({ bootstrap: AppServerModuleNgFactory, providers: [ provideModuleMap(LAZY_MODULE_MAP), {provide: 'LIST_FILES', useValue: listFiles} ] })); constructor( private http: HttpClient, @Optional() @Inject('LIST_FILES') private listFiles: (callback) => void, @Inject(PLATFORM_ID) private platformId: any, private transferState: TransferState ) { const transferKey: StateKey<string> = makeStateKey<string>('fileList'); if (isPlatformServer(this.platformId)) { this.listFiles((err, files) => { this.fileList = files; this.transferState.set(transferKey, this.fileList); }); } else { this.fileList = this.transferState.get<string[]>(transferKey, []); } } server.ts someService.ts
  • 59.
    It’s not onlySSR https://www.twilio.com/blog/transfer-files-data-javascript-applications-angular-node-js
  • 60.
    Let’s go Serverless! •Function as a Service • Event-driven • Scalable • Pay for the up-time
  • 61.
  • 62.
    Outline • Why SPAsare not SEO? • Server-side rendering concept • Working with server-side and browser-side code • API calls performance • What’s more? & Deployment • Prerendering & Summary
  • 63.
    Prerender • Generating HTMLfiles at a build time • Can be hosted from traditional hosting (ie. AWS S3) • Doesn’t perform dynamic request • https://github.com/maciejtreder/angular-ssr-prerender Alternative
  • 64.
    Summary server-side renderingprerenderng build—prod SEO Performance Difficulty SEO + external calls Additional back-end logic
  • 65.