├── src ├── main.server.ts ├── assets │ ├── images │ │ └── universal.png │ ├── themes │ │ ├── site1 │ │ │ ├── images │ │ │ │ ├── favicon.ico │ │ │ │ └── logo.svg │ │ │ └── scss │ │ │ │ └── theme.scss │ │ └── site2 │ │ │ ├── images │ │ │ ├── favicon.ico │ │ │ └── logo.svg │ │ │ └── scss │ │ │ └── theme.scss │ └── scss │ │ └── main.scss ├── environments │ ├── environment.site1.dev.ts │ ├── environment.site1.prod.ts │ ├── environment.site2.dev.ts │ ├── environment.site2.prod.ts │ └── environment.ts ├── tsconfig.app.json ├── index.html ├── app │ ├── app.component.ts │ ├── app.component.html │ ├── home │ │ ├── home.component.html │ │ └── home.component.ts │ ├── geolocation.service.ts │ ├── lazy │ │ └── lazy.module.ts │ ├── app.server.module.ts │ └── app.module.ts ├── main.ts ├── tsconfig.server.json ├── locale │ ├── messages.xlf │ ├── messages.en.xlf │ ├── messages.fr.xlf │ └── messages.it.xlf └── polyfills.ts ├── static.paths.ts ├── .gitignore ├── tsconfig.json ├── server.tsconfig.json ├── prerender.ts ├── README.md ├── package.json ├── tslint.json ├── server.ts └── angular.json /src/main.server.ts: -------------------------------------------------------------------------------- 1 | export {AppServerModule} from './app/app.server.module'; 2 | -------------------------------------------------------------------------------- /static.paths.ts: -------------------------------------------------------------------------------- 1 | export const ROUTES = [ 2 | '/', 3 | '/lazy', 4 | '/lazy/nested' 5 | ]; 6 | -------------------------------------------------------------------------------- /src/assets/images/universal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mzuccaroli/universal-multisite-multilanguage/HEAD/src/assets/images/universal.png -------------------------------------------------------------------------------- /src/environments/environment.site1.dev.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true, 3 | siteAccess: 'site1', 4 | siteName: 'Demo Site 1' 5 | }; 6 | -------------------------------------------------------------------------------- /src/environments/environment.site1.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true, 3 | siteAccess: 'site1', 4 | siteName: 'Demo Site 1' 5 | }; 6 | -------------------------------------------------------------------------------- /src/environments/environment.site2.dev.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true, 3 | siteAccess: 'site2', 4 | siteName: 'Demo Site 2' 5 | }; 6 | -------------------------------------------------------------------------------- /src/environments/environment.site2.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true, 3 | siteAccess: 'site2', 4 | siteName: 'Demo Site 2' 5 | }; 6 | -------------------------------------------------------------------------------- /src/assets/themes/site1/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mzuccaroli/universal-multisite-multilanguage/HEAD/src/assets/themes/site1/images/favicon.ico -------------------------------------------------------------------------------- /src/assets/themes/site2/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mzuccaroli/universal-multisite-multilanguage/HEAD/src/assets/themes/site2/images/favicon.ico -------------------------------------------------------------------------------- /src/assets/scss/main.scss: -------------------------------------------------------------------------------- 1 | img.logo{ 2 | height: 3rem; 3 | float: left; 4 | margin-top: -0.5rem; 5 | } 6 | 7 | img.test-image{ 8 | float: left; 9 | height: 150px; 10 | margin-right: 30px; 11 | } 12 | -------------------------------------------------------------------------------- /src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "baseUrl": "./", 6 | "module": "es2015", 7 | "types": [ 8 | "node" 9 | ] 10 | }, 11 | "exclude": [ 12 | "test.ts", 13 | "**/*.spec.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NgUniversalDemo 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit} from '@angular/core'; 2 | import {environment} from '../environments/environment'; 3 | 4 | @Component({ 5 | selector: 'app-root', 6 | templateUrl: './app.component.html' 7 | }) 8 | export class AppComponent implements OnInit { 9 | public sitename; 10 | 11 | ngOnInit() { 12 | this.sitename = environment.siteName; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |

Multilanguage Multisite Angular Universal Demo {{sitename}}

4 | 8 |
9 | 10 |
11 |
12 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import {enableProdMode} from '@angular/core'; 2 | import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; 3 | 4 | import {AppModule} from './app/app.module'; 5 | import {environment} from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | document.addEventListener('DOMContentLoaded', () => { 12 | platformBrowserDynamic().bootstrapModule(AppModule); 13 | }); 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .DS_Store 3 | .vscode 4 | 5 | package-lock.json 6 | 7 | # Built # 8 | /__build__/ 9 | /__server_build__/ 10 | /node_modules/ 11 | /typings/ 12 | /tsd_typings/ 13 | /dist/ 14 | /dist-server/ 15 | /compiled/ 16 | 17 | # Node # 18 | *.log 19 | /npm-debug.log.* 20 | 21 | # Webpack # 22 | webpack.records.json 23 | 24 | # Angular # 25 | *.ngfactory.ts 26 | *.css.shim.ts 27 | *.ngsummary.json 28 | *.metadata.json 29 | *.shim.ngstyle.ts 30 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "outDir": "./dist/out-tsc", 5 | "sourceMap": true, 6 | "declaration": false, 7 | "moduleResolution": "node", 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "target": "es5", 11 | "typeRoots": [ 12 | "node_modules/@types" 13 | ], 14 | "lib": [ 15 | "es2017", 16 | "dom" 17 | ] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // The file contents for the current environment will overwrite these during build. 2 | // The build system defaults to the dev environment which uses `environment.ts`, but if you do 3 | // `ng build --env=prod` then `environment.prod.ts` will be used instead. 4 | // The list of which env maps to which file can be found in `.angular.json`. 5 | 6 | export const environment = { 7 | production: false, 8 | siteAccess: 'site1', 9 | siteName: 'Demo Site dev' 10 | }; 11 | -------------------------------------------------------------------------------- /server.tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "outDir": "./dist", 5 | "sourceMap": true, 6 | "declaration": false, 7 | "moduleResolution": "node", 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "target": "es5", 11 | "typeRoots": [ 12 | "node_modules/@types" 13 | ], 14 | "lib": [ 15 | "es2017", 16 | "dom" 17 | ] 18 | }, 19 | "include": [ 20 | "server.ts" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /src/app/home/home.component.html: -------------------------------------------------------------------------------- 1 |
2 | test-image 3 |
4 |

Hello i18n!

5 | userLanguage: {{userLanguage}}
6 | userCountry: {{userCountry}}
7 | currency: {{number | currency:'EUR': 'symbol' }}
8 | Today is {{today | date}}
9 | Or if you prefer, {{today | date:'fullDate'}}
10 | The time is {{today | date:'h:mm a z'}}
11 |
12 |
13 | -------------------------------------------------------------------------------- /src/tsconfig.server.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "baseUrl": "./", 6 | // Set the module format to "commonjs": 7 | "module": "commonjs", 8 | "types": [ 9 | "node" 10 | ] 11 | }, 12 | "exclude": [ 13 | "test.ts", 14 | "**/*.spec.ts" 15 | ], 16 | // Add "angularCompilerOptions" with the AppServerModule you wrote 17 | // set as the "entryModule". 18 | "angularCompilerOptions": { 19 | "entryModule": "app/app.server.module#AppServerModule" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/app/geolocation.service.ts: -------------------------------------------------------------------------------- 1 | import {Inject, Injectable, LOCALE_ID} from '@angular/core'; 2 | import {Observable, of} from 'rxjs'; 3 | 4 | @Injectable({ 5 | providedIn: 'root' 6 | }) 7 | export class GeolocationService { 8 | private localeData = { 9 | language: this.locale, 10 | country: this.locale.toUpperCase(), 11 | province: '' 12 | }; 13 | 14 | constructor( 15 | @Inject(LOCALE_ID) public locale: string, 16 | ) { 17 | } 18 | 19 | getClientLocale(): Observable { 20 | return of(this.localeData); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/locale/messages.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Hello i18n! 7 | 8 | app/home/home.component.ts 9 | 4 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/locale/messages.en.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Hello i18n!Hello! 7 | 8 | app/home/home.component.ts 9 | 4 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/locale/messages.fr.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Hello i18n!Bonjour! 7 | 8 | app/home/home.component.ts 9 | 4 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/locale/messages.it.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Hello i18n!Ciao! 7 | 8 | app/home/home.component.ts 9 | 4 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/assets/themes/site1/scss/theme.scss: -------------------------------------------------------------------------------- 1 | $theme-colors: ( 2 | text-color: #06060c, 3 | primary-color: #DD0031, 4 | primary-dark: #C3002F, 5 | light-color: #FFFFFF 6 | ); 7 | 8 | body { 9 | background: map-get($theme-colors, light-color); 10 | font-family: Arial, sans-serif; 11 | } 12 | 13 | .nav-links { 14 | background: map-get($theme-colors, primary-dark); 15 | } 16 | 17 | .nav-links a { 18 | color: map-get($theme-colors, light-color); 19 | display: inline-block; 20 | padding: 1rem; 21 | margin-right: 1rem; 22 | text-decoration: none; 23 | font-weight: bold; 24 | } 25 | 26 | .router-container { 27 | border: 0.5rem map-get($theme-colors, primary-color) solid; 28 | padding: 2rem; 29 | } 30 | -------------------------------------------------------------------------------- /src/app/lazy/lazy.module.ts: -------------------------------------------------------------------------------- 1 | import {Component, NgModule} from '@angular/core'; 2 | import {RouterModule} from '@angular/router'; 3 | 4 | @Component({ 5 | selector: 'app-lazy-view', 6 | template: ` 7 |

This is content from a lazy-loaded route

8 |
Check your Networks tab to see that the js file got loaded
9 |
10 |
/lazy/nested/ routes to the same page
11 | ` 12 | }) 13 | export class LazyComponent { 14 | } 15 | 16 | @NgModule({ 17 | declarations: [LazyComponent], 18 | imports: [ 19 | RouterModule.forChild([ 20 | {path: '', component: LazyComponent, pathMatch: 'full'} 21 | ]) 22 | ] 23 | }) 24 | export class LazyModule { 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/app/home/home.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit} from '@angular/core'; 2 | import {GeolocationService} from '../geolocation.service'; 3 | 4 | @Component({ 5 | selector: 'app-home', 6 | templateUrl: './home.component.html' 7 | }) 8 | export class HomeComponent implements OnInit { 9 | public userLanguage: string; 10 | public userCountry: string; 11 | public number = 10000; 12 | public today = Date.now(); 13 | 14 | 15 | constructor( 16 | private geolocation: GeolocationService, 17 | ) { 18 | } 19 | 20 | ngOnInit() { 21 | this.geolocation.getClientLocale().subscribe((data) => { 22 | this.userLanguage = data.language; 23 | this.userCountry = data.country; 24 | }); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/app/app.server.module.ts: -------------------------------------------------------------------------------- 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 | // The AppServerModule should import your AppModule followed 11 | // by the ServerModule from @angular/platform-server. 12 | AppModule, 13 | ServerModule, 14 | ModuleMapLoaderModule, 15 | ServerTransferStateModule, 16 | ], 17 | // Since the bootstrapped component is not inherited from your 18 | // imported AppModule, it needs to be repeated here. 19 | bootstrap: [AppComponent], 20 | }) 21 | export class AppServerModule {} 22 | -------------------------------------------------------------------------------- /src/assets/themes/site2/scss/theme.scss: -------------------------------------------------------------------------------- 1 | $theme-colors: ( 2 | text-color: #374249, 3 | primary-color: #00afc4, 4 | primary-dark: #008591, 5 | light-color: #f1f1f1 6 | ); 7 | 8 | body { 9 | background: map-get($theme-colors, light-color); 10 | font-family: Roboto, "Helvetica Neue Light", "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif; 11 | } 12 | 13 | .nav-links { 14 | background: map-get($theme-colors, primary-dark); 15 | } 16 | 17 | .nav-links a { 18 | color: map-get($theme-colors, light-color); 19 | display: inline-block; 20 | padding: 1rem; 21 | margin-right: 3rem; 22 | text-decoration: none; 23 | font-weight: bold; 24 | letter-spacing: 0.1rem; 25 | } 26 | 27 | .router-container { 28 | border: 0.5rem map-get($theme-colors, primary-color) solid; 29 | padding: 2rem; 30 | } 31 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import {BrowserModule} from '@angular/platform-browser'; 2 | import {NgModule} from '@angular/core'; 3 | import {RouterModule} from '@angular/router'; 4 | 5 | import {AppComponent} from './app.component'; 6 | import {HomeComponent} from './home/home.component'; 7 | import {TransferHttpCacheModule} from '@nguniversal/common'; 8 | 9 | @NgModule({ 10 | declarations: [ 11 | AppComponent, 12 | HomeComponent, 13 | ], 14 | imports: [ 15 | BrowserModule.withServerTransition({appId: 'my-app'}), 16 | RouterModule.forRoot([ 17 | {path: '', component: HomeComponent, pathMatch: 'full'}, 18 | {path: 'lazy', loadChildren: './lazy/lazy.module#LazyModule'} 19 | ]), 20 | TransferHttpCacheModule, 21 | ], 22 | providers: [], 23 | bootstrap: [AppComponent] 24 | }) 25 | export class AppModule { 26 | } 27 | -------------------------------------------------------------------------------- /src/assets/themes/site1/images/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 10 | 11 | 12 | 13 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/assets/themes/site2/images/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 10 | 11 | 12 | 13 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /prerender.ts: -------------------------------------------------------------------------------- 1 | // Load zone.js for the server. 2 | import 'zone.js/dist/zone-node'; 3 | import 'reflect-metadata'; 4 | import {readFileSync, writeFileSync, existsSync, mkdirSync} from 'fs'; 5 | import {join} from 'path'; 6 | 7 | import {enableProdMode} from '@angular/core'; 8 | // Faster server renders w/ Prod mode (dev mode never needed) 9 | enableProdMode(); 10 | 11 | // Import module map for lazy loading 12 | import {provideModuleMap} from '@nguniversal/module-map-ngfactory-loader'; 13 | import {renderModuleFactory} from '@angular/platform-server'; 14 | import {ROUTES} from './static.paths'; 15 | 16 | // * NOTE :: leave this as require() since this file is built Dynamically from webpack 17 | const {AppServerModuleNgFactory, LAZY_MODULE_MAP} = require('./server/main'); 18 | 19 | const BROWSER_FOLDER = join(process.cwd(), 'browser'); 20 | 21 | // Load the index.html file containing referances to your application bundle. 22 | const index = readFileSync(join('browser', 'index.html'), 'utf8'); 23 | 24 | let previousRender = Promise.resolve(); 25 | 26 | // Iterate each route path 27 | ROUTES.forEach(route => { 28 | const fullPath = join(BROWSER_FOLDER, route); 29 | 30 | // Make sure the directory structure is there 31 | if (!existsSync(fullPath)) { 32 | mkdirSync(fullPath); 33 | } 34 | 35 | // Writes rendered HTML to index.html, replacing the file if it already exists. 36 | previousRender = previousRender.then(_ => renderModuleFactory(AppServerModuleNgFactory, { 37 | document: index, 38 | url: route, 39 | extraProviders: [ 40 | provideModuleMap(LAZY_MODULE_MAP) 41 | ] 42 | })).then(html => writeFileSync(join(fullPath, 'index.html'), html)); 43 | }); 44 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE9, IE10 and IE11 requires all of the following polyfills. **/ 22 | // import 'core-js/es6/symbol'; 23 | // import 'core-js/es6/object'; 24 | // import 'core-js/es6/function'; 25 | // import 'core-js/es6/parse-int'; 26 | // import 'core-js/es6/parse-float'; 27 | // import 'core-js/es6/number'; 28 | // import 'core-js/es6/math'; 29 | // import 'core-js/es6/string'; 30 | // import 'core-js/es6/date'; 31 | // import 'core-js/es6/array'; 32 | // import 'core-js/es6/regexp'; 33 | // import 'core-js/es6/map'; 34 | // import 'core-js/es6/weak-map'; 35 | // import 'core-js/es6/set'; 36 | 37 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 38 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 39 | 40 | /** Evergreen browsers require these. **/ 41 | import 'core-js/es6/reflect'; 42 | import 'core-js/es7/reflect'; 43 | 44 | 45 | /** 46 | * Required to support Web Animations `@angular/animation`. 47 | * Needed for: All but Chrome, Firefox and Opera. http://caniuse.com/#feat=web-animation 48 | **/ 49 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 50 | 51 | 52 | 53 | /*************************************************************************************************** 54 | * Zone JS is required by Angular itself. 55 | */ 56 | import 'zone.js/dist/zone'; // Included with Angular CLI. 57 | 58 | 59 | 60 | /*************************************************************************************************** 61 | * APPLICATION IMPORTS 62 | */ 63 | 64 | /** 65 | * Date, currency, decimal and percent pipes. 66 | * Needed for: All but Chrome, Firefox, Edge, IE11 and Safari 10 67 | */ 68 | // import 'intl'; // Run `npm install --save intl`. 69 | /** 70 | * Need to import at least one locale-data with intl. 71 | */ 72 | // import 'intl/locale-data/jsonp/en'; 73 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Angular Universal Multisite Multilanguage Starter 2 | 3 | This repo demonstrates the use of Angular Universal Server-side Rendering applied to a multilanguage and multisite application 4 | Starting from a simple application codebase it automatically generates two parallel sites with differents assets and contens 5 | and translated version of them using i18n and xliffmerge. 6 | The express server is configured to serve each site in a different port (4000 and 4001) 7 | 8 | ### This project is based on Angular Universal Starter 9 | 10 | ![Angular Universal](https://angular.io/generated/images/marketing/concept-icons/universal.png) 11 | 12 | A minimal Angular starter for Universal JavaScript using the [Angular CLI](https://github.com/angular/angular-cli) 13 | If you're looking for the Angular Universal repo go to [**angular/universal**](https://github.com/angular/universal) 14 | 15 | ## Getting Started 16 | 17 | This demo is built following the [Angular CLI Wiki guide](https://github.com/angular/angular-cli/wiki/stories-universal-rendering) 18 | 19 | We're utilizing packages from the [Angular Universal @nguniversal](https://github.com/angular/universal) repo, such as [ng-module-map-ngfactory-loader](https://github.com/angular/universal/modules/module-map-ngfactory-loader) to enable Lazy Loading. 20 | 21 | --- 22 | 23 | ### Build Time Pre-rendering vs. Server-side Rendering (SSR) 24 | This repo demonstrates the use of Server-side Rendering. 25 | 26 | **Server-side Rendering (SSR)** 27 | * Happens at runtime 28 | * Uses `ngExpressEngine` to render your application on the fly at the requested url. 29 | 30 | --- 31 | 32 | ### Installation 33 | * `npm install` or `yarn` 34 | 35 | ### Development (Client-side only rendering) 36 | * run `npm run start:site1` which will start `ng serve` for site 1 configuration 37 | * run `npm run start:site2` which will start `ng serve` for site 2 configuration 38 | 39 | ### Production (also for testing SSR/Pre-rendering locally) 40 | **`npm run build:ssr && npm run serve:ssr`** - Compiles your application and spins up a Node Express to serve your Universal application: 41 | * the site 1 wil be served on `http://localhost:4000` 42 | * the site 2 wil be served on `http://localhost:4001` 43 | 44 | ### Languages and translations 45 | **`npm run extract-i18n`** - extract all the i18n ready to translate strings and put its into to messages.xlf files located in `src/locale` 46 | After a succesfull production build language specific static versions of the application will be placed in `dist/site1/en` `dist/site1/it` ecc.. 47 | The site 1 translated application will be served on : `http://localhost:4000/en` `http://localhost:4000/it` `http://localhost:4000/fr` 48 | The site 2 translated application will be served on : `http://localhost:4001/en` `http://localhost:4001/it` `http://localhost:4001/fr` 49 | 50 | 51 | **Note**: To deploy your static site to a static hosting platform you will have to deploy the `dist/site1` or `dist/site2` folder, rather than the usual `dist` 52 | 53 | 54 | # License 55 | [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat)](/LICENSE) 56 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ng-site-demo", 3 | "version": "0.0.1", 4 | "license": "MIT", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/mzuccaroli/universal-multisite-multilanguage" 8 | }, 9 | "contributors": [ 10 | "Marco Zuccaroli " 11 | ], 12 | "scripts": { 13 | "ng": "ng", 14 | "start:site1": "ng serve --project='ng-site1-demo' --configuration=dev", 15 | "start:site2": "ng serve --project='ng-site2-demo' --configuration=dev", 16 | "build": "ng build", 17 | "lint": "ng lint ng-site1-demo", 18 | "build:site1:client-and-server-bundles": "npm run build:site1:language-bundles && ng run ng-site1-demo:server:production", 19 | "build:site2:client-and-server-bundles": "npm run build:site2:language-bundles && ng run ng-site1-demo:server:production", 20 | "build:ssr": "npm run build:site1:client-and-server-bundles && npm run build:site2:client-and-server-bundles && npm run compile:server", 21 | "compile:server": "tsc -p server.tsconfig.json", 22 | "generate:prerender": "cd dist && node prerender", 23 | "serve:prerender": "cd dist/browser && http-server", 24 | "serve:ssr": "node dist/server", 25 | "build:site1:language-bundles": "ng run ng-site1-demo:build:production-it && ng run ng-site1-demo:build:production-en && ng run ng-site1-demo:build:production-fr", 26 | "build:site2:language-bundles": "ng run ng-site2-demo:build:production-it && ng run ng-site2-demo:build:production-en && ng run ng-site2-demo:build:production-fr", 27 | "extract-i18n": "ng xi18n --output-path locale && node ./node_modules/ngx-i18nsupport/dist/xliffmerge/xliffmerge" 28 | }, 29 | "pre-commit": [], 30 | "private": true, 31 | "dependencies": { 32 | "@angular/animations": "7.1.4", 33 | "@angular/common": "7.1.4", 34 | "@angular/compiler": "7.1.4", 35 | "@angular/core": "7.1.4", 36 | "@angular/forms": "7.1.4", 37 | "@angular/http": "7.1.4", 38 | "@angular/platform-browser": "7.1.4", 39 | "@angular/platform-browser-dynamic": "7.1.4", 40 | "@angular/platform-server": "7.1.4", 41 | "@angular/router": "7.1.4", 42 | "@nguniversal/common": "^6.0.0", 43 | "@nguniversal/express-engine": "^6.0.0", 44 | "@nguniversal/module-map-ngfactory-loader": "^6.0.0", 45 | "core-js": "^2.4.1", 46 | "express": "^4.15.2", 47 | "ngx-i18nsupport": "^0.17.1", 48 | "reflect-metadata": "^0.1.10", 49 | "rxjs": "6.3.3", 50 | "zone.js": "^0.8.26" 51 | }, 52 | "devDependencies": { 53 | "@angular-devkit/build-angular": "0.11.4", 54 | "@angular/cli": "7.1.4", 55 | "@angular/compiler-cli": "7.1.4", 56 | "@angular/language-service": "7.1.4", 57 | "@types/node": "^8.0.30", 58 | "codelyzer": "^4.0.2", 59 | "http-server": "^0.10.0", 60 | "pre-commit": "^1.2.2", 61 | "ts-loader": "^4.2.0", 62 | "tslint": "^5.7.0", 63 | "typescript": "3.1.6" 64 | }, 65 | "xliffmergeOptions": { 66 | "srcDir": "src/locale", 67 | "genDir": "src/locale", 68 | "i18nFile": "messages.xlf", 69 | "i18nBaseFile": "messages", 70 | "i18nFormat": "xlf", 71 | "encoding": "UTF-8", 72 | "defaultLanguage": "it", 73 | "languages": [ 74 | "it", 75 | "en", 76 | "fr" 77 | ], 78 | "verbose": true 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "node_modules/codelyzer" 4 | ], 5 | "rules": { 6 | "arrow-return-shorthand": true, 7 | "callable-types": true, 8 | "class-name": true, 9 | "comment-format": [ 10 | true, 11 | "check-space" 12 | ], 13 | "curly": true, 14 | "deprecation": { 15 | "severity": "warn" 16 | }, 17 | "eofline": true, 18 | "forin": true, 19 | "import-blacklist": [ 20 | true, 21 | "rxjs/Rx", 22 | "rxjs/operators" 23 | ], 24 | "import-spacing": true, 25 | "indent": [ 26 | true, 27 | "spaces" 28 | ], 29 | "interface-over-type-literal": true, 30 | "label-position": true, 31 | "max-line-length": [ 32 | true, 33 | 140 34 | ], 35 | "member-access": false, 36 | "member-ordering": [ 37 | true, 38 | { 39 | "order": [ 40 | "static-field", 41 | "instance-field", 42 | "static-method", 43 | "instance-method" 44 | ] 45 | } 46 | ], 47 | "no-arg": true, 48 | "no-bitwise": true, 49 | "no-console": [ 50 | true, 51 | "debug", 52 | "info", 53 | "time", 54 | "timeEnd", 55 | "trace" 56 | ], 57 | "no-construct": true, 58 | "no-debugger": true, 59 | "no-duplicate-super": true, 60 | "no-empty": false, 61 | "no-empty-interface": true, 62 | "no-eval": true, 63 | "no-inferrable-types": [ 64 | true, 65 | "ignore-params" 66 | ], 67 | "no-misused-new": true, 68 | "no-non-null-assertion": true, 69 | "no-shadowed-variable": true, 70 | "no-string-literal": false, 71 | "no-string-throw": true, 72 | "no-switch-case-fall-through": true, 73 | "no-trailing-whitespace": true, 74 | "no-unnecessary-initializer": true, 75 | "no-unused-expression": true, 76 | "no-use-before-declare": true, 77 | "no-var-keyword": true, 78 | "object-literal-sort-keys": false, 79 | "one-line": [ 80 | true, 81 | "check-open-brace", 82 | "check-catch", 83 | "check-else", 84 | "check-whitespace" 85 | ], 86 | "prefer-const": true, 87 | "quotemark": [ 88 | true, 89 | "single" 90 | ], 91 | "radix": true, 92 | "semicolon": [ 93 | true, 94 | "always" 95 | ], 96 | "triple-equals": [ 97 | true, 98 | "allow-null-check" 99 | ], 100 | "typedef-whitespace": [ 101 | true, 102 | { 103 | "call-signature": "nospace", 104 | "index-signature": "nospace", 105 | "parameter": "nospace", 106 | "property-declaration": "nospace", 107 | "variable-declaration": "nospace" 108 | } 109 | ], 110 | "typeof-compare": true, 111 | "unified-signatures": true, 112 | "variable-name": false, 113 | "whitespace": [ 114 | true, 115 | "check-branch", 116 | "check-decl", 117 | "check-operator", 118 | "check-separator", 119 | "check-type" 120 | ], 121 | "directive-selector": [ 122 | true, 123 | "attribute", 124 | "app", 125 | "camelCase" 126 | ], 127 | "component-selector": [ 128 | true, 129 | "element", 130 | "app", 131 | "kebab-case" 132 | ], 133 | "no-output-on-prefix": true, 134 | "use-input-property-decorator": true, 135 | "use-output-property-decorator": true, 136 | "use-host-property-decorator": true, 137 | "no-input-rename": true, 138 | "no-output-rename": true, 139 | "use-life-cycle-interface": true, 140 | "use-pipe-transform-interface": true, 141 | "component-class-suffix": true, 142 | "directive-class-suffix": true 143 | } 144 | } -------------------------------------------------------------------------------- /server.ts: -------------------------------------------------------------------------------- 1 | import 'zone.js/dist/zone-node'; 2 | import 'reflect-metadata'; 3 | import {enableProdMode} from '@angular/core'; 4 | // Express Engine 5 | import {ngExpressEngine} from '@nguniversal/express-engine'; 6 | // Import module map for lazy loading 7 | import {provideModuleMap} from '@nguniversal/module-map-ngfactory-loader'; 8 | 9 | import * as express from 'express'; 10 | import {join} from 'path'; 11 | 12 | // Faster server renders w/ Prod mode (dev mode never needed) 13 | enableProdMode(); 14 | 15 | 16 | const DIST_FOLDER = join(process.cwd(), 'dist'); 17 | 18 | 19 | const servers = [ 20 | { 21 | name: 'site1', 22 | server: express(), 23 | port: 4000, 24 | supportedLocales: ['it', 'en', 'fr'], 25 | folder: join(DIST_FOLDER, 'site1') 26 | }, 27 | { 28 | name: 'site2', 29 | server: express(), 30 | port: 4001, 31 | supportedLocales: ['it', 'en', 'es'], 32 | folder: join(DIST_FOLDER, 'site2') 33 | } 34 | ]; 35 | 36 | 37 | // ---------------------------------- 38 | 39 | // Express server 40 | // const app = express(); 41 | 42 | // const PORT = process.env.PORT || 4000; 43 | 44 | // * NOTE :: leave this as require() since this file is built Dynamically from webpack 45 | const {AppServerModuleNgFactory, LAZY_MODULE_MAP} = require('./server/main'); 46 | 47 | // Our Universal express-engine (found @ https://github.com/angular/universal/tree/master/modules/express-engine) 48 | // app.engine('html', ngExpressEngine({ 49 | // bootstrap: AppServerModuleNgFactory, 50 | // providers: [ 51 | // provideModuleMap(LAZY_MODULE_MAP), 52 | // ] 53 | // })); 54 | // 55 | // app.set('view engine', 'html'); 56 | // app.set('views', join(DIST_FOLDER, 'site1')); 57 | 58 | // Server static files from /browser 59 | // app.get('*.*', express.static(join(DIST_FOLDER, 'site1'), { 60 | // maxAge: '1y' 61 | // })); 62 | 63 | // All regular routes use the Universal engine 64 | // app.get('*', (req, res) => { 65 | // // this is for i18n 66 | // const supportedLocales = ['it', 'en', 'es', 'fr']; 67 | // const defaultLocale = 'en'; 68 | // const matches = req.url.match(/^\/([a-z]{2}(?:-[A-Z]{2})?)\//); 69 | // 70 | // // check if the requested url has a correct format '/locale' and matches any of the supportedLocales 71 | // const locale = (matches && supportedLocales.indexOf(matches[1]) !== -1) ? matches[1] : defaultLocale; 72 | // 73 | // 74 | // let ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress; 75 | // if (ip.substr(0, 7) === '::ffff:') { 76 | // ip = ip.substr(7); 77 | // } 78 | // 79 | // res.render(`${locale}/index`, { 80 | // req, 81 | // providers: [ 82 | // {provide: 'language', useFactory: () => locale, deps: []}, 83 | // {provide: 'ip', useFactory: () => ip, deps: []} 84 | // ] 85 | // }); 86 | // }); 87 | 88 | // Start up the Node server 89 | // app.listen(PORT, () => { 90 | // console.log(`Node Express server listening on http://localhost:${PORT}`); 91 | // }); 92 | // ---------------------------------- 93 | 94 | servers.map(app => { 95 | 96 | // Our Universal express-engine (found @ https://github.com/angular/universal/tree/master/modules/express-engine) 97 | app.server.engine('html', ngExpressEngine({ 98 | bootstrap: AppServerModuleNgFactory, 99 | providers: [ 100 | provideModuleMap(LAZY_MODULE_MAP), 101 | ] 102 | })); 103 | 104 | app.server.set('view engine', 'html'); 105 | app.server.set('views', app.folder); 106 | 107 | // Server static files from 108 | app.server.get('*.*', express.static(app.folder, {maxAge: '1y'})); 109 | 110 | app.server.get('*', (req, res) => { 111 | // this is for i18n 112 | const defaultLocale = 'en'; 113 | const matches = req.url.match(/^\/([a-z]{2}(?:-[A-Z]{2})?)\//); 114 | 115 | // check if the requested url has a correct format '/locale' and matches any of the supportedLocales 116 | const locale = (matches && app.supportedLocales.indexOf(matches[1]) !== -1) ? matches[1] : defaultLocale; 117 | 118 | 119 | let ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress; 120 | if (ip.substr(0, 7) === '::ffff:') { 121 | ip = ip.substr(7); 122 | } 123 | 124 | res.render(`${locale}/index`, { 125 | req, 126 | providers: [ 127 | {provide: 'language', useFactory: () => locale, deps: []}, 128 | {provide: 'ip', useFactory: () => ip, deps: []} 129 | ] 130 | }); 131 | }); 132 | 133 | 134 | 135 | // Start up the Node server 136 | app.server.listen(app.port, () => { 137 | console.log(`Node Express server for ${app.name} listening on http://localhost:${app.port}`); 138 | }); 139 | }); 140 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "ng-site1-demo": { 7 | "root": "", 8 | "projectType": "application", 9 | "architect": { 10 | "build": { 11 | "builder": "@angular-devkit/build-angular:browser", 12 | "options": { 13 | "outputPath": "dist/site1", 14 | "index": "src/index.html", 15 | "main": "src/main.ts", 16 | "tsConfig": "src/tsconfig.app.json", 17 | "polyfills": "src/polyfills.ts", 18 | "assets": [ 19 | { 20 | "glob": "favicon.ico", 21 | "input": "src/assets/themes/site1/images/", 22 | "output": "/" 23 | }, 24 | { 25 | "glob": "**/*", 26 | "input": "src/assets/images", 27 | "output": "/assets/images" 28 | }, 29 | { 30 | "glob": "**/*", 31 | "input": "src/assets/themes/site1/images/", 32 | "output": "/assets/images" 33 | } 34 | ], 35 | "styles": [ 36 | "src/assets/scss/main.scss", 37 | "src/assets/themes/site1/scss/theme.scss" 38 | ], 39 | "scripts": [] 40 | }, 41 | "configurations": { 42 | "production-it": { 43 | "optimization": true, 44 | "outputHashing": "all", 45 | "outputPath": "dist/site1/it/", 46 | "sourceMap": false, 47 | "extractCss": true, 48 | "namedChunks": false, 49 | "aot": true, 50 | "extractLicenses": true, 51 | "vendorChunk": false, 52 | "buildOptimizer": true, 53 | "fileReplacements": [ 54 | { 55 | "replace": "src/environments/environment.ts", 56 | "with": "src/environments/environment.site1.prod.ts" 57 | } 58 | ], 59 | "baseHref": "/it/", 60 | "i18nFile": "src/locale/messages.it.xlf", 61 | "i18nFormat": "xlf", 62 | "i18nLocale": "it", 63 | "i18nMissingTranslation": "error" 64 | }, 65 | "production-en": { 66 | "optimization": true, 67 | "outputHashing": "all", 68 | "outputPath": "dist/site1/en/", 69 | "sourceMap": false, 70 | "extractCss": true, 71 | "namedChunks": false, 72 | "aot": true, 73 | "extractLicenses": true, 74 | "vendorChunk": false, 75 | "buildOptimizer": true, 76 | "fileReplacements": [ 77 | { 78 | "replace": "src/environments/environment.ts", 79 | "with": "src/environments/environment.site1.prod.ts" 80 | } 81 | ], 82 | "baseHref": "/en/", 83 | "i18nFile": "src/locale/messages.en.xlf", 84 | "i18nFormat": "xlf", 85 | "i18nLocale": "en", 86 | "i18nMissingTranslation": "error" 87 | }, 88 | "production-fr": { 89 | "optimization": true, 90 | "outputHashing": "all", 91 | "outputPath": "dist/site1/fr/", 92 | "sourceMap": false, 93 | "extractCss": true, 94 | "namedChunks": false, 95 | "aot": true, 96 | "extractLicenses": true, 97 | "vendorChunk": false, 98 | "buildOptimizer": true, 99 | "fileReplacements": [ 100 | { 101 | "replace": "src/environments/environment.ts", 102 | "with": "src/environments/environment.site1.prod.ts" 103 | } 104 | ], 105 | "baseHref": "/fr/", 106 | "i18nFile": "src/locale/messages.fr.xlf", 107 | "i18nFormat": "xlf", 108 | "i18nLocale": "fr", 109 | "i18nMissingTranslation": "error" 110 | }, 111 | "dev": { 112 | "fileReplacements": [ 113 | { 114 | "replace": "src/environments/environment.ts", 115 | "with": "src/environments/environment.site1.dev.ts" 116 | } 117 | ] 118 | } 119 | } 120 | }, 121 | "serve": { 122 | "builder": "@angular-devkit/build-angular:dev-server", 123 | "options": { 124 | "browserTarget": "ng-site1-demo:build" 125 | }, 126 | "configurations": { 127 | "production": { 128 | "browserTarget": "ng-site1-demo:build:production-it" 129 | }, 130 | "dev": { 131 | "browserTarget": "ng-site1-demo:build:dev" 132 | } 133 | } 134 | }, 135 | "extract-i18n": { 136 | "builder": "@angular-devkit/build-angular:extract-i18n", 137 | "options": { 138 | "browserTarget": "ng-site1-demo:build" 139 | } 140 | }, 141 | "test": { 142 | "builder": "@angular-devkit/build-angular:karma", 143 | "options": { 144 | "main": "src/test.ts", 145 | "karmaConfig": "./karma.conf.js", 146 | "polyfills": "src/polyfills.ts", 147 | "tsConfig": "src/tsconfig.spec.json", 148 | "scripts": [], 149 | "styles": [ 150 | "src/styles.css" 151 | ], 152 | "assets": [ 153 | { 154 | "glob": "**/*", 155 | "input": "src/assets", 156 | "output": "/assets" 157 | }, 158 | { 159 | "glob": "favicon.ico", 160 | "input": "src", 161 | "output": "/" 162 | } 163 | ] 164 | } 165 | }, 166 | "lint": { 167 | "builder": "@angular-devkit/build-angular:tslint", 168 | "options": { 169 | "tsConfig": [ 170 | "src/tsconfig.app.json" 171 | ], 172 | "exclude": [ 173 | "**/node_modules/**" 174 | ] 175 | } 176 | }, 177 | "server": { 178 | "builder": "@angular-devkit/build-angular:server", 179 | "options": { 180 | "outputPath": "dist/server", 181 | "main": "src/main.server.ts", 182 | "tsConfig": "src/tsconfig.server.json" 183 | }, 184 | "configurations": { 185 | "production": { 186 | "fileReplacements": [ 187 | { 188 | "replace": "src/environments/environment.ts", 189 | "with": "src/environments/environment.site1.prod.ts" 190 | } 191 | ] 192 | } 193 | } 194 | } 195 | } 196 | }, 197 | "ng-site2-demo": { 198 | "root": "", 199 | "projectType": "application", 200 | "architect": { 201 | "build": { 202 | "builder": "@angular-devkit/build-angular:browser", 203 | "options": { 204 | "outputPath": "dist/site2", 205 | "index": "src/index.html", 206 | "main": "src/main.ts", 207 | "tsConfig": "src/tsconfig.app.json", 208 | "polyfills": "src/polyfills.ts", 209 | "assets": [ 210 | { 211 | "glob": "favicon.ico", 212 | "input": "src/assets/themes/site2/images/", 213 | "output": "/" 214 | }, 215 | { 216 | "glob": "**/*", 217 | "input": "src/assets/images", 218 | "output": "/assets/images" 219 | }, 220 | { 221 | "glob": "**/*", 222 | "input": "src/assets/themes/site2/images/", 223 | "output": "/assets/images" 224 | } 225 | ], 226 | "styles": [ 227 | "src/assets/scss/main.scss", 228 | "src/assets/themes/site2/scss/theme.scss" 229 | ], 230 | "scripts": [] 231 | }, 232 | "configurations": { 233 | "production-it": { 234 | "optimization": true, 235 | "outputHashing": "all", 236 | "outputPath": "dist/site2/it/", 237 | "sourceMap": false, 238 | "extractCss": true, 239 | "namedChunks": false, 240 | "aot": true, 241 | "extractLicenses": true, 242 | "vendorChunk": false, 243 | "buildOptimizer": true, 244 | "fileReplacements": [ 245 | { 246 | "replace": "src/environments/environment.ts", 247 | "with": "src/environments/environment.site2.prod.ts" 248 | } 249 | ], 250 | "baseHref": "/it/", 251 | "i18nFile": "src/locale/messages.it.xlf", 252 | "i18nFormat": "xlf", 253 | "i18nLocale": "it", 254 | "i18nMissingTranslation": "error" 255 | }, 256 | "production-en": { 257 | "optimization": true, 258 | "outputHashing": "all", 259 | "outputPath": "dist/site2/en/", 260 | "sourceMap": false, 261 | "extractCss": true, 262 | "namedChunks": false, 263 | "aot": true, 264 | "extractLicenses": true, 265 | "vendorChunk": false, 266 | "buildOptimizer": true, 267 | "fileReplacements": [ 268 | { 269 | "replace": "src/environments/environment.ts", 270 | "with": "src/environments/environment.site2.prod.ts" 271 | } 272 | ], 273 | "baseHref": "/en/", 274 | "i18nFile": "src/locale/messages.en.xlf", 275 | "i18nFormat": "xlf", 276 | "i18nLocale": "en", 277 | "i18nMissingTranslation": "error" 278 | }, 279 | "production-fr": { 280 | "optimization": true, 281 | "outputHashing": "all", 282 | "outputPath": "dist/site2/fr/", 283 | "sourceMap": false, 284 | "extractCss": true, 285 | "namedChunks": false, 286 | "aot": true, 287 | "extractLicenses": true, 288 | "vendorChunk": false, 289 | "buildOptimizer": true, 290 | "fileReplacements": [ 291 | { 292 | "replace": "src/environments/environment.ts", 293 | "with": "src/environments/environment.site2.prod.ts" 294 | } 295 | ], 296 | "baseHref": "/fr/", 297 | "i18nFile": "src/locale/messages.fr.xlf", 298 | "i18nFormat": "xlf", 299 | "i18nLocale": "en", 300 | "i18nMissingTranslation": "error" 301 | }, 302 | "dev": { 303 | "fileReplacements": [ 304 | { 305 | "replace": "src/environments/environment.ts", 306 | "with": "src/environments/environment.site2.dev.ts" 307 | } 308 | ] 309 | } 310 | } 311 | }, 312 | "serve": { 313 | "builder": "@angular-devkit/build-angular:dev-server", 314 | "options": { 315 | "browserTarget": "ng-site2-demo:build" 316 | }, 317 | "configurations": { 318 | "production": { 319 | "browserTarget": "ng-site2-demo:build:production-en" 320 | }, 321 | "dev": { 322 | "browserTarget": "ng-site2-demo:build:dev" 323 | } 324 | } 325 | }, 326 | "extract-i18n": { 327 | "builder": "@angular-devkit/build-angular:extract-i18n", 328 | "options": { 329 | "browserTarget": "ng-site1-demo:build" 330 | } 331 | }, 332 | "test": { 333 | "builder": "@angular-devkit/build-angular:karma", 334 | "options": { 335 | "main": "src/test.ts", 336 | "karmaConfig": "./karma.conf.js", 337 | "polyfills": "src/polyfills.ts", 338 | "tsConfig": "src/tsconfig.spec.json", 339 | "scripts": [], 340 | "styles": [ 341 | "src/styles.css" 342 | ], 343 | "assets": [ 344 | { 345 | "glob": "**/*", 346 | "input": "src/assets", 347 | "output": "/assets" 348 | }, 349 | { 350 | "glob": "favicon.ico", 351 | "input": "src", 352 | "output": "/" 353 | } 354 | ] 355 | } 356 | }, 357 | "lint": { 358 | "builder": "@angular-devkit/build-angular:tslint", 359 | "options": { 360 | "tsConfig": [ 361 | "src/tsconfig.app.json" 362 | ], 363 | "exclude": [ 364 | "**/node_modules/**" 365 | ] 366 | } 367 | }, 368 | "server": { 369 | "builder": "@angular-devkit/build-angular:server", 370 | "options": { 371 | "outputPath": "dist/server", 372 | "main": "src/main.server.ts", 373 | "tsConfig": "src/tsconfig.server.json" 374 | }, 375 | "configurations": { 376 | "production": { 377 | "fileReplacements": [ 378 | { 379 | "replace": "src/environments/environment.ts", 380 | "with": "src/environments/environment.site2.prod.ts" 381 | } 382 | ] 383 | } 384 | } 385 | } 386 | } 387 | }, 388 | "ng-site1-demo-e2e": { 389 | "root": "", 390 | "projectType": "application", 391 | "cli": {}, 392 | "schematics": {}, 393 | "architect": { 394 | "e2e": { 395 | "builder": "@angular-devkit/build-angular:protractor", 396 | "options": { 397 | "protractorConfig": "./protractor.conf.js", 398 | "devServerTarget": "ng-site1-demo:serve" 399 | } 400 | }, 401 | "lint": { 402 | "builder": "@angular-devkit/build-angular:tslint", 403 | "options": { 404 | "tsConfig": [], 405 | "exclude": [ 406 | "**/node_modules/**" 407 | ] 408 | } 409 | } 410 | } 411 | } 412 | }, 413 | "cli": {}, 414 | "schematics": { 415 | "@schematics/angular:class": { 416 | "spec": false 417 | }, 418 | "@schematics/angular:component": { 419 | "spec": false, 420 | "inlineStyle": true, 421 | "inlineTemplate": true, 422 | "prefix": "app", 423 | "styleext": "css" 424 | }, 425 | "@schematics/angular:directive": { 426 | "spec": false, 427 | "prefix": "app" 428 | }, 429 | "@schematics/angular:guard": { 430 | "spec": false 431 | }, 432 | "@schematics/angular:module": { 433 | "spec": false 434 | }, 435 | "@schematics/angular:pipe": { 436 | "spec": false 437 | }, 438 | "@schematics/angular:service": { 439 | "spec": false 440 | } 441 | } 442 | } 443 | --------------------------------------------------------------------------------