bizmatch-project/bizmatch/SSR_DOKUMENTATION.md

21 KiB

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 <app-root></app-root>
       → lädt JavaScript-Bundles
       → JavaScript rendert die Seite

HTML-Response:

<!doctype html>
<html>
  <head><title>BizMatch</title></head>
  <body>
    <app-root></app-root>  <!-- LEER! -->
    <script src="main.js"></script>
  </body>
</html>

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:

<!doctype html>
<html>
  <head>
    <title>Restaurant "Zum Löwen" | BizMatch</title>
    <meta name="description" content="Restaurant in München...">
  </head>
  <body>
    <app-root>
      <div class="listing-page">
        <h1>Restaurant "Zum Löwen"</h1>
        <p>Traditionelles deutsches Restaurant...</p>
        <!-- Kompletter gerendeter Content! -->
      </div>
    </app-root>
    <script src="main.js"></script>
  </body>
</html>

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

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

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!

# 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

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

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:

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
   → <script>window.__NG_STATE__ = {...}</script>

7. Meta-Tags:
   → Title-Service setzt <title>
   → Meta-Service setzt <meta name="description">

8. HTML-Output:
   → Komplettes HTML mit Daten
   → Wird an Browser gesendet

Server-Output:

<!doctype html>
<html>
  <head>
    <title>Restaurant "Zum Löwen" | BizMatch</title>
    <meta name="description" content="Traditionelles Restaurant...">
  </head>
  <body>
    <app-root>
      <!-- Vollständig gerenderte Component -->
      <div class="listing-detail">
        <h1>Restaurant "Zum Löwen"</h1>
        <p>Adresse: Hauptstraße 1, München</p>
        <!-- etc. -->
      </div>
    </app-root>

    <!-- TransferState: verhindert doppelte API-Calls -->
    <script id="ng-state" type="application/json">
      {"listings":{"restaurant-123":{...}}}
    </script>

    <script src="main.js" defer></script>
  </body>
</html>

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 <app-root></app-root>
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

// 1. Server rendert HTML
<button (click)="openModal()">Details</button>

// 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: '<button>Details</button>',
  clientTemplate: '<button (click)="openModal()">Details</button>',

  // 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:

// Server rendert:
<div>Server Time: {{ serverTime }}</div>

// Client rendert:
<div>Server Time: {{ clientTime }}</div>  // ← 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:

ngOnInit() {
  window.scrollTo(0, 0);  // ← CRASH: window ist undefined im Server
}

Lösung:

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:

import { TransferState, makeStateKey } from '@angular/platform-browser';

const LISTING_KEY = makeStateKey<Listing>('listing-123');

ngOnInit() {
  this.http.get('/api/listings/123').subscribe(data => {
    this.transferState.set(LISTING_KEY, data);  // ← Speichern
    this.listing = data;
  });
}

HTML-Output:

<script id="ng-state" type="application/json">
  {"listing-123": {"name": "Restaurant", "address": "..."}}
</script>

Client-Side:

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:

<!-- Google sieht nur: -->
<app-root></app-root>
<script src="main.js"></script>

Kein Content indexiert → Kein Ranking → Keine Rich Snippets


Mit SSR:

<!-- Google sieht: -->
<title>Restaurant "Zum Löwen" | BizMatch</title>
<meta name="description" content="Traditionelles Restaurant in München">
<h1>Restaurant "Zum Löwen"</h1>
<p>Adresse: Hauptstraße 1, 80331 München</p>
<div itemscope itemtype="https://schema.org/Restaurant">
  <span itemprop="name">Restaurant "Zum Löwen"</span>
  <span itemprop="address">München</span>
</div>

Vollständiger Content indexiert → Besseres Ranking → Rich Snippets (Sterne, Adresse, etc.)


Social Media Previews (Open Graph)

Ohne SSR:

<!-- Facebook/Twitter sehen nur: -->
<title>BizMatch</title>

Kein Preview-Bild → Keine Beschreibung


Mit SSR:

<meta property="og:title" content="Restaurant 'Zum Löwen'" />
<meta property="og:description" content="Traditionelles Restaurant..." />
<meta property="og:image" content="https://bizmatch.net/images/restaurant.jpg" />
<meta property="og:url" content="https://bizmatch.net/business/restaurant-123" />

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