I have updated the Angular from version 14 to version 17 in order to take advantage of the boxed SSR solution. The update was successful, but when ng serve starts, the application content does not load, as if SSR does not work. There is practically no information on the Internet...
The application is loaded on the client. Empty app-root when viewing the page code:
<body>
<app-root></app-root>
<script src="runtime.js" defer></script>
<script src="polyfills.js" type="module"></script>
<script src="styles.js" defer></script>
<script src="vendor.js" type="module"></script>
<script src="main.js" type="module"></script>
</body>
Here are mine...
angular.json:
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"client": {
"projectType": "application",
"schematics": {
"@schematics/angular:component": {
"style": "scss"
}
},
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.app.json",
"assets": [
"src/favicon.ico",
"src/assets",
"src/robots.txt",
"src/sitemap.xml",
"src/yandex_71526e85ef9f15bd.html"
],
"styles": [
"node_modules/ngx-owl-carousel-o/lib/styles/prebuilt-themes/owl.carousel.min.css",
"node_modules/ngx-owl-carousel-o/lib/styles/prebuilt-themes/owl.theme.default.min.css",
"./node_modules/ngx-lightbox/lightbox.css",
"./node_modules/@angular/material/prebuilt-themes/indigo-pink.css",
"src/styles.scss"
],
"scripts": [],
"vendorChunk": true,
"extractLicenses": false,
"buildOptimizer": false,
"sourceMap": true,
"optimization": false,
"namedChunks": true
},
"configurations": {
"production": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"namedChunks": false,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true,
"budgets": [
{
"type": "initial",
"maximumWarning": "2mb",
"maximumError": "5mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "2mb",
"maximumError": "5mb"
}
]
},
"preprod": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.preprod.ts"
}
],
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"namedChunks": false,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true,
"budgets": [
{
"type": "initial",
"maximumWarning": "2mb",
"maximumError": "5mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "2mb",
"maximumError": "5mb"
}
]
},
"local": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.local.ts"
}
],
"optimization": false,
"sourceMap": true,
"namedChunks": false,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": false,
"budgets": [
{
"type": "initial",
"maximumWarning": "2mb",
"maximumError": "5mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "2mb",
"maximumError": "5mb"
}
]
}
}
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"buildTarget": "client:build"
},
"configurations": {
"production": {
"buildTarget": "client:build:production"
},
"preprod": {
"buildTarget": "client:build:preprod"
},
"local": {
"buildTarget": "client:build:local"
}
}
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"buildTarget": "client:build"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "src/test.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.spec.json",
"karmaConfig": "karma.conf.js",
"assets": [
"src/favicon.ico",
"src/assets",
"src/robots.txt",
"src/sitemap.xml",
"src/yandex_71526e85ef9f15bd.html"
],
"styles": [
"node_modules/ngx-owl-carousel-o/lib/styles/prebuilt-themes/owl.carousel.min.css",
"node_modules/ngx-owl-carousel-o/lib/styles/prebuilt-themes/owl.theme.default.min.css",
"./node_modules/@angular/material/prebuilt-themes/deeppurple-amber.css",
"src/styles.scss"
],
"scripts": []
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"tsconfig.app.json",
"tsconfig.spec.json",
"e2e/tsconfig.json"
],
"exclude": [
"**/node_modules/**"
]
}
},
"e2e": {
"builder": "@angular-devkit/build-angular:protractor",
"options": {
"protractorConfig": "e2e/protractor.conf.js",
"devServerTarget": "client:serve"
},
"configurations": {
"production": {
"devServerTarget": "client:serve:production"
}
}
},
"server": {
"builder": "@angular-devkit/build-angular:server",
"options": {
"outputPath": "dist/client/server",
"main": "server.ts",
"tsConfig": "tsconfig.server.json",
"buildOptimizer": false,
"optimization": false,
"sourceMap": true,
"extractLicenses": false,
"vendorChunk": true
},
"configurations": {
"production": {
"buildOptimizer": true,
"outputHashing": "media",
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
"optimization": true,
"sourceMap": false,
"extractLicenses": true,
"vendorChunk": false
},
"preprod": {
"buildOptimizer": true,
"outputHashing": "media",
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.preprod.ts"
}
],
"optimization": true,
"sourceMap": false,
"extractLicenses": true,
"vendorChunk": false
},
"local": {
"buildOptimizer": false,
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.local.ts"
}
],
"optimization": false,
"sourceMap": true,
"extractLicenses": true,
"vendorChunk": false
}
},
"defaultConfiguration": "production"
},
"serve-ssr": {
"builder": "@angular-devkit/build-angular:ssr-dev-server",
"configurations": {
"development": {
"browserTarget": "client:build",
"serverTarget": "client:server"
},
"production": {
"browserTarget": "client:build:production",
"serverTarget": "client:server:production"
}
},
"defaultConfiguration": "development"
},
"prerender": {
"builder": "@angular-devkit/build-angular:prerender",
"options": {
"routes": [
"/"
]
},
"configurations": {
"production": {
"browserTarget": "client:build:production",
"serverTarget": "client:server:production"
},
"development": {
"browserTarget": "client:build:development",
"serverTarget": "client:server:development"
}
},
"defaultConfiguration": "production"
}
}
}
},
"cli": {
"analytics": false,
"cache": {
"enabled": false
}
}
}
package.json:
{
"name": "client",
"version": "1.1.0",
"scripts": {
"ng": "ng",
"clear": "RMDIR /Q/S node_modules && npm install",
"start": "ng serve --hmr",
"start:local": "ng serve --configuration local --hmr",
"start:preprod": "ng serve --configuration preprod --hmr",
"start:ssl": "ng serve --ssl=true",
"start-portal": "ng serve --base-href=\"/portal/\"",
"build": "ng build",
"build:preprod": "ng build --configuration preprod",
"build:prod": "ng build --configuration production --optimization=true",
"build:prod-portal": "ng build --base-href=\"/portal/\" --configuration production",
"buildToTrends": "ng build --base-href=\"/portal/\" --configuration=trends",
"test": "ng test",
"eslint": "eslint --cache --ext .js,.jsx,.ts,.tsx src",
"eslint:fix": "eslint --cache --ext .js,.jsx,.ts,.tsx src --fix",
"eslint:dump": "eslint --print-config ./.eslintrc.json",
"e2e": "ng e2e",
"dev:ssr": "ng run client:serve-ssr",
"serve:ssr": "node dist/client/server/main.js",
"build:ssr": "ng build && ng run client:server",
"prerender": "ng run client:prerender"
},
"private": true,
"dependencies": {
"@angular-material-components/datetime-picker": "^16.0.1",
"@angular/animations": "^17.1.0",
"@angular/cdk": "^16.0.1",
"@angular/cli": "^17.1.0",
"@angular/common": "^17.1.0",
"@angular/compiler": "^17.1.0",
"@angular/compiler-cli": "^17.1.0",
"@angular/core": "^17.1.0",
"@angular/forms": "^17.1.0",
"@angular/localize": "^17.1.0",
"@angular/material": "^16.0.2",
"@angular/material-moment-adapter": "^16.0.1",
"@angular/platform-browser": "^17.1.0",
"@angular/platform-browser-dynamic": "^17.1.0",
"@angular/platform-server": "^17.1.0",
"@angular/router": "^17.1.0",
"@angular/ssr": "^17.1.0",
"@auth0/angular-jwt": "^5.0.2",
"@ng-bootstrap/ng-bootstrap": "^15.1.1",
"@ngrx/store": "^16.0.1",
"@popperjs/core": "^2.11.8",
"angular8-yandex-maps": "^16.0.2",
"bootstrap": "^5.3.1",
"browser-sync": "^3.0.2",
"express": "^4.18.2",
"jszip": "^3.10.1",
"material-design": "0.0.1",
"moment": "^2.29.4",
"ng-recaptcha": "^12.0.2",
"ng2-pdf-viewer": "^10.0.0",
"ngx-cookie": "^6.0.1",
"ngx-cookie-service": "^16.0.0",
"ngx-image-cropper": "^6.3.1",
"ngx-lightbox": "^3.0.0",
"ngx-mask": "^16.2.8",
"ngx-owl-carousel-o": "^16.0.0",
"ngx-pagination": "^6.0.3",
"ngx-quill": "^16.2.1",
"ngx-toastr": "^16.2.0",
"ngx-ui-loader": "^13.0.0",
"oidc-client": "^1.11.5",
"quill": "^1.3.7",
"remove": "^0.1.5",
"rxjs": "^7.8.1",
"swiper": "8.3.2",
"tslib": "2.4.0",
"zone.js": "~0.14.3"
},
"devDependencies": {
"@angular-devkit/build-angular": "^17.1.0",
"@types/express": "^4.17.17",
"@types/jasmine": "4.0.3",
"@types/jasminewd2": "~2.0.3",
"@types/node": "^18.18.0",
"@typescript-eslint/eslint-plugin": "5.32.0",
"@typescript-eslint/parser": "5.32.0",
"browser-sync": "^3.0.0",
"codelyzer": "^6.0.0",
"eslint": "8.21.0",
"eslint-config-google": "^0.14.0",
"jasmine-core": "4.3.0",
"jasmine-spec-reporter": "7.0.0",
"jest-preset-angular": "^7.0.1",
"protractor": "~7.0.0",
"ts-node": "10.9.1",
"typescript": "5.3.3"
}
}
main.ts:
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
const platform = platformBrowserDynamic();
platform.bootstrapModule(AppModule)
.catch((err) => console.error(err));
main.server.ts
export { AppServerModule as default } from './app/app.module.server';
app.module.ts
import {DatePipe} from '@angular/common';
import {HTTP_INTERCEPTORS, HttpClientModule} from '@angular/common/http';
import {InjectionToken, NgModule} from '@angular/core';
import {MAT_DATE_FORMATS, MAT_DATE_LOCALE} from '@angular/material/core';
import {MatDialogModule} from '@angular/material/dialog';
import {MatIconModule} from '@angular/material/icon';
import {MatPaginatorIntl} from "@angular/material/paginator";
import {MatSlideToggleModule} from '@angular/material/slide-toggle';
import {MatSnackBarModule} from '@angular/material/snack-bar';
import {BrowserModule, provideClientHydration} from '@angular/platform-browser';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {UrlSerializer} from '@angular/router';
import {JwtHelperService} from '@auth0/angular-jwt';
import {NgbModule} from '@ng-bootstrap/ng-bootstrap';
import {StoreModule} from '@ngrx/store';
import {CookieModule, CookieService} from 'ngx-cookie';
import {ToastrModule} from 'ngx-toastr';
import {NgxUiLoaderHttpModule, NgxUiLoaderModule, NgxUiLoaderRouterModule} from 'ngx-ui-loader';
import {getPaginatorIntl} from "src/app/core/configs/paginatorConfig";
import {AdminAuthGuard} from 'src/app/core/guards/admin-auth-guard';
import {UnderConstructionGuard} from 'src/app/core/guards/underConstructionGuard';
import {UnderConstructionComponent} from "src/app/pages/under-construction/under-construction.component";
import {environment} from '../environments/environment';
import {AppRoutingModule} from './app-routing.module';
import {AppComponent} from './app.component';
import {ngxUiLoaderConfig} from "./core/configs/ngx-ui-loader-configuration";
import {Interceptor} from './core/services/api.interceptor';
import {AuthService} from './core/services/auth.service';
import {API_BASE_URL, Client} from './core/services/Client';
import {LowerCaseUrlSerializer} from './core/services/lower-case-url-serializer';
import {HeaderComponent} from './layouts/header/header.component';
import {HolidayWindowComponent} from './pages/holiday-window/holiday-window.component';
import {PipesModule} from "./pipes.module";
import {ReactiveFormsModule} from "@angular/forms";
import {MatButtonModule} from "@angular/material/button";
import {UserSubscriptionFilterService} from './core/services/UserSubscriptionFilterService';
import {SeoService} from "./core/services/seo/seo.service";
import {NotfoundComponent} from "./notfound/notfound.component";
import {AngularYandexMapsModule} from 'angular8-yandex-maps';
import {mapConfig} from './core/configs/map-config';
import {TorgiService} from './pages/torgi/torgi.service';
import { appConfig } from './app.config';
export const AUTH_URL = new InjectionToken<string>('AUTH_URL');
export const ORIGIN_URL = new InjectionToken<string>('ORIGIN_URL');
@NgModule({
declarations: [
AppComponent,
HeaderComponent,
HolidayWindowComponent,
UnderConstructionComponent,
NotfoundComponent
],
imports: [
AppRoutingModule,
AngularYandexMapsModule.forRoot(mapConfig),
StoreModule.forRoot({}, {}),
ToastrModule.forRoot(), // ToastrModule added
NgxUiLoaderHttpModule.forRoot({
showForeground: true,
exclude: [environment.AUTH_URL, `${environment.API_URL}/api/v1-web/UserMessage/GetUserMessageList`]
}),
NgxUiLoaderModule.forRoot(ngxUiLoaderConfig),
NgxUiLoaderRouterModule,
BrowserModule,
BrowserAnimationsModule,
HttpClientModule,
NgbModule,
MatSlideToggleModule,
MatIconModule,
MatSnackBarModule,
MatDialogModule,
BrowserAnimationsModule,
CookieModule.forRoot(),
PipesModule,
MatButtonModule,
BrowserAnimationsModule,
CookieModule.forRoot(),
PipesModule,
ReactiveFormsModule
],
providers: [
AuthService,
AdminAuthGuard,
CookieService,
JwtHelperService,
DatePipe,
UnderConstructionGuard,
SeoService,
TorgiService,
Client,
UserSubscriptionFilterService,
{provide: HTTP_INTERCEPTORS, useClass: Interceptor, multi: true},
{provide: API_BASE_URL, useValue: environment.API_URL},
{provide: AUTH_URL, useValue: environment.AUTH_URL},
{provide: ORIGIN_URL, useValue: location.origin},
{provide: UrlSerializer, useClass: LowerCaseUrlSerializer},
{provide: MAT_DATE_LOCALE, useValue: 'ru-RU'},
{provide: MAT_DATE_FORMATS, useValue: {display: {dateInput: 'DD.MM.YYYY', monthYearLabel: 'MMMM YYYY'}}},
{provide: MatPaginatorIntl, useValue: getPaginatorIntl()},
provideClientHydration(),
appConfig.providers,
],
exports: [
HeaderComponent,
],
bootstrap: [AppComponent]
})
export class AppModule {
}
app.module.server.ts:
import { NgModule } from '@angular/core';
import { ServerModule } from '@angular/platform-server';
import { AppModule } from './app.module';
import { AppComponent } from './app.component';
import { config } from './app.config.server';
@NgModule({
imports: [
AppModule,
ServerModule,
],
bootstrap: [AppComponent],
providers: [config.providers]
})
export class AppServerModule {}
app.config.server:
import { ApplicationConfig, mergeApplicationConfig } from '@angular/core';
import { provideServerRendering } from '@angular/platform-server';
import { appConfig } from './app.config';
import { provideHttpClient, withFetch } from '@angular/common/http';
const serverConfig: ApplicationConfig = {
providers: [
provideServerRendering(),
provideHttpClient(withFetch())
]
};
export const config = mergeApplicationConfig(appConfig, serverConfig);
app.config:
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideClientHydration } from '@angular/platform-browser';
import { routes } from './app-routing.module';
export const appConfig: ApplicationConfig = {
providers: [provideRouter(routes), provideClientHydration()]
};
server.ts
import 'zone.js/node';
import { APP_BASE_HREF } from '@angular/common';
import { CommonEngine } from '@angular/ssr';
import * as express from 'express';
import { existsSync } from 'node:fs';
import { join } from 'node:path';
import AppServerModule from './src/main.server';
// The Express app is exported so that it can be used by serverless Functions.
export function app(): express.Express {
const server = express();
const distFolder = join(process.cwd(), 'dist/client/browser');
const indexHtml = existsSync(join(distFolder, 'index.original.html'))
? join(distFolder, 'index.original.html')
: join(distFolder, 'index.html');
const commonEngine = new CommonEngine();
server.set('view engine', 'html');
server.set('views', distFolder);
// Example Express Rest API endpoints
// server.get('/api/**', (req, res) => { });
// Serve static files from /browser
server.get('*.*', express.static(distFolder, {
maxAge: '1y'
}));
// All regular routes use the Angular engine
server.get('*', (req, res, next) => {
const { protocol, originalUrl, baseUrl, headers } = req;
commonEngine
.render({
bootstrap: AppServerModule,
documentFilePath: indexHtml,
url: `${protocol}://${headers.host}${originalUrl}`,
publicPath: distFolder,
providers: [{ provide: APP_BASE_HREF, useValue: baseUrl }],
})
.then((html) => res.send(html))
.catch((err) => next(err));
});
return server;
}
function run(): void {
const port = process.env['PORT'] || 4000;
// Start up the Node server
const server = app();
server.listen(port, () => {
console.log(`Node Express server listening on http://localhost:${port}`);
});
}
// Webpack will replace 'require' with '__webpack_require__'
// '__non_webpack_require__' is a proxy to Node 'require'
// The below code is to ensure that the server is run only when not requiring the bundle.
declare const __non_webpack_require__: NodeRequire;
const mainModule = __non_webpack_require__.main;
const moduleFilename = mainModule && mainModule.filename || '';
if (moduleFilename === __filename || moduleFilename.includes('iisnode')) {
run();
}
export default AppServerModule;
The documentation says exactly ng server angular.dev/guide/ssr. ng dev:ssr I used too, but it doesn't work
To verify that the application is server-side rendered, run it locally with ng serve. The initial HTML request should contain application content.