Skip to content

Commit

Permalink
Add Angular SSR
Browse files Browse the repository at this point in the history
  • Loading branch information
GODrums committed Oct 30, 2024
1 parent a4d17ec commit c935598
Show file tree
Hide file tree
Showing 10 changed files with 245 additions and 168 deletions.
7 changes: 6 additions & 1 deletion webapp/angular.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,12 @@
"styles": [
"src/styles.css"
],
"scripts": []
"scripts": [],
"server": "src/main.server.ts",
"prerender": true,
"ssr": {
"entry": "server.ts"
}
},
"configurations": {
"production": {
Expand Down
249 changes: 141 additions & 108 deletions webapp/package-lock.json

Large diffs are not rendered by default.

10 changes: 8 additions & 2 deletions webapp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
"prettier:check": "prettier --check src/",
"prettier:write": "prettier --write src/",
"lint": "eslint",
"lint:fix": "eslint --fix"
"lint:fix": "eslint --fix",
"serve:ssr:webapp": "node dist/webapp/server/server.mjs"
},
"private": true,
"engines": {
Expand All @@ -28,7 +29,9 @@
"@angular/forms": "18.2.1",
"@angular/platform-browser": "18.2.1",
"@angular/platform-browser-dynamic": "18.2.1",
"@angular/platform-server": "18.2.1",
"@angular/router": "18.2.1",
"@angular/ssr": "^18.2.1",
"@ng-icons/core": "29.5.0",
"@ng-icons/lucide": "^26.3.0",
"@ng-icons/octicons": "29.5.0",
Expand Down Expand Up @@ -63,6 +66,7 @@
"clsx": "2.1.1",
"dayjs": "1.11.13",
"embla-carousel-angular": "^14.0.0",
"express": "^4.18.2",
"keycloak-js": "^26.0.0",
"lucide-angular": "0.429.0",
"ngx-scrollbar": "^13.0.1",
Expand Down Expand Up @@ -90,7 +94,9 @@
"@storybook/angular": "8.3.4",
"@storybook/blocks": "8.3.4",
"@storybook/test": "8.3.4",
"@types/express": "^4.17.17",
"@types/jasmine": "5.1.4",
"@types/node": "^18.18.0",
"@typescript-eslint/eslint-plugin": "8.2.0",
"@typescript-eslint/parser": "8.2.0",
"chromatic": "11.7.1",
Expand All @@ -106,4 +112,4 @@
"storybook": "8.3.4",
"typescript": "5.5.4"
}
}
}
60 changes: 60 additions & 0 deletions webapp/server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { APP_BASE_HREF } from '@angular/common';
import { CommonEngine } from '@angular/ssr';
import express from 'express';
import { fileURLToPath } from 'node:url';
import { dirname, join, resolve } from 'node:path';
import bootstrap 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 serverDistFolder = dirname(fileURLToPath(import.meta.url));
const browserDistFolder = resolve(serverDistFolder, '../browser');
const indexHtml = join(serverDistFolder, 'index.server.html');

const commonEngine = new CommonEngine();

server.set('view engine', 'html');
server.set('views', browserDistFolder);

// Example Express Rest API endpoints
// server.get('/api/**', (req, res) => { });
// Serve static files from /browser
server.get(
'**',
express.static(browserDistFolder, {
maxAge: '1y',
index: 'index.html'
})
);

// All regular routes use the Angular engine
server.get('**', (req, res, next) => {
const { protocol, originalUrl, baseUrl, headers } = req;

commonEngine
.render({
bootstrap,
documentFilePath: indexHtml,
url: `${protocol}://${headers.host}${originalUrl}`,
publicPath: browserDistFolder,
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'] || 4200;

// Start up the Node server
const server = app();
server.listen(port, () => {
console.log(`Node Express server listening on http://localhost:${port}`);
});
}

run();
53 changes: 1 addition & 52 deletions webapp/src/app/app.component.ts
Original file line number Diff line number Diff line change
@@ -1,71 +1,20 @@
import { Component, isDevMode } from '@angular/core';
import { AngularQueryDevtools } from '@tanstack/angular-query-devtools-experimental';
import { ActivatedRoute, Router, RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router';
import { RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router';
import { HeaderComponent } from '@app/core/header/header.component';
import { FooterComponent } from './core/footer/footer.component';
import { Title, Meta } from '@angular/platform-browser';

@Component({
selector: 'app-root',
standalone: true,
imports: [RouterOutlet, RouterLink, RouterLinkActive, AngularQueryDevtools, HeaderComponent, FooterComponent],
providers: [Meta],
templateUrl: './app.component.html',
styles: []
})
export class AppComponent {
title = 'Hephaestus';

constructor(
private titleService: Title,
private metaService: Meta,
private router: Router,
private route: ActivatedRoute
) {}

isDevMode() {
return isDevMode();
}

ngOnInit() {
this.router.events.subscribe((e) => {
console.log('router event', e);
this.updateTitle();
this.updateMeta();
});
}

// Update document title
updateTitle() {
const snapshot = this.route.snapshot;
const id = snapshot.paramMap.get('id');
const titleParam = id ?? this.titleService.getTitle();
const title = titleParam ? `${titleParam} | Hephaestus` : 'Hephaestus';
this.titleService.setTitle(title);
}

updateMeta() {
const snapshot = this.route.snapshot.firstChild;
const title = this.titleService.getTitle();
const metaTags = snapshot?.firstChild?.data['meta'];
console.log('metaTags', snapshot);
if (metaTags) {
const iconURL = 'https://github.com/ls1intum/Hephaestus/raw/refs/heads/develop/docs/images/hammer.svg';
this.metaService.updateTag({ name: 'description', content: metaTags.description });
this.metaService.updateTag({ property: 'og:type', content: 'website' });
this.metaService.updateTag({ property: 'og:title', content: title });
this.metaService.updateTag({ property: 'og:description', content: metaTags.description });
this.metaService.updateTag({ property: 'og:image', content: iconURL });
this.metaService.updateTag({ property: 'og:image:width', content: '67' });
this.metaService.updateTag({ property: 'og:image:height', content: '60' });
this.metaService.updateTag({ property: 'og:url', content: 'https://hephaestus.ase.cit.tum.de' + (snapshot.url.pop()?.path ?? '') });

// this.metaService.updateTag({ name: 'twitter:card', content: 'summary_large_image' });
this.metaService.updateTag({ name: 'twitter:title', content: title });
this.metaService.updateTag({ name: 'twitter:description', content: metaTags.description });
this.metaService.updateTag({ name: 'twitter:image', content: iconURL });
this.metaService.updateTag({ name: 'twitter:image:width', content: '67' });
this.metaService.updateTag({ name: 'twitter:image:height', content: '60' });
}
}
}
11 changes: 11 additions & 0 deletions webapp/src/app/app.config.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { mergeApplicationConfig, ApplicationConfig } from '@angular/core';
import { provideServerRendering } from '@angular/platform-server';
import { appConfig } from './app.config';

const serverConfig: ApplicationConfig = {
providers: [

Check failure on line 6 in webapp/src/app/app.config.server.ts

View workflow job for this annotation

GitHub Actions / Code Quality Checks

Replace `⏎····provideServerRendering()⏎··` with `provideServerRendering()`
provideServerRendering()
]
};

export const config = mergeApplicationConfig(appConfig, serverConfig);
6 changes: 4 additions & 2 deletions webapp/src/app/app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { routes } from 'app/app.routes';
import { AnalyticsService } from './analytics.service';
import { securityInterceptor } from './core/security/security-interceptor';
import { TemplatePageTitleStrategy } from './core/TemplatePageTitleStrategy';
import { provideClientHydration, withHttpTransferCacheOptions } from '@angular/platform-browser';

Check failure on line 12 in webapp/src/app/app.config.ts

View workflow job for this annotation

GitHub Actions / Code Quality Checks

'withHttpTransferCacheOptions' is defined but never used

function initializeAnalytics(analyticsService: AnalyticsService): () => void {
return () => {
Expand All @@ -23,8 +24,9 @@ export const appConfig: ApplicationConfig = {
provideAngularQuery(new QueryClient()),
provideHttpClient(withInterceptors([securityInterceptor])),
provideAnimationsAsync(),
provideClientHydration(),
{ provide: BASE_PATH, useValue: environment.serverUrl },
{ provide: APP_INITIALIZER, useFactory: initializeAnalytics, multi: true, deps: [AnalyticsService] }
// { provide: TitleStrategy, useClass: TemplatePageTitleStrategy }
{ provide: APP_INITIALIZER, useFactory: initializeAnalytics, multi: true, deps: [AnalyticsService] },
{ provide: TitleStrategy, useClass: TemplatePageTitleStrategy }
]
};
2 changes: 1 addition & 1 deletion webapp/src/app/core/header/header.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
</a>
<app-request-feature class="hidden sm:inline-block" />
<app-request-feature class="sm:hidden" iconOnly />
<app-theme-switcher />
<!-- <app-theme-switcher /> -->
@if (signedIn()) {
<button [brnMenuTriggerFor]="usermenu" class="ml-2">
<hlm-avatar>
Expand Down
7 changes: 7 additions & 0 deletions webapp/src/main.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { config } from './app/app.config.server';

const bootstrap = () => bootstrapApplication(AppComponent, config);

export default bootstrap;
8 changes: 6 additions & 2 deletions webapp/tsconfig.app.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,14 @@
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/app",
"types": []
"types": [
"node"
]
},
"files": [
"src/main.ts"
"src/main.ts",
"src/main.server.ts",
"server.ts"
],
"include": [
"src/**/*.d.ts"
Expand Down

0 comments on commit c935598

Please sign in to comment.