Skip to content

Commit feccb6c

Browse files
committed
Angular universal integration (aviabird#224)
Why? In order to support SEO, we should have some provision to allow google crawl the webpages with all the relevant information. This needed a requirement for server side rendered version of this application. This change addresses the need by: Adds Angular universal support. Updated the code using window, document, local storage with Browser platform checks Updated meta information for main app component. [delivers #159558419]
1 parent 80752c2 commit feccb6c

33 files changed

Lines changed: 1172 additions & 645 deletions

.vscode/settings.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,6 @@
1616
"*.csproj.user": true,
1717
"*.suo": true,
1818
".docker": false,
19-
"docs": false
19+
"docs": true
2020
}
2121
}

angular.json

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
"build": {
1111
"builder": "@angular-devkit/build-angular:browser",
1212
"options": {
13-
"outputPath": "dist",
13+
"outputPath": "dist/browser",
1414
"index": "src/index.html",
1515
"main": "src/main.ts",
1616
"tsConfig": "src/tsconfig.app.json",
@@ -106,6 +106,56 @@
106106
}
107107
}
108108
},
109+
"server": {
110+
"builder": "@angular-devkit/build-angular:server",
111+
"options": {
112+
"outputPath": "dist/server",
113+
"main": "src/main.server.ts",
114+
"tsConfig": "src/tsconfig.server.json"
115+
},
116+
"configurations": {
117+
"mock-ng-spree": {
118+
"fileReplacements": [
119+
{
120+
"replace": "src/environments/environment.ts",
121+
"with": "src/environments/environment.mock-ng-spree.ts"
122+
}
123+
]
124+
},
125+
"dev-ng-spree": {
126+
"fileReplacements": [
127+
{
128+
"replace": "src/environments/environment.ts",
129+
"with": "src/environments/environment.dev-ng-spree.ts"
130+
}
131+
]
132+
},
133+
"prod-ng-spree": {
134+
"fileReplacements": [
135+
{
136+
"replace": "src/environments/environment.ts",
137+
"with": "src/environments/environment.prod-ng-spree.ts"
138+
}
139+
]
140+
},
141+
"dev-custom": {
142+
"fileReplacements": [
143+
{
144+
"replace": "src/environments/environment.ts",
145+
"with": "src/environments/environment.dev-custom.ts"
146+
}
147+
]
148+
},
149+
"prod-custom": {
150+
"fileReplacements": [
151+
{
152+
"replace": "src/environments/environment.ts",
153+
"with": "src/environments/environment.prod-custom.ts"
154+
}
155+
]
156+
}
157+
}
158+
},
109159
"serve": {
110160
"builder": "@angular-devkit/build-angular:dev-server",
111161
"options": {

firebase.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"hosting": {
3-
"public": "dist",
3+
"public": "dist/browser",
44
"rewrites": [
55
{
66
"source": "**",

package.json

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,12 @@
2020
"compodoc": "./node_modules/.bin/compodoc -p src/tsconfig.app.json -d docs/",
2121
"sw": "sw-precache --root=dist --config=sw-precache-config.js",
2222
"bundle-report": "webpack-bundle-analyzer dist/stats.json",
23-
"static-serve": "cd dist && live-server --port=4200 --host=localhost --entry-file=/index.html"
23+
"static-serve": "cd dist/browser && live-server --port=4200 --host=localhost --entry-file=/index.html",
24+
"start:ssr:prod-custom": "npm run build:client-and-server-bundles:prod-custom && npm run webpack:server && node dist/server",
25+
"start:ssr:prod-ng-spree": "npm run build:client-and-server-bundles:prod-ng-spree && npm run webpack:server && node dist/server",
26+
"build:client-and-server-bundles:prod-custom": "npm run build:prod-custom && ng run angularspree:server:prod-custom",
27+
"build:client-and-server-bundles:prod-ng-spree": "npm run build:prod-ng-spree && ng run angularspree:server:prod-ng-spree",
28+
"webpack:server": "webpack --config webpack.server.config.js --progress --colors"
2429
},
2530
"private": true,
2631
"dependencies": {
@@ -32,6 +37,7 @@
3237
"@angular/http": "^6.1.1",
3338
"@angular/platform-browser": "^6.1.1",
3439
"@angular/platform-browser-dynamic": "^6.1.1",
40+
"@angular/platform-server": "^6.1.1",
3541
"@angular/pwa": "^0.7.2",
3642
"@angular/router": "^6.1.1",
3743
"@angular/service-worker": "^6.1.1",
@@ -41,6 +47,9 @@
4147
"@ngrx/router-store": "^6.1.0",
4248
"@ngrx/store": "^6.1.0",
4349
"@ngu/carousel": "^1.4.8",
50+
"@nguniversal/common": "^6.0.0",
51+
"@nguniversal/express-engine": "^6.0.0",
52+
"@nguniversal/module-map-ngfactory-loader": "^6.0.0",
4453
"@ngx-lite/input-star-rating": "^0.1.5",
4554
"@ngx-lite/json-ld": "^0.4.2",
4655
"@ngx-progressbar/core": "^5.0.1",
@@ -60,6 +69,7 @@
6069
"reselect": "^3.0.1",
6170
"rxjs": "^6.2.2",
6271
"rxjs-compat": "^6.2.2",
72+
"ts-loader": "^4.4.2",
6373
"web-animations-js": "^2.3.1",
6474
"webpack-bundle-analyzer": "^2.13.1",
6575
"zone.js": "^0.8.26"
@@ -96,10 +106,11 @@
96106
"sw-precache": "^5.2.1",
97107
"ts-node": "~7.0.0",
98108
"tslint": "~5.11.0",
99-
"typescript": "~2.9.0"
109+
"typescript": "~2.9.0",
110+
"webpack-cli": "^3.1.0"
100111
},
101112
"description": "Spree for Angular2",
102113
"main": "index.js",
103114
"repository": "[email protected]:aviabird/angularspree.git",
104115
"author": "Pankaj Rawat <[email protected]>"
105-
}
116+
}

server.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// These are important and needed before anything else
2+
import 'zone.js/dist/zone-node';
3+
import 'reflect-metadata';
4+
5+
import { enableProdMode } from '@angular/core';
6+
7+
import * as express from 'express';
8+
import { join } from 'path';
9+
10+
// Faster server renders w/ Prod mode (dev mode never needed)
11+
enableProdMode();
12+
13+
// Express server
14+
const app = express();
15+
16+
const PORT = process.env.PORT || 4000;
17+
const DIST_FOLDER = join(process.cwd(), 'dist');
18+
19+
// * NOTE :: leave this as require() since this file is built Dynamically from webpack
20+
const { AppServerModuleNgFactory, LAZY_MODULE_MAP } = require('./dist/server/main');
21+
22+
// Express Engine
23+
import { ngExpressEngine } from '@nguniversal/express-engine';
24+
// Import module map for lazy loading
25+
import { provideModuleMap } from '@nguniversal/module-map-ngfactory-loader';
26+
27+
app.engine('html', ngExpressEngine({
28+
bootstrap: AppServerModuleNgFactory,
29+
providers: [
30+
provideModuleMap(LAZY_MODULE_MAP)
31+
]
32+
}));
33+
34+
app.set('view engine', 'html');
35+
app.set('views', join(DIST_FOLDER, 'browser'));
36+
37+
// TODO: implement data requests securely
38+
app.get('/api/*', (req, res) => {
39+
res.status(404).send('data requests are not supported');
40+
});
41+
42+
// Server static files from /browser
43+
app.get('*.*', express.static(join(DIST_FOLDER, 'browser')));
44+
45+
// All regular routes use the Universal engine
46+
app.get('*', (req, res) => {
47+
res.render('index', { req });
48+
});
49+
50+
// Start up the Node server
51+
app.listen(PORT, () => {
52+
console.log(`Node server listening on http://localhost:${PORT}`);
53+
});

src/app/app.component.ts

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@ import { AppState } from './interfaces';
77
import { Store } from '@ngrx/store';
88
import { Subscription, Observable } from 'rxjs';
99
import { CheckoutService } from './core/services/checkout.service';
10-
import { Component, OnInit, OnDestroy } from '@angular/core';
10+
import { Component, OnInit, OnDestroy, Inject, PLATFORM_ID } from '@angular/core';
1111
import { Router, NavigationEnd } from '@angular/router';
1212
import { Title, Meta } from '@angular/platform-browser';
13+
import { isPlatformBrowser } from '../../node_modules/@angular/common';
1314

1415
@Component({
1516
selector: 'app-root',
@@ -22,28 +23,33 @@ export class AppComponent implements OnInit, OnDestroy {
2223
currentStep: string;
2324
checkoutUrls = ['/checkout/cart', '/checkout/address', '/checkout/payment'];
2425
layoutState$: Observable<LayoutState>;
25-
schema = {
26-
'@context': 'https://schema.org',
27-
'@type': 'Organization',
28-
'name': environment.appName,
29-
'url': location.origin
30-
};
26+
schema = {};
3127

3228
constructor(
3329
private router: Router,
3430
private checkoutService: CheckoutService,
3531
private store: Store<AppState>,
3632
private metaTitle: Title,
37-
private meta: Meta
33+
private meta: Meta,
34+
@Inject(PLATFORM_ID) private platformId: any
3835
) {
3936
this.router.events
4037
.pipe(filter(e => e instanceof NavigationEnd))
4138
.subscribe((e: NavigationEnd) => {
4239
this.currentUrl = e.url;
4340
this.findCurrentStep(this.currentUrl);
44-
window.scrollTo(0, 0);
41+
if (isPlatformBrowser(this.platformId)) {
42+
window.scrollTo(0, 0);
43+
}
4544
this.addMetaInfo();
4645
});
46+
47+
this.schema = {
48+
'@context': 'https://schema.org',
49+
'@type': 'Organization',
50+
'name': environment.appName,
51+
'url': isPlatformBrowser(this.platformId) ? location.origin : ''
52+
};
4753
}
4854

4955
ngOnInit() {
@@ -58,13 +64,15 @@ export class AppComponent implements OnInit, OnDestroy {
5864
}
5965

6066
addFaviconIcon() {
61-
const link =
62-
document.querySelector(`link[rel*='icon']`) ||
63-
(document.createElement('link') as any);
64-
link.type = 'image/x-icon';
65-
link.rel = 'shortcut icon';
66-
link.href = environment.config.fevicon;
67-
document.getElementsByTagName('head')[0].appendChild(link);
67+
if (isPlatformBrowser(this.platformId)) {
68+
const link =
69+
document.querySelector(`link[rel*='icon']`) ||
70+
(document.createElement('link') as any);
71+
link.type = 'image/x-icon';
72+
link.rel = 'shortcut icon';
73+
link.href = environment.config.fevicon;
74+
document.getElementsByTagName('head')[0].appendChild(link);
75+
}
6876
}
6977

7078
isCheckoutRoute() {

src/app/app.module.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,15 @@ import { AppPreloadingStrategy } from './app_preloading_strategy';
22
import { myAuthConfig } from './oauth_config';
33
import { Ng2UiAuthModule } from 'ng2-ui-auth';
44
import { EffectsModule } from '@ngrx/effects';
5-
import { BrowserModule } from '@angular/platform-browser';
5+
import { BrowserModule, BrowserTransferStateModule } from '@angular/platform-browser';
66
import { NgModule } from '@angular/core';
77
import { FormsModule } from '@angular/forms';
88
import { HttpModule } from '@angular/http';
99
import { RouterModule } from '@angular/router';
1010
import { StoreDevtoolsModule } from '@ngrx/store-devtools';
1111
import { ServiceWorkerModule } from '@angular/service-worker';
1212
import { environment } from '../environments/environment';
13+
import {TransferHttpCacheModule} from '@nguniversal/common';
1314

1415
// Components
1516
import { AppComponent } from './app.component';
@@ -60,7 +61,9 @@ import { ToastrModule } from 'ngx-toastr';
6061
*/
6162
EffectsModule.forRoot([]),
6263
BrowserAnimationsModule,
63-
BrowserModule,
64+
BrowserModule.withServerTransition({ appId: 'ng-spree' }),
65+
BrowserTransferStateModule,
66+
TransferHttpCacheModule,
6467
FormsModule,
6568
HttpModule,
6669
HomeModule,

src/app/app.server.module.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { NgModule } from '@angular/core';
2+
import { ServerModule, ServerTransferStateModule } from '@angular/platform-server';
3+
import { ModuleMapLoaderModule } from '@nguniversal/module-map-ngfactory-loader';
4+
5+
import { AppModule } from './app.module';
6+
import { AppComponent } from './app.component';
7+
8+
@NgModule({
9+
imports: [
10+
AppModule,
11+
ServerModule,
12+
ModuleMapLoaderModule,
13+
ServerTransferStateModule
14+
],
15+
providers: [
16+
// Add universal-only providers here
17+
],
18+
bootstrap: [ AppComponent ],
19+
})
20+
export class AppServerModule {}

src/app/checkout/cart/cart.component.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ import { getTotalCartValue, getTotalCartItems, getItemTotal } from './../reducer
22
import { Observable } from 'rxjs';
33
import { AppState } from './../../interfaces';
44
import { Store } from '@ngrx/store';
5-
import { Component, OnInit } from '@angular/core';
5+
import { Component, OnInit, PLATFORM_ID, Inject } from '@angular/core';
6+
import { isPlatformBrowser } from '@angular/common';
67

78
@Component({
89
selector: 'app-cart',
@@ -17,14 +18,16 @@ export class CartComponent implements OnInit {
1718
shipTotal$: Observable<number>;
1819
itemTotal$: Observable<number>;
1920

20-
constructor(private store: Store<AppState>) {
21+
constructor(private store: Store<AppState>, @Inject(PLATFORM_ID) private platformId: any) {
2122
this.totalCartValue$ = this.store.select(getTotalCartValue);
2223
this.totalCartItems$ = this.store.select(getTotalCartItems);
2324
this.itemTotal$ = this.store.select(getItemTotal);
2425
}
2526

2627
ngOnInit() {
27-
this.screenwidth = window.innerWidth;
28+
if (isPlatformBrowser(this.platformId)) {
29+
this.screenwidth = window.innerWidth;
30+
}
2831
this.calculateInnerWidth();
2932
}
3033
calculateInnerWidth() {

src/app/checkout/order-failed/order-failed.component.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@ import { LineItem } from './../../core/models/line_item';
22
import { Order } from './../../core/models/order';
33
import { UserService } from './../../user/services/user.service';
44
import { ActivatedRoute, Router } from '@angular/router';
5-
import { Component, OnInit } from '@angular/core';
5+
import { Component, OnInit, PLATFORM_ID, Inject } from '@angular/core';
66
import { CheckoutService } from '../../core/services/checkout.service';
7+
import { isPlatformBrowser } from '../../../../node_modules/@angular/common';
78

89
@Component({
910
selector: 'app-order-failed',
@@ -20,7 +21,9 @@ export class OrderFailedComponent implements OnInit {
2021
private userService: UserService,
2122
private activatedRouter: ActivatedRoute,
2223
private route: Router,
23-
private checkoutService: CheckoutService) {
24+
private checkoutService: CheckoutService,
25+
@Inject(PLATFORM_ID) private platformId: any
26+
) {
2427
this.activatedRouter.queryParams
2528
.subscribe(params => {
2629
this.queryParams = params
@@ -47,8 +50,10 @@ export class OrderFailedComponent implements OnInit {
4750
retryPayment(order: Order) {
4851
this.checkoutService.makePayment(+order.total, order.bill_address, order.number)
4952
.subscribe((response: any) => {
50-
response = response
51-
window.open(response.url, '_self');
53+
response = response;
54+
if (isPlatformBrowser(this.platformId)) {
55+
window.open(response.url, '_self');
56+
}
5257
});
5358
}
5459

0 commit comments

Comments
 (0)