# BizMatch SSR - Technische Dokumentation ## Was ist Server-Side Rendering (SSR)? Server-Side Rendering bedeutet, dass die Angular-Anwendung nicht nur im Browser, sondern auch auf dem Server läuft und HTML vorab generiert. --- ## Unterschied: SPA vs. SSR vs. Prerendering ### 1. Single Page Application (SPA) - OHNE SSR **Ablauf:** ``` Browser → lädt index.html → index.html enthält nur → lädt JavaScript-Bundles → JavaScript rendert die Seite ``` **HTML-Response:** ```html BizMatch ``` **Nachteile:** - ❌ Suchmaschinen sehen leeren Content - ❌ Langsamer "First Contentful Paint" - ❌ Schlechtes SEO - ❌ Kein Social-Media-Preview (Open Graph) --- ### 2. Server-Side Rendering (SSR) **Ablauf:** ``` Browser → fragt Server nach /business/123 → Server rendert Angular-App mit Daten → Server sendet vollständiges HTML → Browser zeigt sofort Inhalt → JavaScript lädt im Hintergrund → Anwendung wird "hydrated" (interaktiv) ``` **HTML-Response:** ```html Restaurant "Zum Löwen" | BizMatch

Restaurant "Zum Löwen"

Traditionelles deutsches Restaurant...

``` **Vorteile:** - ✅ Suchmaschinen sehen vollständigen Inhalt - ✅ Schneller First Contentful Paint - ✅ Besseres SEO - ✅ Social-Media-Previews funktionieren **Nachteile:** - ⚠️ Komplexere Konfiguration - ⚠️ Server-Ressourcen erforderlich - ⚠️ Code muss browser- und server-kompatibel sein --- ### 3. Prerendering (Static Site Generation) **Ablauf:** ``` Build-Zeit → Rendert ALLE Seiten zu statischen HTML-Dateien → /business/123.html, /business/456.html, etc. → HTML-Dateien werden auf CDN deployed ``` **Unterschied zu SSR:** - Prerendering: HTML wird **zur Build-Zeit** generiert - SSR: HTML wird **zur Request-Zeit** generiert **BizMatch nutzt SSR, NICHT Prerendering**, weil: - Listings dynamisch sind (neue Einträge täglich) - Benutzerdaten personalisiert sind - Suche und Filter zur Laufzeit erfolgen --- ## Wie funktioniert SSR in BizMatch? ### Architektur-Überblick ``` ┌─────────────────────────────────────────────────────────────┐ │ Browser Request │ │ GET /business/restaurant-123 │ └────────────────────────────┬────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ Express Server │ │ (server.ts:30-41) │ ├─────────────────────────────────────────────────────────────┤ │ 1. Empfängt Request │ │ 2. Ruft AngularNodeAppEngine auf │ │ 3. Rendert Angular-Komponente serverseitig │ │ 4. Sendet HTML zurück │ └────────────────────────────┬────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ AngularNodeAppEngine │ │ (@angular/ssr/node) │ ├─────────────────────────────────────────────────────────────┤ │ 1. Lädt main.server.ts │ │ 2. Bootstrapped Angular in Node.js │ │ 3. Führt Routing aus (/business/restaurant-123) │ │ 4. Rendert Component-Tree zu HTML-String │ │ 5. Injiziert Meta-Tags, Titel │ └────────────────────────────┬────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ Angular Application │ │ (Browser-Code im Server) │ ├─────────────────────────────────────────────────────────────┤ │ • Komponenten werden ausgeführt │ │ • API-Calls werden gemacht (TransferState) │ │ • DOM wird SIMULIERT (ssr-dom-polyfill.ts) │ │ • HTML-Output wird generiert │ └─────────────────────────────────────────────────────────────┘ ``` --- ## Wichtige Dateien und ihre Rolle ### 1. `server.ts` - Express Server ```typescript const angularApp = new AngularNodeAppEngine(); server.get('*', (req, res, next) => { angularApp.handle(req) // ← Rendert Angular serverseitig .then((response) => { if (response) { writeResponseToNodeResponse(response, res); } }); }); ``` **Rolle:** - HTTP-Server (Express) - Nimmt Requests entgegen - Delegiert an Angular SSR Engine - Sendet gerenderte HTML-Responses zurück --- ### 2. `src/main.server.ts` - Server Entry Point ```typescript import './ssr-dom-polyfill'; // ← WICHTIG: DOM-Mocks laden 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; ``` **Rolle:** - Entry Point für SSR - Lädt DOM-Polyfills **VOR** allen anderen Imports - Bootstrapped Angular im Server-Kontext --- ### 3. `dist/bizmatch/server/index.server.html` - Server Template **WICHTIG:** Diese Datei wird **beim Build erstellt**, nicht manuell geschrieben! ```bash # Build-Prozess erstellt automatisch: npm run build:ssr → dist/bizmatch/server/index.server.html ✅ → dist/bizmatch/server/server.mjs ✅ → dist/bizmatch/browser/index.csr.html ✅ ``` **Quelle:** - Angular nimmt `src/index.html` als Vorlage - Fügt SSR-spezifische Meta-Tags hinzu - Generiert `index.server.html` für serverseitiges Rendering - Generiert `index.csr.html` für clientseitiges Rendering (Fallback) **Warum nicht im Git?** - Build-Artefakte werden nicht eingecheckt (`.gitignore`) - Jeder Build erstellt sie neu - Verhindert Merge-Konflikte bei generierten Dateien **Fehlerquelle bei neuem Laptop:** ``` git clone → dist/ Ordner fehlt → index.server.html fehlt → npm run serve:ssr crasht ❌ Lösung: → npm run build:ssr → index.server.html wird erstellt ✅ ``` --- ### 4. `src/ssr-dom-polyfill.ts` - DOM-Mocks ```typescript const windowMock = { document: { createElement: () => ({ ... }) }, localStorage: { getItem: () => null }, navigator: { userAgent: 'node' }, // ... etc }; if (typeof window === 'undefined') { (global as any).window = windowMock; } ``` **Rolle:** - Simuliert Browser-APIs in Node.js - Verhindert `ReferenceError: window is not defined` - Ermöglicht die Ausführung von Browser-Code im Server - Kritisch für Libraries wie Leaflet, die `window` erwarten **Warum notwendig?** - Angular-Code nutzt `window`, `document`, `localStorage`, etc. - Node.js hat diese APIs nicht - Ohne Polyfills: Crash beim Server-Start --- ### 4. `ssr-dom-preload.mjs` - Node.js Preload Script ```javascript import { isMainThread } from 'node:worker_threads'; if (!isMainThread) { // Skip polyfills in worker threads (sass, esbuild) } else { globalThis.window = windowMock; globalThis.document = documentMock; } ``` **Rolle:** - Wird beim `dev:ssr` verwendet - Lädt DOM-Mocks **VOR** allen anderen Modulen - Nutzt Node.js `--import` Flag - Vermeidet Probleme mit early imports **Verwendung:** ```bash NODE_OPTIONS='--import ./ssr-dom-preload.mjs' ng serve ``` --- ### 5. `app.config.server.ts` - Server-spezifische Config Enthält Provider, die nur im Server-Kontext geladen werden: - `provideServerRendering()` - Server-spezifische HTTP-Interceptors - TransferState für API-Daten --- ## Rendering-Ablauf im Detail ### Phase 1: Server-Side Rendering ``` 1. Request kommt an: GET /business/restaurant-123 2. Express Router: → server.get('*', ...) 3. AngularNodeAppEngine: → bootstrapApplication(AppComponent, serverConfig) → Angular läuft in Node.js 4. Angular Router: → Route /business/:slug matched → ListingDetailComponent wird aktiviert 5. Component Lifecycle: → ngOnInit() wird ausgeführt → API-Call: fetch('/api/listings/restaurant-123') → Daten werden geladen → Template wird mit Daten gerendert 6. TransferState: → API-Response wird in HTML injiziert → 7. Meta-Tags: → Title-Service setzt → Meta-Service setzt <meta name="description"> 8. HTML-Output: → Komplettes HTML mit Daten → Wird an Browser gesendet ``` **Server-Output:** ```html <!doctype html> <html> <head> <title>Restaurant "Zum Löwen" | BizMatch

Restaurant "Zum Löwen"

Adresse: Hauptstraße 1, München

``` --- ### Phase 2: Client-Side Hydration ``` 1. Browser empfängt HTML: → Zeigt sofort gerenderten Content an ✅ → User sieht Inhalt ohne Verzögerung 2. JavaScript lädt: → main.js wird heruntergeladen → Angular-Runtime startet 3. Hydration beginnt: → Angular scannt DOM → Vergleicht Server-HTML mit Client-Template → Attachiert Event Listener → Aktiviert Interaktivität 4. TransferState wiederverwenden: → Liest window.__NG_STATE__ → Überspringt erneute API-Calls ✅ → Daten sind bereits vorhanden 5. App ist interaktiv: → Buttons funktionieren → Routing funktioniert → SPA-Verhalten aktiviert ``` **Wichtig:** - **Kein Flickern** (Server-HTML = Client-HTML) - **Keine doppelten API-Calls** (TransferState) - **Schneller First Contentful Paint** (HTML sofort sichtbar) --- ## SSR vs. Non-SSR: Was wird wann gerendert? ### Ohne SSR (`npm start`) | Zeitpunkt | Server | Browser | |-----------|--------|---------| | T0: Request | Sendet leere `index.html` | - | | T1: HTML empfangen | - | Leeres `` | | T2: JS geladen | - | Angular startet | | T3: API-Call | - | Lädt Daten | | T4: Rendering | - | **Erst jetzt sichtbar** ❌ | **Time to First Contentful Paint:** ~2-3 Sekunden --- ### Mit SSR (`npm run serve:ssr`) | Zeitpunkt | Server | Browser | |-----------|--------|---------| | T0: Request | Angular rendert + API-Call | - | | T1: HTML empfangen | - | **Inhalt sofort sichtbar** ✅ | | T2: JS geladen | - | Hydration beginnt | | T3: Interaktiv | - | Event Listener attached | **Time to First Contentful Paint:** ~200-500ms --- ## Prerendering vs. SSR: Wann wird gerendert? ### Prerendering (Static Site Generation) ``` Build-Zeit (npm run build): → ng build → Rendert /business/1.html → Rendert /business/2.html → Rendert /business/3.html → ... → Alle HTML-Dateien auf Server deployed Request-Zeit: → Nginx sendet vorgefertigte HTML-Datei → KEIN Server-Side Rendering ``` **Vorteile:** - Extrem schnell (statisches HTML) - Kein Node.js-Server erforderlich - Günstig (CDN-Hosting) **Nachteile:** - Nicht für dynamische Daten geeignet - Re-Build bei jeder Änderung nötig - Tausende Seiten = lange Build-Zeit --- ### SSR (Server-Side Rendering) ``` Build-Zeit (npm run build:ssr): → ng build (Client-Bundles) → ng build (Server-Bundles) → KEINE HTML-Dateien generiert Request-Zeit: → Node.js Server empfängt Request → Angular rendert HTML on-the-fly → Frische Daten aus DB → Sendet HTML zurück ``` **Vorteile:** - Immer aktuelle Daten - Personalisierte Inhalte - Keine lange Build-Zeit **Nachteile:** - Server-Ressourcen erforderlich - Langsamer als Prerendering (Rendering kostet Zeit) - Komplexere Infrastruktur --- ### BizMatch: Warum SSR statt Prerendering? **Gründe:** 1. **Dynamische Listings:** - Neue Businesses werden täglich hinzugefügt - Prerendering würde tägliche Re-Builds erfordern 2. **Personalisierte Daten:** - Benutzer sehen unterschiedliche Inhalte (Favoriten, etc.) - Prerendering kann nicht personalisieren 3. **Suche und Filter:** - Unendliche Kombinationen von Filtern - Unmöglich, alle Varianten vorzurendern 4. **Skalierung:** - 10.000+ Listings → Prerendering = 10.000+ HTML-Dateien - SSR = 1 Server, rendert on-demand --- ## Client-Side Hydration im Detail ### Was ist Hydration? **Hydration** = Angular "erweckt" das Server-HTML zum Leben. **Ohne Hydration:** - HTML ist statisch - Buttons funktionieren nicht - Routing funktioniert nicht - Kein JavaScript-Event-Handling **Nach Hydration:** - Angular übernimmt Kontrolle - Event Listener werden attached - SPA-Routing funktioniert - Interaktivität aktiviert --- ### Hydration-Ablauf ```typescript // 1. Server rendert HTML // 2. Browser empfängt HTML // → Button ist sichtbar, aber (click) funktioniert NICHT // 3. Angular-JavaScript lädt // → main.js wird ausgeführt // 4. Hydration scannt DOM angular.hydrate({ serverHTML: '', clientTemplate: '', // Vergleich: HTML matches Template? ✅ // → Reuse DOM node // → Attach Event Listener }); // 5. Button ist jetzt interaktiv // → (click) funktioniert ✅ ``` --- ### Probleme bei Hydration #### Problem 1: Mismatch zwischen Server und Client **Ursache:** ```typescript // Server rendert:
Server Time: {{ serverTime }}
// Client rendert:
Server Time: {{ clientTime }}
// ← Unterschiedlich! ``` **Folge:** - Angular erkennt Mismatch - Wirft Warnung in Console - Re-rendert Component (Performance-Verlust) **Lösung:** - TransferState nutzen für gemeinsame Daten - `isPlatformServer()` für unterschiedliche Logik --- #### Problem 2: Browser-only Code wird im Server ausgeführt **Ursache:** ```typescript ngOnInit() { window.scrollTo(0, 0); // ← CRASH: window ist undefined im Server } ``` **Lösung:** ```typescript import { isPlatformBrowser } from '@angular/common'; import { PLATFORM_ID } from '@angular/core'; constructor(@Inject(PLATFORM_ID) private platformId: Object) {} ngOnInit() { if (isPlatformBrowser(this.platformId)) { window.scrollTo(0, 0); // ← Nur im Browser } } ``` --- ## TransferState: Verhindert doppelte API-Calls ### Problem ohne TransferState ``` Server: → GET /api/listings/123 ← API-Call 1 → Rendert HTML mit Daten Browser (nach JS-Load): → GET /api/listings/123 ← API-Call 2 (doppelt!) → Re-rendert Component ``` **Problem:** - Doppelter Netzwerk-Traffic - Langsamere Hydration - Flickern beim Re-Render --- ### Lösung: TransferState **Server-Side:** ```typescript import { TransferState, makeStateKey } from '@angular/platform-browser'; const LISTING_KEY = makeStateKey('listing-123'); ngOnInit() { this.http.get('/api/listings/123').subscribe(data => { this.transferState.set(LISTING_KEY, data); // ← Speichern this.listing = data; }); } ``` **HTML-Output:** ```html ``` **Client-Side:** ```typescript ngOnInit() { const cachedData = this.transferState.get(LISTING_KEY, null); if (cachedData) { this.listing = cachedData; // ← Wiederverwenden ✅ } else { this.http.get('/api/listings/123').subscribe(...); // ← Nur wenn nicht cached } this.transferState.remove(LISTING_KEY); // ← Cleanup } ``` **Ergebnis:** - ✅ Nur 1 API-Call (serverseitig) - ✅ Kein Flickern - ✅ Schnellere Hydration --- ## Performance-Vergleich ### Metriken | Metrik | Ohne SSR | Mit SSR | Verbesserung | |--------|----------|---------|--------------| | **Time to First Byte (TTFB)** | 50ms | 200ms | -150ms ❌ | | **First Contentful Paint (FCP)** | 2.5s | 0.5s | **-2s ✅** | | **Largest Contentful Paint (LCP)** | 3.2s | 0.8s | **-2.4s ✅** | | **Time to Interactive (TTI)** | 3.5s | 2.8s | -0.7s ✅ | | **SEO Score (Lighthouse)** | 60 | 95 | +35 ✅ | **Wichtig:** - TTFB ist langsamer (Server muss rendern) - Aber FCP viel schneller (HTML sofort sichtbar) - User-Wahrnehmung: SSR fühlt sich schneller an --- ## SEO-Vorteile ### Google Crawler **Ohne SSR:** ```html ``` → ❌ Kein Content indexiert → ❌ Kein Ranking → ❌ Keine Rich Snippets --- **Mit SSR:** ```html Restaurant "Zum Löwen" | BizMatch

Restaurant "Zum Löwen"

Adresse: Hauptstraße 1, 80331 München

Restaurant "Zum Löwen" München
``` → ✅ Vollständiger Content indexiert → ✅ Besseres Ranking → ✅ Rich Snippets (Sterne, Adresse, etc.) --- ### Social Media Previews (Open Graph) **Ohne SSR:** ```html BizMatch ``` → ❌ Kein Preview-Bild → ❌ Keine Beschreibung --- **Mit SSR:** ```html ``` → ✅ Schönes Preview beim Teilen → ✅ Mehr Klicks → ✅ Bessere User Experience --- ## Zusammenfassung ### SSR in BizMatch bedeutet: 1. **Server rendert HTML vorab** (nicht erst im Browser) 2. **Browser zeigt sofort Inhalt** (schneller First Paint) 3. **JavaScript hydrated im Hintergrund** (macht HTML interaktiv) 4. **Kein Flickern, keine doppelten API-Calls** (TransferState) 5. **Besseres SEO** (Google sieht vollständigen Content) 6. **Social-Media-Previews funktionieren** (Open Graph Tags) ### Technischer Stack: - **@angular/ssr**: SSR-Engine - **Express**: HTTP-Server - **AngularNodeAppEngine**: Rendert Angular in Node.js - **ssr-dom-polyfill.ts**: Simuliert Browser-APIs - **TransferState**: Verhindert doppelte API-Calls ### Wann wird was gerendert? - **Build-Zeit:** Nichts (kein Prerendering) - **Request-Zeit:** Server rendert HTML on-the-fly - **Nach JS-Load:** Hydration macht HTML interaktiv ### Best Practices: 1. Browser-Code mit `isPlatformBrowser()` schützen 2. TransferState für API-Daten nutzen 3. DOM-Polyfills für Third-Party-Libraries 4. Meta-Tags serverseitig setzen 5. Server-Build vor Deployment testen