diff --git a/bizmatch-server/prod.dump b/bizmatch-server/prod.dump old mode 100644 new mode 100755 diff --git a/bizmatch-server/scripts/migrate-slugs.sql b/bizmatch-server/scripts/migrate-slugs.sql index 8fa83ab..bfc654c 100644 --- a/bizmatch-server/scripts/migrate-slugs.sql +++ b/bizmatch-server/scripts/migrate-slugs.sql @@ -1,117 +1,117 @@ --- ============================================================= --- SEO SLUG MIGRATION SCRIPT --- Run this directly in your PostgreSQL database --- ============================================================= - --- First, let's see how many listings need slugs -SELECT 'Businesses without slugs: ' || COUNT(*) FROM businesses_json -WHERE data->>'slug' IS NULL OR data->>'slug' = ''; - -SELECT 'Commercial properties without slugs: ' || COUNT(*) FROM commercials_json -WHERE data->>'slug' IS NULL OR data->>'slug' = ''; - --- ============================================================= --- UPDATE BUSINESS LISTINGS WITH SEO SLUGS --- Format: title-city-state-shortid (e.g., restaurant-austin-tx-a3f7b2c1) --- ============================================================= - -UPDATE businesses_json -SET data = jsonb_set( - data::jsonb, - '{slug}', - to_jsonb( - LOWER( - REGEXP_REPLACE( - REGEXP_REPLACE( - CONCAT( - -- Title (first 50 chars, cleaned) - SUBSTRING( - REGEXP_REPLACE( - LOWER(COALESCE(data->>'title', '')), - '[^a-z0-9\s-]', '', 'g' - ), 1, 50 - ), - '-', - -- City or County - REGEXP_REPLACE( - LOWER(COALESCE(data->'location'->>'name', data->'location'->>'county', '')), - '[^a-z0-9\s-]', '', 'g' - ), - '-', - -- State - LOWER(COALESCE(data->'location'->>'state', '')), - '-', - -- First 8 chars of UUID - SUBSTRING(id::text, 1, 8) - ), - '\s+', '-', 'g' -- Replace spaces with hyphens - ), - '-+', '-', 'g' -- Replace multiple hyphens with single - ) - ) - ) -) -WHERE data->>'slug' IS NULL OR data->>'slug' = ''; - --- ============================================================= --- UPDATE COMMERCIAL PROPERTIES WITH SEO SLUGS --- ============================================================= - -UPDATE commercials_json -SET data = jsonb_set( - data::jsonb, - '{slug}', - to_jsonb( - LOWER( - REGEXP_REPLACE( - REGEXP_REPLACE( - CONCAT( - -- Title (first 50 chars, cleaned) - SUBSTRING( - REGEXP_REPLACE( - LOWER(COALESCE(data->>'title', '')), - '[^a-z0-9\s-]', '', 'g' - ), 1, 50 - ), - '-', - -- City or County - REGEXP_REPLACE( - LOWER(COALESCE(data->'location'->>'name', data->'location'->>'county', '')), - '[^a-z0-9\s-]', '', 'g' - ), - '-', - -- State - LOWER(COALESCE(data->'location'->>'state', '')), - '-', - -- First 8 chars of UUID - SUBSTRING(id::text, 1, 8) - ), - '\s+', '-', 'g' -- Replace spaces with hyphens - ), - '-+', '-', 'g' -- Replace multiple hyphens with single - ) - ) - ) -) -WHERE data->>'slug' IS NULL OR data->>'slug' = ''; - --- ============================================================= --- VERIFY THE RESULTS --- ============================================================= - -SELECT 'Migration complete! Checking results...' AS status; - --- Show sample of updated slugs -SELECT - id, - data->>'title' AS title, - data->>'slug' AS slug -FROM businesses_json -LIMIT 5; - -SELECT - id, - data->>'title' AS title, - data->>'slug' AS slug -FROM commercials_json -LIMIT 5; +-- ============================================================= +-- SEO SLUG MIGRATION SCRIPT +-- Run this directly in your PostgreSQL database +-- ============================================================= + +-- First, let's see how many listings need slugs +SELECT 'Businesses without slugs: ' || COUNT(*) FROM businesses_json +WHERE data->>'slug' IS NULL OR data->>'slug' = ''; + +SELECT 'Commercial properties without slugs: ' || COUNT(*) FROM commercials_json +WHERE data->>'slug' IS NULL OR data->>'slug' = ''; + +-- ============================================================= +-- UPDATE BUSINESS LISTINGS WITH SEO SLUGS +-- Format: title-city-state-shortid (e.g., restaurant-austin-tx-a3f7b2c1) +-- ============================================================= + +UPDATE businesses_json +SET data = jsonb_set( + data::jsonb, + '{slug}', + to_jsonb( + LOWER( + REGEXP_REPLACE( + REGEXP_REPLACE( + CONCAT( + -- Title (first 50 chars, cleaned) + SUBSTRING( + REGEXP_REPLACE( + LOWER(COALESCE(data->>'title', '')), + '[^a-z0-9\s-]', '', 'g' + ), 1, 50 + ), + '-', + -- City or County + REGEXP_REPLACE( + LOWER(COALESCE(data->'location'->>'name', data->'location'->>'county', '')), + '[^a-z0-9\s-]', '', 'g' + ), + '-', + -- State + LOWER(COALESCE(data->'location'->>'state', '')), + '-', + -- First 8 chars of UUID + SUBSTRING(id::text, 1, 8) + ), + '\s+', '-', 'g' -- Replace spaces with hyphens + ), + '-+', '-', 'g' -- Replace multiple hyphens with single + ) + ) + ) +) +WHERE data->>'slug' IS NULL OR data->>'slug' = ''; + +-- ============================================================= +-- UPDATE COMMERCIAL PROPERTIES WITH SEO SLUGS +-- ============================================================= + +UPDATE commercials_json +SET data = jsonb_set( + data::jsonb, + '{slug}', + to_jsonb( + LOWER( + REGEXP_REPLACE( + REGEXP_REPLACE( + CONCAT( + -- Title (first 50 chars, cleaned) + SUBSTRING( + REGEXP_REPLACE( + LOWER(COALESCE(data->>'title', '')), + '[^a-z0-9\s-]', '', 'g' + ), 1, 50 + ), + '-', + -- City or County + REGEXP_REPLACE( + LOWER(COALESCE(data->'location'->>'name', data->'location'->>'county', '')), + '[^a-z0-9\s-]', '', 'g' + ), + '-', + -- State + LOWER(COALESCE(data->'location'->>'state', '')), + '-', + -- First 8 chars of UUID + SUBSTRING(id::text, 1, 8) + ), + '\s+', '-', 'g' -- Replace spaces with hyphens + ), + '-+', '-', 'g' -- Replace multiple hyphens with single + ) + ) + ) +) +WHERE data->>'slug' IS NULL OR data->>'slug' = ''; + +-- ============================================================= +-- VERIFY THE RESULTS +-- ============================================================= + +SELECT 'Migration complete! Checking results...' AS status; + +-- Show sample of updated slugs +SELECT + id, + data->>'title' AS title, + data->>'slug' AS slug +FROM businesses_json +LIMIT 5; + +SELECT + id, + data->>'title' AS title, + data->>'slug' AS slug +FROM commercials_json +LIMIT 5; diff --git a/bizmatch-server/scripts/migrate-slugs.ts b/bizmatch-server/scripts/migrate-slugs.ts index ee32b3c..a83170c 100644 --- a/bizmatch-server/scripts/migrate-slugs.ts +++ b/bizmatch-server/scripts/migrate-slugs.ts @@ -1,162 +1,162 @@ -/** - * Migration Script: Generate Slugs for Existing Listings - * - * This script generates SEO-friendly slugs for all existing businesses - * and commercial properties that don't have slugs yet. - * - * Run with: npx ts-node scripts/migrate-slugs.ts - */ - -import { Pool } from 'pg'; -import { drizzle, NodePgDatabase } from 'drizzle-orm/node-postgres'; -import { sql, eq, isNull } from 'drizzle-orm'; -import * as schema from '../src/drizzle/schema'; - -// Slug generation function (copied from utils for standalone execution) -function generateSlug(title: string, location: any, id: string): string { - if (!title || !id) return id; // Fallback to ID if no title - - const titleSlug = title - .toLowerCase() - .trim() - .replace(/[^\w\s-]/g, '') - .replace(/\s+/g, '-') - .replace(/-+/g, '-') - .substring(0, 50); - - let locationSlug = ''; - if (location) { - const locationName = location.name || location.county || ''; - const state = location.state || ''; - - if (locationName) { - locationSlug = locationName - .toLowerCase() - .trim() - .replace(/[^\w\s-]/g, '') - .replace(/\s+/g, '-') - .replace(/-+/g, '-'); - } - - if (state) { - locationSlug = locationSlug - ? `${locationSlug}-${state.toLowerCase()}` - : state.toLowerCase(); - } - } - - const shortId = id.substring(0, 8); - const parts = [titleSlug, locationSlug, shortId].filter(Boolean); - return parts.join('-').replace(/-+/g, '-').replace(/^-|-$/g, '').toLowerCase(); -} - -async function migrateBusinessSlugs(db: NodePgDatabase) { - console.log('πŸ”„ Migrating Business Listings...'); - - // Get all businesses without slugs - const businesses = await db - .select({ - id: schema.businesses_json.id, - email: schema.businesses_json.email, - data: schema.businesses_json.data, - }) - .from(schema.businesses_json); - - let updated = 0; - let skipped = 0; - - for (const business of businesses) { - const data = business.data as any; - - // Skip if slug already exists - if (data.slug) { - skipped++; - continue; - } - - const slug = generateSlug(data.title || '', data.location || {}, business.id); - - // Update with new slug - const updatedData = { ...data, slug }; - await db - .update(schema.businesses_json) - .set({ data: updatedData }) - .where(eq(schema.businesses_json.id, business.id)); - - console.log(` βœ“ ${data.title?.substring(0, 40)}... β†’ ${slug}`); - updated++; - } - - console.log(`βœ… Business Listings: ${updated} updated, ${skipped} skipped (already had slugs)`); - return updated; -} - -async function migrateCommercialSlugs(db: NodePgDatabase) { - console.log('\nπŸ”„ Migrating Commercial Properties...'); - - // Get all commercial properties without slugs - const properties = await db - .select({ - id: schema.commercials_json.id, - email: schema.commercials_json.email, - data: schema.commercials_json.data, - }) - .from(schema.commercials_json); - - let updated = 0; - let skipped = 0; - - for (const property of properties) { - const data = property.data as any; - - // Skip if slug already exists - if (data.slug) { - skipped++; - continue; - } - - const slug = generateSlug(data.title || '', data.location || {}, property.id); - - // Update with new slug - const updatedData = { ...data, slug }; - await db - .update(schema.commercials_json) - .set({ data: updatedData }) - .where(eq(schema.commercials_json.id, property.id)); - - console.log(` βœ“ ${data.title?.substring(0, 40)}... β†’ ${slug}`); - updated++; - } - - console.log(`βœ… Commercial Properties: ${updated} updated, ${skipped} skipped (already had slugs)`); - return updated; -} - -async function main() { - console.log('═══════════════════════════════════════════════════════'); - console.log(' SEO SLUG MIGRATION SCRIPT'); - console.log('═══════════════════════════════════════════════════════\n'); - - // Connect to database - const connectionString = process.env.DATABASE_URL || 'postgresql://postgres:postgres@localhost:5432/bizmatch'; - console.log(`πŸ“‘ Connecting to database...`); - - const pool = new Pool({ connectionString }); - const db = drizzle(pool, { schema }); - - try { - const businessCount = await migrateBusinessSlugs(db); - const commercialCount = await migrateCommercialSlugs(db); - - console.log('\n═══════════════════════════════════════════════════════'); - console.log(`πŸŽ‰ Migration complete! Total: ${businessCount + commercialCount} listings updated`); - console.log('═══════════════════════════════════════════════════════\n'); - } catch (error) { - console.error('❌ Migration failed:', error); - process.exit(1); - } finally { - await pool.end(); - } -} - -main(); +/** + * Migration Script: Generate Slugs for Existing Listings + * + * This script generates SEO-friendly slugs for all existing businesses + * and commercial properties that don't have slugs yet. + * + * Run with: npx ts-node scripts/migrate-slugs.ts + */ + +import { Pool } from 'pg'; +import { drizzle, NodePgDatabase } from 'drizzle-orm/node-postgres'; +import { sql, eq, isNull } from 'drizzle-orm'; +import * as schema from '../src/drizzle/schema'; + +// Slug generation function (copied from utils for standalone execution) +function generateSlug(title: string, location: any, id: string): string { + if (!title || !id) return id; // Fallback to ID if no title + + const titleSlug = title + .toLowerCase() + .trim() + .replace(/[^\w\s-]/g, '') + .replace(/\s+/g, '-') + .replace(/-+/g, '-') + .substring(0, 50); + + let locationSlug = ''; + if (location) { + const locationName = location.name || location.county || ''; + const state = location.state || ''; + + if (locationName) { + locationSlug = locationName + .toLowerCase() + .trim() + .replace(/[^\w\s-]/g, '') + .replace(/\s+/g, '-') + .replace(/-+/g, '-'); + } + + if (state) { + locationSlug = locationSlug + ? `${locationSlug}-${state.toLowerCase()}` + : state.toLowerCase(); + } + } + + const shortId = id.substring(0, 8); + const parts = [titleSlug, locationSlug, shortId].filter(Boolean); + return parts.join('-').replace(/-+/g, '-').replace(/^-|-$/g, '').toLowerCase(); +} + +async function migrateBusinessSlugs(db: NodePgDatabase) { + console.log('πŸ”„ Migrating Business Listings...'); + + // Get all businesses without slugs + const businesses = await db + .select({ + id: schema.businesses_json.id, + email: schema.businesses_json.email, + data: schema.businesses_json.data, + }) + .from(schema.businesses_json); + + let updated = 0; + let skipped = 0; + + for (const business of businesses) { + const data = business.data as any; + + // Skip if slug already exists + if (data.slug) { + skipped++; + continue; + } + + const slug = generateSlug(data.title || '', data.location || {}, business.id); + + // Update with new slug + const updatedData = { ...data, slug }; + await db + .update(schema.businesses_json) + .set({ data: updatedData }) + .where(eq(schema.businesses_json.id, business.id)); + + console.log(` βœ“ ${data.title?.substring(0, 40)}... β†’ ${slug}`); + updated++; + } + + console.log(`βœ… Business Listings: ${updated} updated, ${skipped} skipped (already had slugs)`); + return updated; +} + +async function migrateCommercialSlugs(db: NodePgDatabase) { + console.log('\nπŸ”„ Migrating Commercial Properties...'); + + // Get all commercial properties without slugs + const properties = await db + .select({ + id: schema.commercials_json.id, + email: schema.commercials_json.email, + data: schema.commercials_json.data, + }) + .from(schema.commercials_json); + + let updated = 0; + let skipped = 0; + + for (const property of properties) { + const data = property.data as any; + + // Skip if slug already exists + if (data.slug) { + skipped++; + continue; + } + + const slug = generateSlug(data.title || '', data.location || {}, property.id); + + // Update with new slug + const updatedData = { ...data, slug }; + await db + .update(schema.commercials_json) + .set({ data: updatedData }) + .where(eq(schema.commercials_json.id, property.id)); + + console.log(` βœ“ ${data.title?.substring(0, 40)}... β†’ ${slug}`); + updated++; + } + + console.log(`βœ… Commercial Properties: ${updated} updated, ${skipped} skipped (already had slugs)`); + return updated; +} + +async function main() { + console.log('═══════════════════════════════════════════════════════'); + console.log(' SEO SLUG MIGRATION SCRIPT'); + console.log('═══════════════════════════════════════════════════════\n'); + + // Connect to database + const connectionString = process.env.DATABASE_URL || 'postgresql://postgres:postgres@localhost:5432/bizmatch'; + console.log(`πŸ“‘ Connecting to database...`); + + const pool = new Pool({ connectionString }); + const db = drizzle(pool, { schema }); + + try { + const businessCount = await migrateBusinessSlugs(db); + const commercialCount = await migrateCommercialSlugs(db); + + console.log('\n═══════════════════════════════════════════════════════'); + console.log(`πŸŽ‰ Migration complete! Total: ${businessCount + commercialCount} listings updated`); + console.log('═══════════════════════════════════════════════════════\n'); + } catch (error) { + console.error('❌ Migration failed:', error); + process.exit(1); + } finally { + await pool.end(); + } +} + +main(); diff --git a/bizmatch/DEPLOYMENT.md b/bizmatch/DEPLOYMENT.md index 790ea68..ee36502 100644 --- a/bizmatch/DEPLOYMENT.md +++ b/bizmatch/DEPLOYMENT.md @@ -1,91 +1,91 @@ -# BizMatch Deployment Guide - -## Übersicht - -| Umgebung | Befehl | Port | SSR | -|----------|--------|------|-----| -| **Development** | `npm start` | 4200 | ❌ Aus | -| **Production** | `npm run build:ssr` β†’ `npm run serve:ssr` | 4200 | βœ… An | - ---- - -## Development (Lokale Entwicklung) - -```bash -cd ~/bizmatch-project/bizmatch -npm start -``` -- LΓ€uft auf http://localhost:4200 -- Hot-Reload aktiv -- Kein SSR (schneller fΓΌr Entwicklung) - ---- - -## Production Deployment - -### 1. Build erstellen -```bash -npm run build:ssr -``` -Erstellt optimierte Bundles in `dist/bizmatch/` - -### 2. Server starten - -**Direkt (zum Testen):** -```bash -npm run serve:ssr -``` - -**Mit PM2 (empfohlen fΓΌr Production):** -```bash -# Einmal PM2 installieren -npm install -g pm2 - -# Server starten -pm2 start dist/bizmatch/server/server.mjs --name "bizmatch" - -# Nach Code-Γ„nderungen -npm run build:ssr && pm2 restart bizmatch - -# Logs anzeigen -pm2 logs bizmatch - -# Status prΓΌfen -pm2 status -``` - -### 3. Nginx Reverse Proxy (optional) -```nginx -server { - listen 80; - server_name deinedomain.com; - - location / { - proxy_pass http://localhost:4200; - proxy_http_version 1.1; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - } -} -``` - ---- - -## SEO Features (aktiv mit SSR) - -- βœ… Server-Side Rendering fΓΌr alle Seiten -- βœ… Meta-Tags und Titel werden serverseitig generiert -- βœ… Sitemaps unter `/sitemap.xml` -- βœ… robots.txt konfiguriert -- βœ… Strukturierte Daten (Schema.org) - ---- - -## Wichtige Dateien - -| Datei | Zweck | -|-------|-------| -| `server.ts` | Express SSR Server | -| `src/main.server.ts` | Angular Server Entry Point | -| `src/ssr-dom-polyfill.ts` | DOM Polyfills fΓΌr SSR | -| `dist/bizmatch/server/` | Kompilierte Server-Bundles | +# BizMatch Deployment Guide + +## Übersicht + +| Umgebung | Befehl | Port | SSR | +|----------|--------|------|-----| +| **Development** | `npm start` | 4200 | ❌ Aus | +| **Production** | `npm run build:ssr` β†’ `npm run serve:ssr` | 4200 | βœ… An | + +--- + +## Development (Lokale Entwicklung) + +```bash +cd ~/bizmatch-project/bizmatch +npm start +``` +- LΓ€uft auf http://localhost:4200 +- Hot-Reload aktiv +- Kein SSR (schneller fΓΌr Entwicklung) + +--- + +## Production Deployment + +### 1. Build erstellen +```bash +npm run build:ssr +``` +Erstellt optimierte Bundles in `dist/bizmatch/` + +### 2. Server starten + +**Direkt (zum Testen):** +```bash +npm run serve:ssr +``` + +**Mit PM2 (empfohlen fΓΌr Production):** +```bash +# Einmal PM2 installieren +npm install -g pm2 + +# Server starten +pm2 start dist/bizmatch/server/server.mjs --name "bizmatch" + +# Nach Code-Γ„nderungen +npm run build:ssr && pm2 restart bizmatch + +# Logs anzeigen +pm2 logs bizmatch + +# Status prΓΌfen +pm2 status +``` + +### 3. Nginx Reverse Proxy (optional) +```nginx +server { + listen 80; + server_name deinedomain.com; + + location / { + proxy_pass http://localhost:4200; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + } +} +``` + +--- + +## SEO Features (aktiv mit SSR) + +- βœ… Server-Side Rendering fΓΌr alle Seiten +- βœ… Meta-Tags und Titel werden serverseitig generiert +- βœ… Sitemaps unter `/sitemap.xml` +- βœ… robots.txt konfiguriert +- βœ… Strukturierte Daten (Schema.org) + +--- + +## Wichtige Dateien + +| Datei | Zweck | +|-------|-------| +| `server.ts` | Express SSR Server | +| `src/main.server.ts` | Angular Server Entry Point | +| `src/ssr-dom-polyfill.ts` | DOM Polyfills fΓΌr SSR | +| `dist/bizmatch/server/` | Kompilierte Server-Bundles | diff --git a/bizmatch/SSR_ANLEITUNG.md b/bizmatch/SSR_ANLEITUNG.md new file mode 100644 index 0000000..ca8070c --- /dev/null +++ b/bizmatch/SSR_ANLEITUNG.md @@ -0,0 +1,275 @@ +# BizMatch SSR - Schritt-fΓΌr-Schritt-Anleitung + +## Problem: SSR startet nicht auf neuem Laptop? + +Diese Anleitung hilft Ihnen, BizMatch mit Server-Side Rendering (SSR) auf einem neuen Rechner zum Laufen zu bringen. + +--- + +## Voraussetzungen prΓΌfen + +```bash +# Node.js Version prΓΌfen (mind. v18 erforderlich) +node --version + +# npm Version prΓΌfen +npm --version + +# Falls Node.js fehlt oder veraltet ist: +# https://nodejs.org/ β†’ LTS Version herunterladen +``` + +--- + +## Schritt 1: Repository klonen (falls noch nicht geschehen) + +```bash +git clone https://gitea.bizmatch.net/aknuth/bizmatch-project.git +cd bizmatch-project/bizmatch +``` + +--- + +## Schritt 2: Dependencies installieren + +**WICHTIG:** Dieser Schritt ist essentiell und wird oft vergessen! + +```bash +cd ~/bizmatch-project/bizmatch +npm install +``` + +> **Tipp:** Bei Problemen versuchen Sie: `rm -rf node_modules package-lock.json && npm install` + +--- + +## ⚠️ WICHTIG: Erstes Setup auf neuem Laptop + +**Wenn Sie das Projekt zum ersten Mal auf einem neuen Rechner klonen, mΓΌssen Sie ZUERST einen Build erstellen!** + +```bash +cd ~/bizmatch-project/bizmatch + +# 1. Dependencies installieren +npm install + +# 2. Build erstellen (erstellt dist/bizmatch/server/index.server.html) +npm run build:ssr +``` + +**Warum?** +- Die `dist/` Folder werden NICHT ins Git eingecheckt (`.gitignore`) +- Die Datei `dist/bizmatch/server/index.server.html` fehlt nach `git clone` +- Ohne Build β†’ `npm run serve:ssr` crasht mit "Cannot find index.server.html" + +**Nach dem ersten Build** kΓΆnnen Sie dann Development-Befehle nutzen. + +--- + +## Schritt 3: Umgebung wΓ€hlen + +### Option A: Entwicklung (OHNE SSR) + +Schnellster Weg fΓΌr lokale Entwicklung: + +```bash +npm start +``` + +- Γ–ffnet automatisch: http://localhost:4200 +- Hot-Reload aktiv (Code-Γ„nderungen werden sofort sichtbar) +- **Kein SSR** (schneller fΓΌr Entwicklung) + +### Option B: Development mit SSR + +FΓΌr SSR-Testing wΓ€hrend der Entwicklung: + +```bash +npm run dev:ssr +``` + +- Γ–ffnet: http://localhost:4200 +- Hot-Reload aktiv +- **SSR aktiv** (simuliert Production) +- Nutzt DOM-Polyfills via `ssr-dom-preload.mjs` + +### Option C: Production Build mit SSR + +FΓΌr finalen Production-Test: + +```bash +# 1. Build erstellen +npm run build:ssr + +# 2. Server starten +npm run serve:ssr +``` + +- Server lΓ€uft auf: http://localhost:4200 +- **VollstΓ€ndiges SSR** (wie in Production) +- Kein Hot-Reload (fΓΌr Γ„nderungen erneut builden) + +--- + +## Schritt 4: Testen + +Γ–ffnen Sie http://localhost:4200 im Browser. + +### SSR funktioniert, wenn: + +1. **Seitenquelltext ansehen** (Rechtsklick β†’ "Seitenquelltext anzeigen"): + - HTML-Inhalt ist bereits vorhanden (nicht nur ``) + - Meta-Tags sind sichtbar + +2. **JavaScript deaktivieren** (Chrome DevTools β†’ Settings β†’ Disable JavaScript): + - Seite zeigt Inhalt an (wenn auch nicht interaktiv) + +3. **Network-Tab** (Chrome DevTools β†’ Network β†’ Doc): + - HTML-Response enthΓ€lt bereits gerenderten Content + +--- + +## HΓ€ufige Probleme und LΓΆsungen + +### Problem 1: `npm: command not found` + +**LΓΆsung:** Node.js installieren + +```bash +# Ubuntu/Debian +curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash - +sudo apt-get install -y nodejs + +# macOS +brew install node + +# Windows +# https://nodejs.org/ β†’ Installer herunterladen +``` + +### Problem 2: `Cannot find module '@angular/ssr'` + +**LΓΆsung:** Dependencies neu installieren + +```bash +rm -rf node_modules package-lock.json +npm install +``` + +### Problem 3: `Error: EADDRINUSE: address already in use :::4200` + +**LΓΆsung:** Port ist bereits belegt + +```bash +# Prozess finden und beenden +lsof -i :4200 +kill -9 + +# Oder anderen Port nutzen +PORT=4300 npm run serve:ssr +``` + +### Problem 4: `Error loading @angular/platform-server` oder "Cannot find index.server.html" + +**LΓΆsung:** Build fehlt oder ist veraltet + +```bash +# dist-Ordner lΓΆschen und neu builden +rm -rf dist +npm run build:ssr + +# Dann starten +npm run serve:ssr +``` + +**HΓ€ufiger Fehler auf neuem Laptop:** +- Nach `git pull` fehlt der `dist/` Ordner komplett +- `index.server.html` wird beim Build erstellt, nicht ins Git eingecheckt +- **LΓΆsung:** Immer erst `npm run build:ssr` ausfΓΌhren! + +### Problem 5: "Seite lΓ€dt nicht" oder "White Screen" + +**LΓΆsung:** + +1. Browser-Cache leeren (Strg+Shift+R / Cmd+Shift+R) +2. DevTools ΓΆffnen β†’ Console-Tab β†’ Fehler prΓΌfen +3. Sicherstellen, dass Backend lΓ€uft (falls API-Calls) + +### Problem 6: "Module not found: Error: Can't resolve 'window'" + +**LΓΆsung:** Browser-spezifischer Code wird im SSR-Build verwendet + +- PrΓΌfen Sie `ssr-dom-polyfill.ts` - DOM-Mocks sollten vorhanden sein +- Code mit `isPlatformBrowser()` schΓΌtzen: + +```typescript +import { isPlatformBrowser } from '@angular/common'; +import { PLATFORM_ID } from '@angular/core'; + +constructor(@Inject(PLATFORM_ID) private platformId: Object) {} + +ngOnInit() { + if (isPlatformBrowser(this.platformId)) { + // Nur im Browser ausfΓΌhren + window.scrollTo(0, 0); + } +} +``` + +--- + +## Production Deployment mit PM2 + +FΓΌr dauerhaften Betrieb (Server-Umgebung): + +```bash +# PM2 global installieren +npm install -g pm2 + +# Production Build +npm run build:ssr + +# Server mit PM2 starten +pm2 start dist/bizmatch/server/server.mjs --name "bizmatch" + +# Auto-Start bei Server-Neustart +pm2 startup +pm2 save + +# Logs anzeigen +pm2 logs bizmatch + +# Server neustarten nach Updates +npm run build:ssr && pm2 restart bizmatch +``` + +--- + +## Unterschiede der Befehle + +| Befehl | SSR | Hot-Reload | Verwendung | +|--------|-----|-----------|------------| +| `npm start` | ❌ | βœ… | Entwicklung (schnell) | +| `npm run dev:ssr` | βœ… | βœ… | Entwicklung mit SSR | +| `npm run build:ssr` | βœ… Build | ❌ | Production Build erstellen | +| `npm run serve:ssr` | βœ… | ❌ | Production Server starten | + +--- + +## NΓ€chste Schritte + +1. FΓΌr normale Entwicklung: **`npm start`** verwenden +2. Vor Production-Deployment: **`npm run build:ssr`** testen +3. SSR-FunktionalitΓ€t prΓΌfen (siehe "Schritt 4: Testen") +4. Bei Problemen: Logs prΓΌfen und obige LΓΆsungen durchgehen + +--- + +## Support + +Bei weiteren Problemen: + +1. **Logs prΓΌfen:** `npm run serve:ssr` zeigt Fehler in der Konsole +2. **Browser DevTools:** Console + Network Tab +3. **Build-Output:** `npm run build:ssr` zeigt Build-Fehler +4. **Node-Version:** `node --version` (sollte β‰₯ v18 sein) diff --git a/bizmatch/SSR_DOKUMENTATION.md b/bizmatch/SSR_DOKUMENTATION.md new file mode 100644 index 0000000..a37bfe4 --- /dev/null +++ b/bizmatch/SSR_DOKUMENTATION.md @@ -0,0 +1,784 @@ +# 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 diff --git a/bizmatch/src/ssr-dom-polyfill.ts b/bizmatch/src/ssr-dom-polyfill.ts index 05086b7..a06aa84 100644 --- a/bizmatch/src/ssr-dom-polyfill.ts +++ b/bizmatch/src/ssr-dom-polyfill.ts @@ -1,163 +1,163 @@ -/** - * DOM Polyfills for Server-Side Rendering - * - * This file must be imported BEFORE any browser-only libraries like Leaflet. - * It provides minimal stubs for browser globals that are required during module loading. - */ - -// Create a minimal screen mock -const screenMock = { - width: 1920, - height: 1080, - availWidth: 1920, - availHeight: 1080, - colorDepth: 24, - pixelDepth: 24, - deviceXDPI: 96, - deviceYDPI: 96, - logicalXDPI: 96, - logicalYDPI: 96, -}; - -// Create a minimal document mock -const documentMock = { - createElement: (tag: string) => ({ - style: {}, - setAttribute: () => { }, - getAttribute: () => null, - appendChild: () => { }, - removeChild: () => { }, - classList: { - add: () => { }, - remove: () => { }, - contains: () => false, - }, - tagName: tag.toUpperCase(), - }), - createElementNS: (ns: string, tag: string) => ({ - style: {}, - setAttribute: () => { }, - getAttribute: () => null, - appendChild: () => { }, - getBBox: () => ({ x: 0, y: 0, width: 0, height: 0 }), - tagName: tag.toUpperCase(), - }), - createTextNode: () => ({}), - head: { appendChild: () => { }, removeChild: () => { } }, - body: { appendChild: () => { }, removeChild: () => { } }, - documentElement: { - style: {}, - clientWidth: 1920, - clientHeight: 1080, - }, - addEventListener: () => { }, - removeEventListener: () => { }, - querySelector: () => null, - querySelectorAll: () => [], - getElementById: () => null, - getElementsByTagName: () => [], - getElementsByClassName: () => [], -}; - -// Create a minimal window mock for libraries that check for window existence during load -const windowMock = { - requestAnimationFrame: (callback: FrameRequestCallback) => setTimeout(callback, 16), - cancelAnimationFrame: (id: number) => clearTimeout(id), - addEventListener: () => { }, - removeEventListener: () => { }, - getComputedStyle: () => ({ - getPropertyValue: () => '', - }), - matchMedia: () => ({ - matches: false, - addListener: () => { }, - removeListener: () => { }, - addEventListener: () => { }, - removeEventListener: () => { }, - }), - document: documentMock, - screen: screenMock, - devicePixelRatio: 1, - navigator: { - userAgent: 'node', - platform: 'server', - language: 'en', - languages: ['en'], - onLine: true, - geolocation: null, - }, - location: { - hostname: 'localhost', - href: 'http://localhost', - protocol: 'http:', - pathname: '/', - search: '', - hash: '', - host: 'localhost', - origin: 'http://localhost', - }, - history: { - pushState: () => { }, - replaceState: () => { }, - back: () => { }, - forward: () => { }, - go: () => { }, - length: 0, - }, - localStorage: { - getItem: () => null, - setItem: () => { }, - removeItem: () => { }, - clear: () => { }, - }, - sessionStorage: { - getItem: () => null, - setItem: () => { }, - removeItem: () => { }, - clear: () => { }, - }, - setTimeout, - clearTimeout, - setInterval, - clearInterval, - innerWidth: 1920, - innerHeight: 1080, - outerWidth: 1920, - outerHeight: 1080, - scrollX: 0, - scrollY: 0, - pageXOffset: 0, - pageYOffset: 0, - scrollTo: () => { }, - scroll: () => { }, - Image: class Image { }, - HTMLElement: class HTMLElement { }, - SVGElement: class SVGElement { }, -}; - -// Only set globals if they don't exist (i.e., we're in Node.js) -if (typeof window === 'undefined') { - (global as any).window = windowMock; -} - -if (typeof document === 'undefined') { - (global as any).document = documentMock; -} - -if (typeof navigator === 'undefined') { - (global as any).navigator = windowMock.navigator; -} - -if (typeof screen === 'undefined') { - (global as any).screen = screenMock; -} - -if (typeof HTMLElement === 'undefined') { - (global as any).HTMLElement = windowMock.HTMLElement; -} - -if (typeof SVGElement === 'undefined') { - (global as any).SVGElement = windowMock.SVGElement; -} - -export { }; +/** + * DOM Polyfills for Server-Side Rendering + * + * This file must be imported BEFORE any browser-only libraries like Leaflet. + * It provides minimal stubs for browser globals that are required during module loading. + */ + +// Create a minimal screen mock +const screenMock = { + width: 1920, + height: 1080, + availWidth: 1920, + availHeight: 1080, + colorDepth: 24, + pixelDepth: 24, + deviceXDPI: 96, + deviceYDPI: 96, + logicalXDPI: 96, + logicalYDPI: 96, +}; + +// Create a minimal document mock +const documentMock = { + createElement: (tag: string) => ({ + style: {}, + setAttribute: () => { }, + getAttribute: () => null, + appendChild: () => { }, + removeChild: () => { }, + classList: { + add: () => { }, + remove: () => { }, + contains: () => false, + }, + tagName: tag.toUpperCase(), + }), + createElementNS: (ns: string, tag: string) => ({ + style: {}, + setAttribute: () => { }, + getAttribute: () => null, + appendChild: () => { }, + getBBox: () => ({ x: 0, y: 0, width: 0, height: 0 }), + tagName: tag.toUpperCase(), + }), + createTextNode: () => ({}), + head: { appendChild: () => { }, removeChild: () => { } }, + body: { appendChild: () => { }, removeChild: () => { } }, + documentElement: { + style: {}, + clientWidth: 1920, + clientHeight: 1080, + }, + addEventListener: () => { }, + removeEventListener: () => { }, + querySelector: () => null, + querySelectorAll: () => [], + getElementById: () => null, + getElementsByTagName: () => [], + getElementsByClassName: () => [], +}; + +// Create a minimal window mock for libraries that check for window existence during load +const windowMock = { + requestAnimationFrame: (callback: FrameRequestCallback) => setTimeout(callback, 16), + cancelAnimationFrame: (id: number) => clearTimeout(id), + addEventListener: () => { }, + removeEventListener: () => { }, + getComputedStyle: () => ({ + getPropertyValue: () => '', + }), + matchMedia: () => ({ + matches: false, + addListener: () => { }, + removeListener: () => { }, + addEventListener: () => { }, + removeEventListener: () => { }, + }), + document: documentMock, + screen: screenMock, + devicePixelRatio: 1, + navigator: { + userAgent: 'node', + platform: 'server', + language: 'en', + languages: ['en'], + onLine: true, + geolocation: null, + }, + location: { + hostname: 'localhost', + href: 'http://localhost', + protocol: 'http:', + pathname: '/', + search: '', + hash: '', + host: 'localhost', + origin: 'http://localhost', + }, + history: { + pushState: () => { }, + replaceState: () => { }, + back: () => { }, + forward: () => { }, + go: () => { }, + length: 0, + }, + localStorage: { + getItem: () => null, + setItem: () => { }, + removeItem: () => { }, + clear: () => { }, + }, + sessionStorage: { + getItem: () => null, + setItem: () => { }, + removeItem: () => { }, + clear: () => { }, + }, + setTimeout, + clearTimeout, + setInterval, + clearInterval, + innerWidth: 1920, + innerHeight: 1080, + outerWidth: 1920, + outerHeight: 1080, + scrollX: 0, + scrollY: 0, + pageXOffset: 0, + pageYOffset: 0, + scrollTo: () => { }, + scroll: () => { }, + Image: class Image { }, + HTMLElement: class HTMLElement { }, + SVGElement: class SVGElement { }, +}; + +// Only set globals if they don't exist (i.e., we're in Node.js) +if (typeof window === 'undefined') { + (global as any).window = windowMock; +} + +if (typeof document === 'undefined') { + (global as any).document = documentMock; +} + +if (typeof navigator === 'undefined') { + (global as any).navigator = windowMock.navigator; +} + +if (typeof screen === 'undefined') { + (global as any).screen = screenMock; +} + +if (typeof HTMLElement === 'undefined') { + (global as any).HTMLElement = windowMock.HTMLElement; +} + +if (typeof SVGElement === 'undefined') { + (global as any).SVGElement = windowMock.SVGElement; +} + +export { }; diff --git a/bizmatch/ssr-dom-preload.mjs b/bizmatch/ssr-dom-preload.mjs index 6123516..31450a0 100644 --- a/bizmatch/ssr-dom-preload.mjs +++ b/bizmatch/ssr-dom-preload.mjs @@ -1,154 +1,154 @@ -/** - * Node.js Preload Script for SSR Development - * - * This script creates DOM global mocks BEFORE any modules are loaded. - * It only applies in the main thread - NOT in worker threads (sass, esbuild). - */ - -import { isMainThread } from 'node:worker_threads'; - -// Only apply polyfills in the main thread, not in workers -if (!isMainThread) { - // Skip polyfills in worker threads to avoid breaking sass/esbuild - // console.log('[SSR] Skipping polyfills in worker thread'); -} else { - // Create screen mock - const screenMock = { - width: 1920, - height: 1080, - availWidth: 1920, - availHeight: 1080, - colorDepth: 24, - pixelDepth: 24, - deviceXDPI: 96, - deviceYDPI: 96, - logicalXDPI: 96, - logicalYDPI: 96, - }; - - // Create document mock - const documentMock = { - createElement: (tag) => ({ - style: {}, - setAttribute: () => { }, - getAttribute: () => null, - appendChild: () => { }, - removeChild: () => { }, - classList: { add: () => { }, remove: () => { }, contains: () => false }, - tagName: tag?.toUpperCase() || 'DIV', - }), - createElementNS: (ns, tag) => ({ - style: {}, - setAttribute: () => { }, - getAttribute: () => null, - appendChild: () => { }, - getBBox: () => ({ x: 0, y: 0, width: 0, height: 0 }), - tagName: tag?.toUpperCase() || 'SVG', - }), - createTextNode: () => ({}), - head: { appendChild: () => { }, removeChild: () => { } }, - body: { appendChild: () => { }, removeChild: () => { } }, - documentElement: { - style: {}, - clientWidth: 1920, - clientHeight: 1080, - querySelector: () => null, - querySelectorAll: () => [], - getAttribute: () => null, - setAttribute: () => { }, - }, - addEventListener: () => { }, - removeEventListener: () => { }, - querySelector: () => null, - querySelectorAll: () => [], - getElementById: () => null, - getElementsByTagName: () => [], - getElementsByClassName: () => [], - }; - - // Create window mock - const windowMock = { - requestAnimationFrame: (callback) => setTimeout(callback, 16), - cancelAnimationFrame: (id) => clearTimeout(id), - addEventListener: () => { }, - removeEventListener: () => { }, - getComputedStyle: () => ({ getPropertyValue: () => '' }), - matchMedia: () => ({ - matches: false, - addListener: () => { }, - removeListener: () => { }, - addEventListener: () => { }, - removeEventListener: () => { }, - }), - document: documentMock, - screen: screenMock, - devicePixelRatio: 1, - navigator: { - userAgent: 'node', - platform: 'server', - language: 'en', - languages: ['en'], - onLine: true, - geolocation: null, - }, - location: { - hostname: 'localhost', - href: 'http://localhost', - protocol: 'http:', - pathname: '/', - search: '', - hash: '', - host: 'localhost', - origin: 'http://localhost', - }, - history: { - pushState: () => { }, - replaceState: () => { }, - back: () => { }, - forward: () => { }, - go: () => { }, - length: 0, - }, - localStorage: { - getItem: () => null, - setItem: () => { }, - removeItem: () => { }, - clear: () => { }, - }, - sessionStorage: { - getItem: () => null, - setItem: () => { }, - removeItem: () => { }, - clear: () => { }, - }, - setTimeout, - clearTimeout, - setInterval, - clearInterval, - innerWidth: 1920, - innerHeight: 1080, - outerWidth: 1920, - outerHeight: 1080, - scrollX: 0, - scrollY: 0, - pageXOffset: 0, - pageYOffset: 0, - scrollTo: () => { }, - scroll: () => { }, - Image: class Image { }, - HTMLElement: class HTMLElement { }, - SVGElement: class SVGElement { }, - }; - - // Set globals - globalThis.window = windowMock; - globalThis.document = documentMock; - globalThis.navigator = windowMock.navigator; - globalThis.screen = screenMock; - globalThis.HTMLElement = windowMock.HTMLElement; - globalThis.SVGElement = windowMock.SVGElement; - globalThis.localStorage = windowMock.localStorage; - globalThis.sessionStorage = windowMock.sessionStorage; - - console.log('[SSR] DOM polyfills loaded'); -} +/** + * Node.js Preload Script for SSR Development + * + * This script creates DOM global mocks BEFORE any modules are loaded. + * It only applies in the main thread - NOT in worker threads (sass, esbuild). + */ + +import { isMainThread } from 'node:worker_threads'; + +// Only apply polyfills in the main thread, not in workers +if (!isMainThread) { + // Skip polyfills in worker threads to avoid breaking sass/esbuild + // console.log('[SSR] Skipping polyfills in worker thread'); +} else { + // Create screen mock + const screenMock = { + width: 1920, + height: 1080, + availWidth: 1920, + availHeight: 1080, + colorDepth: 24, + pixelDepth: 24, + deviceXDPI: 96, + deviceYDPI: 96, + logicalXDPI: 96, + logicalYDPI: 96, + }; + + // Create document mock + const documentMock = { + createElement: (tag) => ({ + style: {}, + setAttribute: () => { }, + getAttribute: () => null, + appendChild: () => { }, + removeChild: () => { }, + classList: { add: () => { }, remove: () => { }, contains: () => false }, + tagName: tag?.toUpperCase() || 'DIV', + }), + createElementNS: (ns, tag) => ({ + style: {}, + setAttribute: () => { }, + getAttribute: () => null, + appendChild: () => { }, + getBBox: () => ({ x: 0, y: 0, width: 0, height: 0 }), + tagName: tag?.toUpperCase() || 'SVG', + }), + createTextNode: () => ({}), + head: { appendChild: () => { }, removeChild: () => { } }, + body: { appendChild: () => { }, removeChild: () => { } }, + documentElement: { + style: {}, + clientWidth: 1920, + clientHeight: 1080, + querySelector: () => null, + querySelectorAll: () => [], + getAttribute: () => null, + setAttribute: () => { }, + }, + addEventListener: () => { }, + removeEventListener: () => { }, + querySelector: () => null, + querySelectorAll: () => [], + getElementById: () => null, + getElementsByTagName: () => [], + getElementsByClassName: () => [], + }; + + // Create window mock + const windowMock = { + requestAnimationFrame: (callback) => setTimeout(callback, 16), + cancelAnimationFrame: (id) => clearTimeout(id), + addEventListener: () => { }, + removeEventListener: () => { }, + getComputedStyle: () => ({ getPropertyValue: () => '' }), + matchMedia: () => ({ + matches: false, + addListener: () => { }, + removeListener: () => { }, + addEventListener: () => { }, + removeEventListener: () => { }, + }), + document: documentMock, + screen: screenMock, + devicePixelRatio: 1, + navigator: { + userAgent: 'node', + platform: 'server', + language: 'en', + languages: ['en'], + onLine: true, + geolocation: null, + }, + location: { + hostname: 'localhost', + href: 'http://localhost', + protocol: 'http:', + pathname: '/', + search: '', + hash: '', + host: 'localhost', + origin: 'http://localhost', + }, + history: { + pushState: () => { }, + replaceState: () => { }, + back: () => { }, + forward: () => { }, + go: () => { }, + length: 0, + }, + localStorage: { + getItem: () => null, + setItem: () => { }, + removeItem: () => { }, + clear: () => { }, + }, + sessionStorage: { + getItem: () => null, + setItem: () => { }, + removeItem: () => { }, + clear: () => { }, + }, + setTimeout, + clearTimeout, + setInterval, + clearInterval, + innerWidth: 1920, + innerHeight: 1080, + outerWidth: 1920, + outerHeight: 1080, + scrollX: 0, + scrollY: 0, + pageXOffset: 0, + pageYOffset: 0, + scrollTo: () => { }, + scroll: () => { }, + Image: class Image { }, + HTMLElement: class HTMLElement { }, + SVGElement: class SVGElement { }, + }; + + // Set globals + globalThis.window = windowMock; + globalThis.document = documentMock; + globalThis.navigator = windowMock.navigator; + globalThis.screen = screenMock; + globalThis.HTMLElement = windowMock.HTMLElement; + globalThis.SVGElement = windowMock.SVGElement; + globalThis.localStorage = windowMock.localStorage; + globalThis.sessionStorage = windowMock.sessionStorage; + + console.log('[SSR] DOM polyfills loaded'); +} diff --git a/fix-vulnerabilities.sh b/fix-vulnerabilities.sh old mode 100644 new mode 100755