785 lines
21 KiB
Markdown
785 lines
21 KiB
Markdown
# 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:**
|
|
```html
|
|
<!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:**
|
|
```html
|
|
<!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
|
|
|
|
```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
|
|
→ <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:**
|
|
```html
|
|
<!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
|
|
|
|
```typescript
|
|
// 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:**
|
|
```typescript
|
|
// 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:**
|
|
```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>('listing-123');
|
|
|
|
ngOnInit() {
|
|
this.http.get('/api/listings/123').subscribe(data => {
|
|
this.transferState.set(LISTING_KEY, data); // ← Speichern
|
|
this.listing = data;
|
|
});
|
|
}
|
|
```
|
|
|
|
**HTML-Output:**
|
|
```html
|
|
<script id="ng-state" type="application/json">
|
|
{"listing-123": {"name": "Restaurant", "address": "..."}}
|
|
</script>
|
|
```
|
|
|
|
**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
|
|
<!-- Google sieht nur: -->
|
|
<app-root></app-root>
|
|
<script src="main.js"></script>
|
|
```
|
|
|
|
→ ❌ Kein Content indexiert
|
|
→ ❌ Kein Ranking
|
|
→ ❌ Keine Rich Snippets
|
|
|
|
---
|
|
|
|
**Mit SSR:**
|
|
```html
|
|
<!-- 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:**
|
|
```html
|
|
<!-- Facebook/Twitter sehen nur: -->
|
|
<title>BizMatch</title>
|
|
```
|
|
|
|
→ ❌ Kein Preview-Bild
|
|
→ ❌ Keine Beschreibung
|
|
|
|
---
|
|
|
|
**Mit SSR:**
|
|
```html
|
|
<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
|