ng generate universal ssr-app
It creates a new app which:
main.server.ts
tsconfig.server.json
dist-server
foldernpm install
installs @angular/platform-server
ng build --prod --app=ssr-app --output-hashing=false
builds the Universal bundle outputting it to the dist-server
folder
we set --output-hashing=false
as we won't benefit from the build hashes in file names on the server
package.json
"scripts": {
"build:server-app": "ng build --prod --app=ssr-app --output-hashing=false"
}
which can then be run like this:
npm run build:server-app
package.json
"scripts": {
"build:server-app": "ng build --prod --app=ssr-app --output-hashing=false"
}
which can then be run like this:
npm run build:server-app
/home
route (1)Create an npm script named prerender.ts
in the prerender
folder:
import 'zone.js/dist/zone-node';
import {renderModuleFactory} from '@angular/platform-server';
import {writeFileSync} from 'fs';
const {AppServerModuleNgFactory} = require('../dist-server/main.bundle');
renderModuleFactory(AppServerModuleNgFactory, {
document: ' ',
url: '/home'
})
.then(html => {
console.log('Pre-rendering successful, saving prerender.html...');
writeFileSync(__dirname + '/prerender.html', html);
console.log('Done');
})
.catch(error => {
console.error('Error occurred:', error);
});
/home
route (2)Run the script (Windows):
node_modules\.bin\ts-node prerender\prerender.ts
Open the prerender\prerender.html
in your browser
Add a pre-render script to package.json
for convenience:
"scripts": {
"prerender": "./node_modules/.bin/ts-node ./prerender/prerender.ts"
}
/books
route (1)Change the url to /books
in prerender.ts
and run pre-rendering:
npm run prerender
Open the prerender\prerender.html
in your browser. No books have been rendered!
/books
route (2)There are two problems: the /api/book
endpoint is not available while pre-rendering (1) and this bug (2).
(1) can easily be solved starting the server:
npm run server
A workaround for (2) is to use an absolute instead of relative URL in the BookService while pre-rendering.
/books
route (3)We make use of the isPlatformServer
function and the PLATFORM_ID
token:
import {Inject, Injectable, PLATFORM_ID} from '@angular/core';
import {Observable} from 'rxjs/Observable';
import {Book} from './book';
import {HttpClient} from '@angular/common/http';
import {isPlatformServer} from '@angular/common';
@Injectable()
export class BookService {
private static BOOK_URI = 'api/book';
constructor(private http: HttpClient,
@Inject(PLATFORM_ID) private platformId) {
}
findAll(): Observable<Book> {
const hostName = isPlatformServer(this.platformId) ?
'http://localhost:9000/' : '';
return this.http.get<Book>(hostName + BookService.BOOK_URI);
}
}
1-prerender
branch...server/server.ts
) which serves the /api/book
endpoint.prerender.ts
we passed <app-root></app-root>
to renderModuleFactory()
. Now we'll pass the index.html
content of the regular frontend app:
npm run build
server/server.ts
import 'zone.js/dist/zone-node';
import * as express from 'express';
import {readFileSync} from 'fs';
import {enableProdMode} from '@angular/core';
import {renderModuleFactory} from '@angular/platform-server';
const {AppServerModuleNgFactory} = require('../dist-server/main.bundle');
enableProdMode();
const app = express();
app.get('/api/book', (req, res) => {
const books = readFileSync(__dirname + '/books.json', 'utf-8').toString();
res.json(JSON.parse(books));
}
);
const indexHtml = readFileSync(__dirname + '/../dist/index.html', 'utf-8').toString();
// serve js, css, ico, etc. files required by /../dist/index.html
app.get('*.*', express.static(__dirname + '/../dist', {
maxAge: '1y'
}));
// pre-render the content of the requested route
app.route('*').get((req, res) => {
renderModuleFactory(AppServerModuleNgFactory, {
document: indexHtml,
url: req.url
})
.then(html => {
res.status(200).send(html);
})
.catch(err => {
console.log(err);
res.sendStatus(500);
});
});
app.listen(9000, () => {
console.log('Server listening on port 9000!');
});
For details please refer to this Angular Material Github issue
2-ssr
branch...The data from the /api/book
endpoint is requested twice: while pre-rendering on the server (1) and from the browser after the app gets bootstrapped (2):
Apart from the unnecessary server hit, this leads to bad user experience:
import {Component, Inject, OnInit, PLATFORM_ID} from '@angular/core';
import {BookService} from '../book.service';
import {makeStateKey, TransferState} from '@angular/platform-browser';
import {Book} from '../book';
import {of as observableOf} from 'rxjs/observable/of';
import {tap} from 'rxjs/operators';
import {isPlatformServer} from '@angular/common';
@Component({
selector: 'app-book-overview',
templateUrl: './book-overview.component.html',
styleUrls: ['./book-overview.component.scss']
})
export class BookOverviewComponent implements OnInit {
books$;
constructor(private book: BookService, private transferState: TransferState, @Inject(PLATFORM_ID) private platformId) {
}
ngOnInit() {
const BOOKS_KEY = makeStateKey<Book[]>('books');
if (this.transferState.hasKey(BOOKS_KEY)) {
const books = this.transferState.get<Book[]>(BOOKS_KEY, []);
this.transferState.remove(BOOKS_KEY);
this.books$ = observableOf(books);
} else {
this.books$ = this.book.findAll().pipe(
tap(books => {
if (isPlatformServer(this.platformId)) {
this.transferState.set(BOOKS_KEY, books);
}
})
);
}
}
}
To get the code working both the client and the Universal app have to import specific modules: BrowserTransferStateModule
and ServerTransferStateModule
respectively.
While rendering on the server the books' data is put for transferring to the client. It then gets this data instead of requesting the server.
3-state-transfer
branch...We'll update the <title>
and <meta name="description">
elements specifically to routes.
import {Component} from '@angular/core';
import {Meta, Title} from '@angular/platform-browser';
@Component({
selector: 'app-home',
templateUrl: './home.component.html',
styleUrls: ['./home.component.scss']
})
export class HomeComponent {
constructor(private title: Title, private meta: Meta) {
this.title.setTitle('Welcome to the Book App');
this.meta.updateTag({name: 'description',
content: 'Welcome to the library'});
}
}
Social media crawlers can understand the Open Graph protocol, e.g <meta property="og:title" content="Books..." >
As in previous example we can make use of the Meta
service to do this.
4-seo-friendly
branch...