mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-17 01:39:39 +02:00
style: auto-format codebase with Prettier
Applied formatting to 1487+ files using pnpm format:write - TypeScript/JavaScript files - Svelte components - Astro pages - JSON configs - Markdown docs 13 files still need manual review (Astro JSX comments)
This commit is contained in:
parent
0241f5554c
commit
d36b321d9d
3952 changed files with 661498 additions and 739751 deletions
|
|
@ -5,24 +5,27 @@ Die `links` Collection hat noch nicht die neuen Felder. Diese müssen **manuell
|
|||
## Schritt-für-Schritt Anleitung:
|
||||
|
||||
### 1. PocketBase Admin öffnen
|
||||
|
||||
Gehe zu: https://pb.ulo.ad/_/
|
||||
|
||||
### 2. Links Collection bearbeiten
|
||||
|
||||
- Navigiere zu: Collections → links
|
||||
- Klicke auf "Edit collection"
|
||||
|
||||
### 3. Diese Felder hinzufügen:
|
||||
|
||||
#### a) System-Timestamps (WICHTIGSTE!)
|
||||
|
||||
- **created** (Type: autodate)
|
||||
- onCreate: ✅ aktiviert
|
||||
- onUpdate: ❌ deaktiviert
|
||||
|
||||
- **updated** (Type: autodate)
|
||||
- onCreate: ✅ aktiviert
|
||||
- onUpdate: ✅ aktiviert
|
||||
|
||||
#### b) Neue Funktionsfelder
|
||||
|
||||
- **use_username** (Type: bool)
|
||||
- Default value: false
|
||||
- Required: No
|
||||
|
|
@ -37,6 +40,7 @@ Gehe zu: https://pb.ulo.ad/_/
|
|||
- Required: No
|
||||
|
||||
#### c) Marketing-Felder (optional)
|
||||
|
||||
- **utm_source** (Type: text)
|
||||
- Max length: 255
|
||||
- Required: No
|
||||
|
|
@ -50,6 +54,7 @@ Gehe zu: https://pb.ulo.ad/_/
|
|||
- Required: No
|
||||
|
||||
### 4. Speichern
|
||||
|
||||
Klicke auf "Save collection"
|
||||
|
||||
## Frontend-Anpassungen die bereits gemacht wurden:
|
||||
|
|
@ -61,6 +66,7 @@ Klicke auf "Save collection"
|
|||
## Was noch geprüft werden muss:
|
||||
|
||||
Nach dem Hinzufügen der Felder in PocketBase:
|
||||
|
||||
1. Teste das Erstellen eines neuen Links
|
||||
2. Prüfe ob created/updated gesetzt werden
|
||||
3. Teste die use_username Checkbox Funktion
|
||||
3. Teste die use_username Checkbox Funktion
|
||||
|
|
|
|||
|
|
@ -7,11 +7,13 @@ The application now supports automatic switching between development and product
|
|||
## Configuration
|
||||
|
||||
### Development Mode
|
||||
|
||||
- **Database URL**: `http://localhost:8090`
|
||||
- **Config File**: `.env` (for local development)
|
||||
- **MCP Config**: `.mcp.development.json`
|
||||
|
||||
### Production Mode
|
||||
|
||||
- **Database URL**: `https://pb.ulo.ad`
|
||||
- **Config File**: `.env.production`
|
||||
- **MCP Config**: `.mcp.json`
|
||||
|
|
@ -21,6 +23,7 @@ The application now supports automatic switching between development and product
|
|||
### Local Development
|
||||
|
||||
1. **Start the local PocketBase server**:
|
||||
|
||||
```bash
|
||||
npm run backend
|
||||
# or
|
||||
|
|
@ -28,11 +31,13 @@ cd backend && ./pocketbase serve
|
|||
```
|
||||
|
||||
2. **Start the development server**:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
3. **Or start both together**:
|
||||
|
||||
```bash
|
||||
npm run dev:all
|
||||
```
|
||||
|
|
@ -51,11 +56,13 @@ npm run preview
|
|||
## Switching Between Environments
|
||||
|
||||
### To use local database:
|
||||
|
||||
1. Ensure `.env` contains: `PUBLIC_POCKETBASE_URL=http://localhost:8090`
|
||||
2. Start local PocketBase: `npm run backend`
|
||||
3. Run development server: `npm run dev`
|
||||
|
||||
### To use production database (NOT RECOMMENDED for development):
|
||||
|
||||
1. Change `.env` to: `PUBLIC_POCKETBASE_URL=https://pb.ulo.ad`
|
||||
2. Restart development server: `npm run dev`
|
||||
|
||||
|
|
@ -82,16 +89,19 @@ Is Production: false
|
|||
## Troubleshooting
|
||||
|
||||
### Local PocketBase not starting
|
||||
|
||||
- Check if port 8090 is already in use
|
||||
- Ensure PocketBase binary is executable: `chmod +x backend/pocketbase`
|
||||
- Check logs: `cat pocketbase-dev.log`
|
||||
|
||||
### Still connecting to production
|
||||
|
||||
- Clear browser cache and restart dev server
|
||||
- Verify `.env` file contains correct URL
|
||||
- Check console output for which URL is being used
|
||||
|
||||
### MCP Server issues
|
||||
|
||||
- For local development, use `.mcp.development.json`
|
||||
- For production, use `.mcp.json`
|
||||
- Restart Claude Code after changing MCP configuration
|
||||
- Restart Claude Code after changing MCP configuration
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ The team invitation system is fully implemented and ready to send emails. To ena
|
|||
- Copy the API key
|
||||
|
||||
3. **Add to `.env`**
|
||||
|
||||
```env
|
||||
BREVO_API_KEY=your-api-key-here
|
||||
```
|
||||
|
|
@ -60,6 +61,7 @@ The team invitation system is fully implemented and ready to send emails. To ena
|
|||
3. **Get your API key**
|
||||
|
||||
4. **Add to `.env`**
|
||||
|
||||
```env
|
||||
RESEND_API_KEY=your-api-key-here
|
||||
```
|
||||
|
|
@ -71,6 +73,7 @@ The team invitation system is fully implemented and ready to send emails. To ena
|
|||
## Testing Email Setup
|
||||
|
||||
1. **Restart the dev server** (if you added environment variables)
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
|
@ -103,4 +106,4 @@ All templates are bilingual (German/English) and mobile-responsive.
|
|||
- **Gmail blocking?** Use App Password, not regular password
|
||||
- **Rate limits?** Free tiers have daily limits
|
||||
|
||||
For production, we recommend using PocketBase SMTP with a professional email service like SendGrid, Mailgun, or Amazon SES.
|
||||
For production, we recommend using PocketBase SMTP with a professional email service like SendGrid, Mailgun, or Amazon SES.
|
||||
|
|
|
|||
|
|
@ -1,41 +1,48 @@
|
|||
# 🚨 SOFORTIGE LÖSUNG: Created/Updated Felder hinzufügen
|
||||
|
||||
## Das Problem
|
||||
|
||||
Die `links` Collection hat keine `created` und `updated` Felder, deshalb zeigt die UI "N/A" an.
|
||||
|
||||
## ✅ EINFACHSTE LÖSUNG - Manuelle Schritte in PocketBase Admin:
|
||||
|
||||
### 1. PocketBase Admin öffnen
|
||||
|
||||
Gehe zu: **https://pb.ulo.ad/_/**
|
||||
|
||||
### 2. Links Collection bearbeiten
|
||||
### 2. Links Collection bearbeiten
|
||||
|
||||
1. Navigiere zu: **Collections → links**
|
||||
2. Klicke auf **"Edit collection"**
|
||||
|
||||
### 3. Diese 2 wichtigsten Felder hinzufügen:
|
||||
|
||||
#### a) **created** (Type: autodate) - WICHTIGSTE!
|
||||
|
||||
- **Field name**: `created`
|
||||
- **Type**: `autodate`
|
||||
- **Type**: `autodate`
|
||||
- **onCreate**: ✅ **aktiviert**
|
||||
- **onUpdate**: ❌ deaktiviert
|
||||
|
||||
#### b) **updated** (Type: autodate) - WICHTIGSTE!
|
||||
#### b) **updated** (Type: autodate) - WICHTIGSTE!
|
||||
|
||||
- **Field name**: `updated`
|
||||
- **Type**: `autodate`
|
||||
- **onCreate**: ✅ **aktiviert**
|
||||
- **onCreate**: ✅ **aktiviert**
|
||||
- **onUpdate**: ✅ **aktiviert**
|
||||
|
||||
### 4. Optional - Weitere nützliche Felder:
|
||||
|
||||
#### c) **use_username** (Type: bool)
|
||||
|
||||
- **Field name**: `use_username`
|
||||
- **Type**: `bool`
|
||||
- **Default value**: `false`
|
||||
- **Required**: No
|
||||
|
||||
#### d) **click_count** (Type: number)
|
||||
- **Field name**: `click_count`
|
||||
|
||||
- **Field name**: `click_count`
|
||||
- **Type**: `number`
|
||||
- **Default value**: `0`
|
||||
- **Only integers**: ✅
|
||||
|
|
@ -43,20 +50,25 @@ Gehe zu: **https://pb.ulo.ad/_/**
|
|||
- **Required**: No
|
||||
|
||||
### 5. Speichern
|
||||
|
||||
Klicke auf **"Save collection"**
|
||||
|
||||
## ✅ ERGEBNIS
|
||||
|
||||
Nach dem Hinzufügen der Felder:
|
||||
|
||||
- **Neue Links** bekommen automatisch Timestamps ✅
|
||||
- **UI zeigt Datum korrekt an** statt "N/A" ✅
|
||||
- **Alle anderen Features** funktionieren weiterhin ✅
|
||||
|
||||
## 🧹 Aufräumen (Optional)
|
||||
|
||||
Nach erfolgreicher Implementierung:
|
||||
|
||||
- Lösche die `links_improved` Collection (war nur ein Test)
|
||||
- Die Anwendung nutzt wieder die ursprüngliche `links` Collection
|
||||
|
||||
---
|
||||
|
||||
**⏱️ Zeitaufwand**: 2-3 Minuten
|
||||
**🎯 Resultat**: Problem komplett gelöst!
|
||||
**🎯 Resultat**: Problem komplett gelöst!
|
||||
|
|
|
|||
|
|
@ -3,42 +3,49 @@
|
|||
## 🎯 Übersicht
|
||||
|
||||
Wir haben jetzt **ZWEI separate MCP-Konfigurationen**:
|
||||
|
||||
- **Development**: Lokale PocketBase (`localhost:8090`)
|
||||
- **Production**: Cloud PocketBase (`pb.ulo.ad`)
|
||||
|
||||
## 📁 Konfigurationsdateien
|
||||
|
||||
### `.mcp.development.json` (Lokale Entwicklung)
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"pocketbase-dev": {
|
||||
"command": "node",
|
||||
"args": ["/Users/tillschneider/Documents/__00__Code/uload/mcp-servers/pocketbase-mcp/build/index.js"],
|
||||
"env": {
|
||||
"POCKETBASE_URL": "http://localhost:8090",
|
||||
"POCKETBASE_ADMIN_EMAIL": "till.schneider@memoro.ai",
|
||||
"POCKETBASE_ADMIN_PASSWORD": "p0ck3t-RAJ"
|
||||
}
|
||||
}
|
||||
}
|
||||
"mcpServers": {
|
||||
"pocketbase-dev": {
|
||||
"command": "node",
|
||||
"args": [
|
||||
"/Users/tillschneider/Documents/__00__Code/uload/mcp-servers/pocketbase-mcp/build/index.js"
|
||||
],
|
||||
"env": {
|
||||
"POCKETBASE_URL": "http://localhost:8090",
|
||||
"POCKETBASE_ADMIN_EMAIL": "till.schneider@memoro.ai",
|
||||
"POCKETBASE_ADMIN_PASSWORD": "p0ck3t-RAJ"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `.mcp.json` (Production)
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"pocketbase-prod": {
|
||||
"command": "node",
|
||||
"args": ["/Users/tillschneider/Documents/__00__Code/uload/mcp-servers/pocketbase-mcp/build/index.js"],
|
||||
"env": {
|
||||
"POCKETBASE_URL": "https://pb.ulo.ad",
|
||||
"POCKETBASE_ADMIN_EMAIL": "till.schneider@memoro.ai",
|
||||
"POCKETBASE_ADMIN_PASSWORD": "p0ck3t-RA1N" // Anderes Passwort!
|
||||
}
|
||||
}
|
||||
}
|
||||
"mcpServers": {
|
||||
"pocketbase-prod": {
|
||||
"command": "node",
|
||||
"args": [
|
||||
"/Users/tillschneider/Documents/__00__Code/uload/mcp-servers/pocketbase-mcp/build/index.js"
|
||||
],
|
||||
"env": {
|
||||
"POCKETBASE_URL": "https://pb.ulo.ad",
|
||||
"POCKETBASE_ADMIN_EMAIL": "till.schneider@memoro.ai",
|
||||
"POCKETBASE_ADMIN_PASSWORD": "p0ck3t-RA1N" // Anderes Passwort!
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -47,11 +54,13 @@ Wir haben jetzt **ZWEI separate MCP-Konfigurationen**:
|
|||
### Option 1: Config-Datei wechseln
|
||||
|
||||
**Für Development:**
|
||||
|
||||
1. Umbenennen: `.mcp.development.json` → `.mcp.json`
|
||||
2. Claude Desktop neu starten
|
||||
3. Du hast Zugriff auf lokale DB
|
||||
|
||||
**Für Production:**
|
||||
|
||||
1. Umbenennen zurück
|
||||
2. Claude Desktop neu starten
|
||||
3. Du hast Zugriff auf Prod DB
|
||||
|
|
@ -62,43 +71,49 @@ Beide Configs in einer `.mcp.json` kombinieren:
|
|||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"pocketbase-dev": {
|
||||
"command": "node",
|
||||
"args": ["/Users/tillschneider/Documents/__00__Code/uload/mcp-servers/pocketbase-mcp/build/index.js"],
|
||||
"env": {
|
||||
"POCKETBASE_URL": "http://localhost:8090",
|
||||
"POCKETBASE_ADMIN_EMAIL": "till.schneider@memoro.ai",
|
||||
"POCKETBASE_ADMIN_PASSWORD": "p0ck3t-RAJ"
|
||||
}
|
||||
},
|
||||
"pocketbase-prod": {
|
||||
"command": "node",
|
||||
"args": ["/Users/tillschneider/Documents/__00__Code/uload/mcp-servers/pocketbase-mcp/build/index.js"],
|
||||
"env": {
|
||||
"POCKETBASE_URL": "https://pb.ulo.ad",
|
||||
"POCKETBASE_ADMIN_EMAIL": "till.schneider@memoro.ai",
|
||||
"POCKETBASE_ADMIN_PASSWORD": "p0ck3t-RA1N"
|
||||
}
|
||||
},
|
||||
"stripe": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@stripe/mcp", "--tools=all"],
|
||||
"env": {
|
||||
"STRIPE_SECRET_KEY": "sk_test_..."
|
||||
}
|
||||
}
|
||||
}
|
||||
"mcpServers": {
|
||||
"pocketbase-dev": {
|
||||
"command": "node",
|
||||
"args": [
|
||||
"/Users/tillschneider/Documents/__00__Code/uload/mcp-servers/pocketbase-mcp/build/index.js"
|
||||
],
|
||||
"env": {
|
||||
"POCKETBASE_URL": "http://localhost:8090",
|
||||
"POCKETBASE_ADMIN_EMAIL": "till.schneider@memoro.ai",
|
||||
"POCKETBASE_ADMIN_PASSWORD": "p0ck3t-RAJ"
|
||||
}
|
||||
},
|
||||
"pocketbase-prod": {
|
||||
"command": "node",
|
||||
"args": [
|
||||
"/Users/tillschneider/Documents/__00__Code/uload/mcp-servers/pocketbase-mcp/build/index.js"
|
||||
],
|
||||
"env": {
|
||||
"POCKETBASE_URL": "https://pb.ulo.ad",
|
||||
"POCKETBASE_ADMIN_EMAIL": "till.schneider@memoro.ai",
|
||||
"POCKETBASE_ADMIN_PASSWORD": "p0ck3t-RA1N"
|
||||
}
|
||||
},
|
||||
"stripe": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@stripe/mcp", "--tools=all"],
|
||||
"env": {
|
||||
"STRIPE_SECRET_KEY": "sk_test_..."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Dann in Claude:
|
||||
|
||||
- `mcp__pocketbase-dev__*` für lokale DB
|
||||
- `mcp__pocketbase-prod__*` für Production DB
|
||||
|
||||
## 🔧 Setup für neue Session
|
||||
|
||||
### 1. Lokale PocketBase starten
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
./pocketbase serve
|
||||
|
|
@ -115,6 +130,7 @@ cd backend
|
|||
### 4. Collections erstellen mit MCP
|
||||
|
||||
In Claude kannst du dann:
|
||||
|
||||
```
|
||||
mcp__pocketbase-dev__create_collection
|
||||
```
|
||||
|
|
@ -123,13 +139,13 @@ Mit dem Schema aus `docs/COMPLETE-POCKETBASE-DEV-SETUP.md`
|
|||
|
||||
## ⚠️ Wichtige Unterschiede
|
||||
|
||||
| Aspekt | Development | Production |
|
||||
|--------|------------|------------|
|
||||
| URL | http://localhost:8090 | https://pb.ulo.ad |
|
||||
| Admin PW | p0ck3t-RAJ | p0ck3t-RA1N |
|
||||
| MCP Name | pocketbase-dev | pocketbase-prod |
|
||||
| Daten | Test-Daten | Echte Daten |
|
||||
| Änderungen | Sicher | Vorsichtig! |
|
||||
| Aspekt | Development | Production |
|
||||
| ---------- | --------------------- | ----------------- |
|
||||
| URL | http://localhost:8090 | https://pb.ulo.ad |
|
||||
| Admin PW | p0ck3t-RAJ | p0ck3t-RA1N |
|
||||
| MCP Name | pocketbase-dev | pocketbase-prod |
|
||||
| Daten | Test-Daten | Echte Daten |
|
||||
| Änderungen | Sicher | Vorsichtig! |
|
||||
|
||||
## 🎯 Vorteile
|
||||
|
||||
|
|
@ -152,4 +168,4 @@ Wenn du Schema-Änderungen von Prod nach Dev synchronisieren willst:
|
|||
1. Export aus Prod mit `mcp__pocketbase-prod__get_collection`
|
||||
2. Import in Dev mit `mcp__pocketbase-dev__create_collection`
|
||||
|
||||
Aber **NIEMALS** echte Daten kopieren!
|
||||
Aber **NIEMALS** echte Daten kopieren!
|
||||
|
|
|
|||
|
|
@ -70,10 +70,10 @@ export default defineConfig({
|
|||
plugins: [
|
||||
paraglide({
|
||||
project: './project.inlang',
|
||||
outdir: './src/paraglide' // Wichtig: Konsistenter Pfad!
|
||||
outdir: './src/paraglide', // Wichtig: Konsistenter Pfad!
|
||||
}),
|
||||
sveltekit()
|
||||
]
|
||||
sveltekit(),
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
|
|
@ -94,15 +94,15 @@ export const i18n = createI18n(runtime, {
|
|||
de: '/ueber-uns',
|
||||
fr: '/a-propos',
|
||||
it: '/chi-siamo',
|
||||
es: '/acerca-de'
|
||||
es: '/acerca-de',
|
||||
},
|
||||
'/dashboard': {
|
||||
de: '/dashboard',
|
||||
fr: '/tableau-de-bord',
|
||||
it: '/cruscotto',
|
||||
es: '/panel'
|
||||
}
|
||||
}
|
||||
es: '/panel',
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
|
|
@ -195,9 +195,9 @@ const config = {
|
|||
adapter: adapter(),
|
||||
alias: {
|
||||
$paraglide: './src/paraglide',
|
||||
'$paraglide/*': './src/paraglide/*'
|
||||
}
|
||||
}
|
||||
'$paraglide/*': './src/paraglide/*',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
|
@ -248,7 +248,7 @@ export default config;
|
|||
{ code: 'de', name: 'Deutsch', flag: '🇩🇪' },
|
||||
{ code: 'it', name: 'Italiano', flag: '🇮🇹' },
|
||||
{ code: 'fr', name: 'Français', flag: '🇫🇷' },
|
||||
{ code: 'es', name: 'Español', flag: '🇪🇸' }
|
||||
{ code: 'es', name: 'Español', flag: '🇪🇸' },
|
||||
];
|
||||
|
||||
let currentLanguage = $derived(
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ PocketBase Container (Port 8090) - Separat in Coolify
|
|||
## 📦 Was wurde geändert?
|
||||
|
||||
### Entfernte Komponenten:
|
||||
|
||||
- ❌ Embedded PocketBase im Container
|
||||
- ❌ Supervisor Process Manager
|
||||
- ❌ Docker Entrypoint Script
|
||||
|
|
@ -29,6 +30,7 @@ PocketBase Container (Port 8090) - Separat in Coolify
|
|||
- ❌ Volume für pb_data
|
||||
|
||||
### Neue Struktur:
|
||||
|
||||
- ✅ Schlanker Container nur mit SvelteKit
|
||||
- ✅ Externe PocketBase Verbindung via `PUBLIC_POCKETBASE_URL`
|
||||
- ✅ Direkte Node.js Ausführung (kein Supervisor)
|
||||
|
|
@ -37,6 +39,7 @@ PocketBase Container (Port 8090) - Separat in Coolify
|
|||
## 🔧 Deployment in Coolify
|
||||
|
||||
### 1. PocketBase Service (falls noch nicht vorhanden)
|
||||
|
||||
```yaml
|
||||
# Als separater Service in Coolify
|
||||
Service: PocketBase
|
||||
|
|
@ -45,13 +48,14 @@ Persistent Volume: /pb_data
|
|||
```
|
||||
|
||||
### 2. uLoad App Service
|
||||
|
||||
```yaml
|
||||
# docker-compose.coolify.yml verwenden
|
||||
Service: uLoad
|
||||
Port: 3000
|
||||
Build: Dockerfile
|
||||
Environment:
|
||||
- PUBLIC_POCKETBASE_URL=http://pocketbase:8090 # Interner Service Name
|
||||
- PUBLIC_POCKETBASE_URL=http://pocketbase:8090 # Interner Service Name
|
||||
- ORIGIN=https://ulo.ad
|
||||
- PUBLIC_UMAMI_URL=...
|
||||
- PUBLIC_UMAMI_WEBSITE_ID=...
|
||||
|
|
@ -62,6 +66,7 @@ Environment:
|
|||
### Für bestehende Deployments:
|
||||
|
||||
1. **PocketBase Daten sichern** (falls noch embedded):
|
||||
|
||||
```bash
|
||||
docker cp container_name:/app/pb_data ./pb_data_backup
|
||||
```
|
||||
|
|
@ -71,6 +76,7 @@ Environment:
|
|||
3. **Daten importieren** in neue PocketBase Instanz
|
||||
|
||||
4. **Environment Variables anpassen**:
|
||||
|
||||
```env
|
||||
PUBLIC_POCKETBASE_URL=http://pocketbase:8090
|
||||
```
|
||||
|
|
@ -93,6 +99,7 @@ PUBLIC_UMAMI_WEBSITE_ID=xxx-xxx-xxx
|
|||
## 🏗️ Build & Deploy
|
||||
|
||||
### Lokal testen:
|
||||
|
||||
```bash
|
||||
# Build
|
||||
docker build -t uload:latest .
|
||||
|
|
@ -105,6 +112,7 @@ docker run -p 3000:3000 \
|
|||
```
|
||||
|
||||
### Production Deploy:
|
||||
|
||||
```bash
|
||||
# Via Coolify mit docker-compose.coolify.yml
|
||||
# Oder direkt:
|
||||
|
|
@ -124,4 +132,4 @@ docker compose -f docker-compose.yml up -d
|
|||
- PocketBase Hooks (`pb_hooks/`) müssen manuell zur externen PocketBase migriert werden
|
||||
- Schema (`backend/pb_schema.json`) muss in der externen PocketBase importiert werden
|
||||
- Interne Coolify Networking nutzen (nicht über Public URL)
|
||||
- Health Check läuft nur auf Port 3000 (nicht mehr auf 8090)
|
||||
- Health Check läuft nur auf Port 3000 (nicht mehr auf 8090)
|
||||
|
|
|
|||
|
|
@ -3,12 +3,15 @@
|
|||
## Was wurde gemacht:
|
||||
|
||||
### 1. Redis lokal installiert
|
||||
|
||||
- Redis via Homebrew installiert: `brew install redis`
|
||||
- Redis-Service gestartet: `brew services start redis`
|
||||
- Läuft jetzt permanent im Hintergrund
|
||||
|
||||
### 2. Umgebungsvariablen konfiguriert
|
||||
|
||||
**Lokal (.env.development):**
|
||||
|
||||
```env
|
||||
REDIS_HOST=localhost
|
||||
REDIS_PORT=6379
|
||||
|
|
@ -16,6 +19,7 @@ REDIS_PASSWORD=
|
|||
```
|
||||
|
||||
**Produktion (.env):**
|
||||
|
||||
```env
|
||||
# Füge deine Coolify Redis-Daten hier ein:
|
||||
REDIS_HOST=your-redis-host
|
||||
|
|
@ -25,6 +29,7 @@ REDIS_PASSWORD=your-redis-password
|
|||
```
|
||||
|
||||
### 3. Robuster Fallback-Mechanismus
|
||||
|
||||
- App funktioniert auch OHNE Redis
|
||||
- Automatische Erkennung ob Redis verfügbar ist
|
||||
- Graceful degradation wenn Redis offline ist
|
||||
|
|
@ -32,6 +37,7 @@ REDIS_PASSWORD=your-redis-password
|
|||
## So prüfst du ob Redis funktioniert:
|
||||
|
||||
### 1. Status-Endpoint
|
||||
|
||||
```bash
|
||||
# Lokal
|
||||
curl http://localhost:5173/api/redis-status
|
||||
|
|
@ -41,15 +47,19 @@ curl https://ulo.ad/api/redis-status
|
|||
```
|
||||
|
||||
### 2. Console Logs
|
||||
|
||||
Beim App-Start siehst du:
|
||||
|
||||
- ✅ Redis: Connected successfully (wenn verbunden)
|
||||
- ⚠️ Redis: Disabled (wenn nicht konfiguriert)
|
||||
|
||||
### 3. Performance Test
|
||||
|
||||
1. Besuche einen Link zum ersten Mal → "Cache MISS"
|
||||
2. Besuche denselben Link nochmal → "Cache HIT!" (viel schneller!)
|
||||
|
||||
### 4. Redis CLI
|
||||
|
||||
```bash
|
||||
# Zeige alle gecachten Links
|
||||
redis-cli keys "redirect:*"
|
||||
|
|
@ -96,8 +106,8 @@ redis-cli flushall
|
|||
## Cache-Strategie:
|
||||
|
||||
- **Normale Links**: 5 Minuten Cache
|
||||
- **Populäre Links**: 24 Stunden Cache
|
||||
- **Populäre Links**: 24 Stunden Cache
|
||||
- **Passwort-geschützte Links**: Kein Cache
|
||||
- **Abgelaufene Links**: Kein Cache
|
||||
|
||||
Der Cache beschleunigt Redirects von ~100ms auf ~10ms! 🚀
|
||||
Der Cache beschleunigt Redirects von ~100ms auf ~10ms! 🚀
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ const config = {
|
|||
api: 'https://ulo.ad/api/collections/',
|
||||
|
||||
// Admin NUR über SSH oder separaten Port
|
||||
admin: 'Nicht öffentlich!'
|
||||
admin: 'Nicht öffentlich!',
|
||||
};
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -97,7 +97,7 @@ export function getVariantContent(variantId: string): VariantContent {
|
|||
name: 'Control',
|
||||
headline: m.hero_control_headline(),
|
||||
subheadline: m.hero_control_subheadline(),
|
||||
ctaText: m.hero_control_cta()
|
||||
ctaText: m.hero_control_cta(),
|
||||
// ...
|
||||
};
|
||||
case 'a1':
|
||||
|
|
@ -107,7 +107,7 @@ export function getVariantContent(variantId: string): VariantContent {
|
|||
headline: m.hero_a1_headline(),
|
||||
subheadline: m.hero_a1_subheadline(),
|
||||
ctaText: m.hero_a1_cta(),
|
||||
features: [m.hero_a1_feature_1(), m.hero_a1_feature_2(), m.hero_a1_feature_3()]
|
||||
features: [m.hero_a1_feature_1(), m.hero_a1_feature_2(), m.hero_a1_feature_3()],
|
||||
// ...
|
||||
};
|
||||
// ...
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -7,7 +7,7 @@ Basierend auf der Analyse empfehle ich die **PocketBase-Integration (Option 1)**
|
|||
## Projektziele
|
||||
|
||||
- ✅ Content Marketing Platform für SEO & Thought Leadership
|
||||
- ✅ Nahtlose Integration in bestehendes uload-System
|
||||
- ✅ Nahtlose Integration in bestehendes uload-System
|
||||
- ✅ Skalierbar für 100+ Artikel
|
||||
- ✅ DSGVO-konform
|
||||
- ✅ Mobile-optimiert
|
||||
|
|
@ -16,75 +16,90 @@ Basierend auf der Analyse empfehle ich die **PocketBase-Integration (Option 1)**
|
|||
## Timeline & Milestones
|
||||
|
||||
### Tag 1: Database & Backend Setup
|
||||
|
||||
**Ziel**: Komplette Datenbank-Struktur und API-Endpoints
|
||||
|
||||
#### Vormittag (4h)
|
||||
|
||||
- [ ] PocketBase Collections erstellen
|
||||
- [ ] Relationen definieren
|
||||
- [ ] Validation Rules setzen
|
||||
- [ ] Test-Daten einfügen
|
||||
|
||||
#### Nachmittag (4h)
|
||||
|
||||
- [ ] Server-Routes implementieren
|
||||
- [ ] API-Endpoints testen
|
||||
- [ ] Error Handling
|
||||
- [ ] Pagination Logic
|
||||
|
||||
### Tag 2: Frontend Basis-Komponenten
|
||||
|
||||
**Ziel**: Blog-Übersicht und Artikel-Ansicht funktionsfähig
|
||||
|
||||
#### Vormittag (4h)
|
||||
|
||||
- [ ] Blog-Übersichtsseite
|
||||
- [ ] BlogCard Komponente
|
||||
- [ ] Kategorie-Filter
|
||||
- [ ] Tag-Cloud
|
||||
|
||||
#### Nachmittag (4h)
|
||||
|
||||
- [ ] Artikel-Detailseite
|
||||
- [ ] Reading Progress Bar
|
||||
- [ ] Table of Contents
|
||||
- [ ] Share Buttons
|
||||
|
||||
### Tag 3: Admin-Interface
|
||||
|
||||
**Ziel**: Vollständiges CMS für Blog-Verwaltung
|
||||
|
||||
#### Vormittag (4h)
|
||||
|
||||
- [ ] Admin-Dashboard
|
||||
- [ ] Artikel-Liste mit Status
|
||||
- [ ] Bulk-Actions
|
||||
- [ ] Suchfunktion
|
||||
|
||||
#### Nachmittag (4h)
|
||||
|
||||
- [ ] Rich-Text Editor (Tiptap)
|
||||
- [ ] Media Upload
|
||||
- [ ] Preview-Funktion
|
||||
- [ ] Auto-Save
|
||||
|
||||
### Tag 4: SEO & Performance
|
||||
|
||||
**Ziel**: Optimale Sichtbarkeit und Geschwindigkeit
|
||||
|
||||
#### Vormittag (4h)
|
||||
|
||||
- [ ] Meta-Tags Management
|
||||
- [ ] Schema.org Markup
|
||||
- [ ] XML Sitemap
|
||||
- [ ] RSS/Atom Feed
|
||||
|
||||
#### Nachmittag (4h)
|
||||
|
||||
- [ ] Image Optimization
|
||||
- [ ] Lazy Loading
|
||||
- [ ] Cache-Strategy
|
||||
- [ ] CDN-Integration
|
||||
|
||||
### Tag 5: Features & Polish
|
||||
|
||||
**Ziel**: Premium-Features und finale Optimierungen
|
||||
|
||||
#### Vormittag (4h)
|
||||
|
||||
- [ ] Verwandte Artikel
|
||||
- [ ] Lesezeit-Berechnung
|
||||
- [ ] Newsletter-Integration
|
||||
- [ ] Social Media Auto-Post
|
||||
|
||||
#### Nachmittag (4h)
|
||||
|
||||
- [ ] Analytics Dashboard
|
||||
- [ ] A/B Testing Setup
|
||||
- [ ] Mobile Optimierung
|
||||
|
|
@ -146,21 +161,21 @@ CREATE TABLE blog_post_tags (
|
|||
|
||||
```typescript
|
||||
// Blog API Routes
|
||||
GET /api/blog/posts // Liste mit Pagination
|
||||
GET /api/blog/posts/[slug] // Einzelner Artikel
|
||||
POST /api/blog/posts/[slug]/view // View Counter
|
||||
GET /api/blog/categories // Alle Kategorien
|
||||
GET /api/blog/tags // Alle Tags
|
||||
GET /api/blog/search // Volltextsuche
|
||||
GET /api/blog/feed.xml // RSS Feed
|
||||
GET /api/blog/sitemap.xml // Sitemap
|
||||
GET / api / blog / posts; // Liste mit Pagination
|
||||
GET / api / blog / posts / [slug]; // Einzelner Artikel
|
||||
POST / api / blog / posts / [slug] / view; // View Counter
|
||||
GET / api / blog / categories; // Alle Kategorien
|
||||
GET / api / blog / tags; // Alle Tags
|
||||
GET / api / blog / search; // Volltextsuche
|
||||
GET / api / blog / feed.xml; // RSS Feed
|
||||
GET / api / blog / sitemap.xml; // Sitemap
|
||||
|
||||
// Admin API Routes (auth required)
|
||||
POST /api/admin/blog/posts // Neuer Artikel
|
||||
PUT /api/admin/blog/posts/[id] // Update
|
||||
DELETE /api/admin/blog/posts/[id] // Löschen
|
||||
POST /api/admin/blog/posts/[id]/publish // Veröffentlichen
|
||||
POST /api/admin/blog/upload // Bild-Upload
|
||||
POST / api / admin / blog / posts; // Neuer Artikel
|
||||
PUT / api / admin / blog / posts / [id]; // Update
|
||||
DELETE / api / admin / blog / posts / [id]; // Löschen
|
||||
POST / api / admin / blog / posts / [id] / publish; // Veröffentlichen
|
||||
POST / api / admin / blog / upload; // Bild-Upload
|
||||
```
|
||||
|
||||
### Komponenten-Architektur
|
||||
|
|
@ -212,18 +227,21 @@ src/lib/components/blog/
|
|||
### Content-Kalender (erste 3 Monate)
|
||||
|
||||
**Monat 1**: Foundations (4 Artikel/Monat)
|
||||
|
||||
- Woche 1: Psychology & UX
|
||||
- Woche 2: Technical Tutorial
|
||||
- Woche 3: Case Study
|
||||
- Woche 4: Industry Trends
|
||||
|
||||
**Monat 2**: Deep Dives (6 Artikel/Monat)
|
||||
|
||||
- Analytics Deep Dives
|
||||
- Integration Guides
|
||||
- Performance Optimization
|
||||
- Security Best Practices
|
||||
|
||||
**Monat 3**: Growth (8 Artikel/Monat)
|
||||
|
||||
- Guest Posts
|
||||
- User Success Stories
|
||||
- Feature Announcements
|
||||
|
|
@ -234,11 +252,13 @@ src/lib/components/blog/
|
|||
### Target Keywords
|
||||
|
||||
**Primary Keywords** (Schwierigkeit: Mittel)
|
||||
|
||||
- "url kürzen" (9.900 Suchen/Monat)
|
||||
- "link verkürzen" (6.600 Suchen/Monat)
|
||||
- "kurze urls" (2.400 Suchen/Monat)
|
||||
|
||||
**Long-Tail Keywords** (Schwierigkeit: Leicht)
|
||||
|
||||
- "kostenlos url kürzen ohne anmeldung"
|
||||
- "eigene domain für kurze links"
|
||||
- "qr code mit logo erstellen"
|
||||
|
|
@ -260,12 +280,14 @@ src/lib/components/blog/
|
|||
## Performance KPIs
|
||||
|
||||
### Technical Metrics
|
||||
|
||||
- **Page Load**: < 2s (Mobile 3G)
|
||||
- **Time to Interactive**: < 3.5s
|
||||
- **Core Web Vitals**: Alle grün
|
||||
- **Lighthouse Score**: > 90
|
||||
|
||||
### Business Metrics
|
||||
|
||||
- **Organic Traffic**: +50% in 3 Monaten
|
||||
- **Conversion Rate**: Blog → Sign-up > 3%
|
||||
- **Engagement Rate**: > 2 Min Average Time
|
||||
|
|
@ -275,26 +297,29 @@ src/lib/components/blog/
|
|||
|
||||
### Potenzielle Risiken & Mitigationen
|
||||
|
||||
| Risiko | Wahrscheinlichkeit | Impact | Mitigation |
|
||||
|--------|-------------------|---------|------------|
|
||||
| Editor-Komplexität | Mittel | Hoch | Start mit SimpleMDE, später Upgrade |
|
||||
| Performance-Probleme | Niedrig | Mittel | Caching-Strategy von Anfang an |
|
||||
| Content-Erstellung | Hoch | Mittel | Freelancer-Pool aufbauen |
|
||||
| SEO-Konkurrenz | Mittel | Niedrig | Nischen-Keywords fokussieren |
|
||||
| Risiko | Wahrscheinlichkeit | Impact | Mitigation |
|
||||
| -------------------- | ------------------ | ------- | ----------------------------------- |
|
||||
| Editor-Komplexität | Mittel | Hoch | Start mit SimpleMDE, später Upgrade |
|
||||
| Performance-Probleme | Niedrig | Mittel | Caching-Strategy von Anfang an |
|
||||
| Content-Erstellung | Hoch | Mittel | Freelancer-Pool aufbauen |
|
||||
| SEO-Konkurrenz | Mittel | Niedrig | Nischen-Keywords fokussieren |
|
||||
|
||||
## Budget & Ressourcen
|
||||
|
||||
### Entwicklung (5 Tage)
|
||||
|
||||
- **Developer**: 5 Tage × 8h = 40h
|
||||
- **Design Assets**: Vorhandene UI-Komponenten nutzen
|
||||
- **Testing**: 1 Tag zusätzlich
|
||||
|
||||
### Content (Ongoing)
|
||||
|
||||
- **Launch Content**: 5 Artikel (intern)
|
||||
- **Monthly Content**: 4-8 Artikel
|
||||
- **Freelancer Budget**: €500-1000/Monat
|
||||
|
||||
### Tools & Services
|
||||
|
||||
- **Grammarly**: Rechtschreibprüfung
|
||||
- **Canva Pro**: Grafiken
|
||||
- **Unsplash+**: Stock-Fotos
|
||||
|
|
@ -305,6 +330,7 @@ src/lib/components/blog/
|
|||
### Pre-Launch Checklist
|
||||
|
||||
#### Funktionalität
|
||||
|
||||
- [ ] Alle CRUD-Operationen funktionieren
|
||||
- [ ] Pagination arbeitet korrekt
|
||||
- [ ] Suche liefert relevante Ergebnisse
|
||||
|
|
@ -312,24 +338,28 @@ src/lib/components/blog/
|
|||
- [ ] RSS Feed validiert
|
||||
|
||||
#### Performance
|
||||
|
||||
- [ ] Lighthouse Audit > 90
|
||||
- [ ] Mobile-Responsive
|
||||
- [ ] Bilder optimiert
|
||||
- [ ] Caching aktiviert
|
||||
|
||||
#### SEO
|
||||
|
||||
- [ ] Meta-Tags vollständig
|
||||
- [ ] Sitemap generiert
|
||||
- [ ] Schema.org implementiert
|
||||
- [ ] Social Cards funktionieren
|
||||
|
||||
#### Security
|
||||
|
||||
- [ ] Input-Validation
|
||||
- [ ] XSS-Protection
|
||||
- [ ] CSRF-Token
|
||||
- [ ] Rate Limiting
|
||||
|
||||
#### Accessibility
|
||||
|
||||
- [ ] WCAG 2.1 AA konform
|
||||
- [ ] Keyboard-Navigation
|
||||
- [ ] Screen-Reader kompatibel
|
||||
|
|
@ -338,18 +368,21 @@ src/lib/components/blog/
|
|||
## Post-Launch Plan
|
||||
|
||||
### Woche 1
|
||||
|
||||
- Monitoring Setup
|
||||
- Bug Fixes
|
||||
- Performance Tuning
|
||||
- Erste Analytics
|
||||
|
||||
### Monat 1
|
||||
|
||||
- Content-Pipeline etablieren
|
||||
- SEO-Optimierungen
|
||||
- User-Feedback sammeln
|
||||
- A/B Tests starten
|
||||
|
||||
### Quartal 1
|
||||
|
||||
- Feature-Erweiterungen
|
||||
- Newsletter-Integration
|
||||
- Kommentar-System
|
||||
|
|
@ -358,16 +391,19 @@ src/lib/components/blog/
|
|||
## Success Metrics
|
||||
|
||||
### Launch (Tag 1)
|
||||
|
||||
- ✅ Blog live und funktional
|
||||
- ✅ 5 Launch-Artikel online
|
||||
- ✅ Keine kritischen Bugs
|
||||
|
||||
### Monat 1
|
||||
|
||||
- ✅ 20+ Blog-Artikel
|
||||
- ✅ 1000+ Unique Visitors
|
||||
- ✅ 5+ Backlinks
|
||||
|
||||
### Quartal 1
|
||||
|
||||
- ✅ 50+ Blog-Artikel
|
||||
- ✅ 10.000+ Monthly Visitors
|
||||
- ✅ Top 10 Rankings für Target Keywords
|
||||
|
|
@ -394,4 +430,4 @@ src/lib/components/blog/
|
|||
|
||||
**Empfehlung**: Sofortiger Start mit der PocketBase-Integration. Der vorgeschlagene 5-Tage-Plan ist realistisch und liefert ein production-ready Blog-System, das perfekt in die bestehende uload-Architektur passt.
|
||||
|
||||
**Kontakt für Rückfragen**: Bei Fragen zur Implementierung oder für Detail-Diskussionen stehe ich zur Verfügung.
|
||||
**Kontakt für Rückfragen**: Bei Fragen zur Implementierung oder für Detail-Diskussionen stehe ich zur Verfügung.
|
||||
|
|
|
|||
|
|
@ -3,7 +3,9 @@
|
|||
## Übersicht: 3 Ansätze für statische Markdown-Blogs
|
||||
|
||||
### 1. **mdsvex** - Markdown als Svelte-Komponenten (Empfohlen)
|
||||
### 2. **Vite Glob Import** - Dynamisches Laden zur Build-Zeit
|
||||
|
||||
### 2. **Vite Glob Import** - Dynamisches Laden zur Build-Zeit
|
||||
|
||||
### 3. **Content Collections** - Strukturierte Markdown-Verwaltung
|
||||
|
||||
---
|
||||
|
|
@ -26,7 +28,7 @@ import { sveltekit } from '@sveltejs/kit/vite';
|
|||
import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [sveltekit()]
|
||||
plugins: [sveltekit()],
|
||||
});
|
||||
```
|
||||
|
||||
|
|
@ -41,23 +43,20 @@ import rehypeAutolinkHeadings from 'rehype-autolink-headings';
|
|||
|
||||
/** @type {import('mdsvex').MdsvexOptions} */
|
||||
const mdsvexOptions = {
|
||||
extensions: ['.md', '.mdx'],
|
||||
layout: {
|
||||
blog: './src/lib/layouts/BlogLayout.svelte'
|
||||
},
|
||||
rehypePlugins: [
|
||||
rehypeSlug,
|
||||
[rehypeAutolinkHeadings, { behavior: 'wrap' }]
|
||||
]
|
||||
extensions: ['.md', '.mdx'],
|
||||
layout: {
|
||||
blog: './src/lib/layouts/BlogLayout.svelte',
|
||||
},
|
||||
rehypePlugins: [rehypeSlug, [rehypeAutolinkHeadings, { behavior: 'wrap' }]],
|
||||
};
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
const config = {
|
||||
extensions: ['.svelte', '.md', '.mdx'],
|
||||
preprocess: [vitePreprocess(), mdsvex(mdsvexOptions)],
|
||||
kit: {
|
||||
adapter: adapter()
|
||||
}
|
||||
extensions: ['.svelte', '.md', '.mdx'],
|
||||
preprocess: [vitePreprocess(), mdsvex(mdsvexOptions)],
|
||||
kit: {
|
||||
adapter: adapter(),
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
|
@ -68,54 +67,54 @@ export default config;
|
|||
```svelte
|
||||
<!-- src/lib/layouts/BlogLayout.svelte -->
|
||||
<script>
|
||||
export let title = '';
|
||||
export let date = '';
|
||||
export let author = '';
|
||||
export let excerpt = '';
|
||||
export let tags = [];
|
||||
export let image = '';
|
||||
|
||||
let formattedDate = $derived(
|
||||
new Date(date).toLocaleDateString('de-DE', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
})
|
||||
);
|
||||
export let title = '';
|
||||
export let date = '';
|
||||
export let author = '';
|
||||
export let excerpt = '';
|
||||
export let tags = [];
|
||||
export let image = '';
|
||||
|
||||
let formattedDate = $derived(
|
||||
new Date(date).toLocaleDateString('de-DE', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
})
|
||||
);
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{title} | uload Blog</title>
|
||||
<meta name="description" content={excerpt} />
|
||||
<meta property="og:title" content={title} />
|
||||
<meta property="og:description" content={excerpt} />
|
||||
{#if image}
|
||||
<meta property="og:image" content={image} />
|
||||
{/if}
|
||||
<title>{title} | uload Blog</title>
|
||||
<meta name="description" content={excerpt} />
|
||||
<meta property="og:title" content={title} />
|
||||
<meta property="og:description" content={excerpt} />
|
||||
{#if image}
|
||||
<meta property="og:image" content={image} />
|
||||
{/if}
|
||||
</svelte:head>
|
||||
|
||||
<article class="prose prose-lg mx-auto px-4 py-8 max-w-4xl">
|
||||
<header class="mb-8">
|
||||
<h1 class="text-4xl font-bold mb-2">{title}</h1>
|
||||
<div class="text-gray-600 flex gap-4 items-center">
|
||||
<time datetime={date}>{formattedDate}</time>
|
||||
<span>•</span>
|
||||
<span>{author}</span>
|
||||
</div>
|
||||
{#if tags.length > 0}
|
||||
<div class="flex gap-2 mt-4">
|
||||
{#each tags as tag}
|
||||
<span class="bg-gray-100 px-3 py-1 rounded-full text-sm">
|
||||
{tag}
|
||||
</span>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</header>
|
||||
|
||||
<div class="content">
|
||||
<slot />
|
||||
</div>
|
||||
<header class="mb-8">
|
||||
<h1 class="text-4xl font-bold mb-2">{title}</h1>
|
||||
<div class="text-gray-600 flex gap-4 items-center">
|
||||
<time datetime={date}>{formattedDate}</time>
|
||||
<span>•</span>
|
||||
<span>{author}</span>
|
||||
</div>
|
||||
{#if tags.length > 0}
|
||||
<div class="flex gap-2 mt-4">
|
||||
{#each tags as tag}
|
||||
<span class="bg-gray-100 px-3 py-1 rounded-full text-sm">
|
||||
{tag}
|
||||
</span>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</header>
|
||||
|
||||
<div class="content">
|
||||
<slot />
|
||||
</div>
|
||||
</article>
|
||||
```
|
||||
|
||||
|
|
@ -162,52 +161,52 @@ src/routes/
|
|||
```javascript
|
||||
// src/routes/blog/+page.js
|
||||
export async function load() {
|
||||
const posts = import.meta.glob('/src/content/blog/*.md');
|
||||
const postPromises = Object.entries(posts).map(async ([path, resolver]) => {
|
||||
const { metadata } = await resolver();
|
||||
const slug = path.split('/').pop().replace('.md', '');
|
||||
|
||||
return {
|
||||
slug,
|
||||
...metadata
|
||||
};
|
||||
});
|
||||
|
||||
const allPosts = await Promise.all(postPromises);
|
||||
|
||||
// Nach Datum sortieren
|
||||
allPosts.sort((a, b) => new Date(b.date) - new Date(a.date));
|
||||
|
||||
return {
|
||||
posts: allPosts
|
||||
};
|
||||
const posts = import.meta.glob('/src/content/blog/*.md');
|
||||
const postPromises = Object.entries(posts).map(async ([path, resolver]) => {
|
||||
const { metadata } = await resolver();
|
||||
const slug = path.split('/').pop().replace('.md', '');
|
||||
|
||||
return {
|
||||
slug,
|
||||
...metadata,
|
||||
};
|
||||
});
|
||||
|
||||
const allPosts = await Promise.all(postPromises);
|
||||
|
||||
// Nach Datum sortieren
|
||||
allPosts.sort((a, b) => new Date(b.date) - new Date(a.date));
|
||||
|
||||
return {
|
||||
posts: allPosts,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
```svelte
|
||||
<!-- src/routes/blog/+page.svelte -->
|
||||
<script>
|
||||
let { data } = $props();
|
||||
let { data } = $props();
|
||||
</script>
|
||||
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<h1 class="text-3xl font-bold mb-8">Blog</h1>
|
||||
|
||||
<div class="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
{#each data.posts as post}
|
||||
<article class="border rounded-lg p-6 hover:shadow-lg transition">
|
||||
<h2 class="text-xl font-semibold mb-2">
|
||||
<a href="/blog/{post.slug}" class="hover:text-blue-600">
|
||||
{post.title}
|
||||
</a>
|
||||
</h2>
|
||||
<p class="text-gray-600 mb-4">{post.excerpt}</p>
|
||||
<div class="text-sm text-gray-500">
|
||||
{new Date(post.date).toLocaleDateString('de-DE')}
|
||||
</div>
|
||||
</article>
|
||||
{/each}
|
||||
</div>
|
||||
<h1 class="text-3xl font-bold mb-8">Blog</h1>
|
||||
|
||||
<div class="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
{#each data.posts as post}
|
||||
<article class="border rounded-lg p-6 hover:shadow-lg transition">
|
||||
<h2 class="text-xl font-semibold mb-2">
|
||||
<a href="/blog/{post.slug}" class="hover:text-blue-600">
|
||||
{post.title}
|
||||
</a>
|
||||
</h2>
|
||||
<p class="text-gray-600 mb-4">{post.excerpt}</p>
|
||||
<div class="text-sm text-gray-500">
|
||||
{new Date(post.date).toLocaleDateString('de-DE')}
|
||||
</div>
|
||||
</article>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
|
|
@ -231,13 +230,13 @@ import { marked } from 'marked';
|
|||
import matter from 'gray-matter';
|
||||
|
||||
export function parseMarkdown(content) {
|
||||
const { data, content: markdown } = matter(content);
|
||||
const html = marked(markdown);
|
||||
|
||||
return {
|
||||
metadata: data,
|
||||
html
|
||||
};
|
||||
const { data, content: markdown } = matter(content);
|
||||
const html = marked(markdown);
|
||||
|
||||
return {
|
||||
metadata: data,
|
||||
html,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -248,28 +247,28 @@ export function parseMarkdown(content) {
|
|||
import { parseMarkdown } from '$lib/utils/markdown';
|
||||
|
||||
export async function load() {
|
||||
const postFiles = import.meta.glob('/src/content/blog/*.md', {
|
||||
query: '?raw',
|
||||
import: 'default'
|
||||
});
|
||||
|
||||
const posts = await Promise.all(
|
||||
Object.entries(postFiles).map(async ([path, resolver]) => {
|
||||
const content = await resolver();
|
||||
const { metadata, html } = parseMarkdown(content);
|
||||
const slug = path.split('/').pop().replace('.md', '');
|
||||
|
||||
return {
|
||||
slug,
|
||||
...metadata,
|
||||
content: html
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
posts: posts.sort((a, b) => new Date(b.date) - new Date(a.date))
|
||||
};
|
||||
const postFiles = import.meta.glob('/src/content/blog/*.md', {
|
||||
query: '?raw',
|
||||
import: 'default',
|
||||
});
|
||||
|
||||
const posts = await Promise.all(
|
||||
Object.entries(postFiles).map(async ([path, resolver]) => {
|
||||
const content = await resolver();
|
||||
const { metadata, html } = parseMarkdown(content);
|
||||
const slug = path.split('/').pop().replace('.md', '');
|
||||
|
||||
return {
|
||||
slug,
|
||||
...metadata,
|
||||
content: html,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
posts: posts.sort((a, b) => new Date(b.date) - new Date(a.date)),
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -281,37 +280,37 @@ import { parseMarkdown } from '$lib/utils/markdown';
|
|||
import { error } from '@sveltejs/kit';
|
||||
|
||||
export async function load({ params }) {
|
||||
try {
|
||||
const post = await import(`../../../content/blog/${params.slug}.md?raw`);
|
||||
const { metadata, html } = parseMarkdown(post.default);
|
||||
|
||||
return {
|
||||
...metadata,
|
||||
content: html,
|
||||
slug: params.slug
|
||||
};
|
||||
} catch (e) {
|
||||
throw error(404, 'Post nicht gefunden');
|
||||
}
|
||||
try {
|
||||
const post = await import(`../../../content/blog/${params.slug}.md?raw`);
|
||||
const { metadata, html } = parseMarkdown(post.default);
|
||||
|
||||
return {
|
||||
...metadata,
|
||||
content: html,
|
||||
slug: params.slug,
|
||||
};
|
||||
} catch (e) {
|
||||
throw error(404, 'Post nicht gefunden');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```svelte
|
||||
<!-- src/routes/blog/[slug]/+page.svelte -->
|
||||
<script>
|
||||
let { data } = $props();
|
||||
let { data } = $props();
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{data.title}</title>
|
||||
<meta name="description" content={data.excerpt} />
|
||||
<title>{data.title}</title>
|
||||
<meta name="description" content={data.excerpt} />
|
||||
</svelte:head>
|
||||
|
||||
<article class="prose mx-auto px-4 py-8">
|
||||
<h1>{data.title}</h1>
|
||||
<time>{new Date(data.date).toLocaleDateString('de-DE')}</time>
|
||||
|
||||
{@html data.content}
|
||||
<h1>{data.title}</h1>
|
||||
<time>{new Date(data.date).toLocaleDateString('de-DE')}</time>
|
||||
|
||||
{@html data.content}
|
||||
</article>
|
||||
```
|
||||
|
||||
|
|
@ -340,20 +339,20 @@ src/content/
|
|||
import { z } from 'zod';
|
||||
|
||||
export const blogSchema = z.object({
|
||||
title: z.string(),
|
||||
date: z.date(),
|
||||
author: z.string(),
|
||||
excerpt: z.string(),
|
||||
tags: z.array(z.string()),
|
||||
image: z.string().optional(),
|
||||
draft: z.boolean().default(false)
|
||||
title: z.string(),
|
||||
date: z.date(),
|
||||
author: z.string(),
|
||||
excerpt: z.string(),
|
||||
tags: z.array(z.string()),
|
||||
image: z.string().optional(),
|
||||
draft: z.boolean().default(false),
|
||||
});
|
||||
|
||||
export const collections = {
|
||||
blog: {
|
||||
schema: blogSchema,
|
||||
directory: 'src/content/blog'
|
||||
}
|
||||
blog: {
|
||||
schema: blogSchema,
|
||||
directory: 'src/content/blog',
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
|
|
@ -366,45 +365,45 @@ import matter from 'gray-matter';
|
|||
import { marked } from 'marked';
|
||||
|
||||
export async function getCollection(collection) {
|
||||
const posts = import.meta.glob('/src/content/blog/*.md', {
|
||||
query: '?raw',
|
||||
import: 'default'
|
||||
});
|
||||
|
||||
const entries = await Promise.all(
|
||||
Object.entries(posts).map(async ([path, resolver]) => {
|
||||
// Drafts überspringen
|
||||
if (path.includes('_drafts')) return null;
|
||||
|
||||
const content = await resolver();
|
||||
const { data, content: markdown } = matter(content);
|
||||
|
||||
// Schema validieren
|
||||
const metadata = blogSchema.parse({
|
||||
...data,
|
||||
date: new Date(data.date)
|
||||
});
|
||||
|
||||
// Draft-Posts in Production ausblenden
|
||||
if (metadata.draft && import.meta.env.PROD) return null;
|
||||
|
||||
const slug = path.split('/').pop().replace('.md', '');
|
||||
const html = marked(markdown);
|
||||
|
||||
return {
|
||||
slug,
|
||||
...metadata,
|
||||
content: html
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return entries.filter(Boolean);
|
||||
const posts = import.meta.glob('/src/content/blog/*.md', {
|
||||
query: '?raw',
|
||||
import: 'default',
|
||||
});
|
||||
|
||||
const entries = await Promise.all(
|
||||
Object.entries(posts).map(async ([path, resolver]) => {
|
||||
// Drafts überspringen
|
||||
if (path.includes('_drafts')) return null;
|
||||
|
||||
const content = await resolver();
|
||||
const { data, content: markdown } = matter(content);
|
||||
|
||||
// Schema validieren
|
||||
const metadata = blogSchema.parse({
|
||||
...data,
|
||||
date: new Date(data.date),
|
||||
});
|
||||
|
||||
// Draft-Posts in Production ausblenden
|
||||
if (metadata.draft && import.meta.env.PROD) return null;
|
||||
|
||||
const slug = path.split('/').pop().replace('.md', '');
|
||||
const html = marked(markdown);
|
||||
|
||||
return {
|
||||
slug,
|
||||
...metadata,
|
||||
content: html,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return entries.filter(Boolean);
|
||||
}
|
||||
|
||||
export async function getEntry(collection, slug) {
|
||||
const posts = await getCollection(collection);
|
||||
return posts.find(post => post.slug === slug);
|
||||
const posts = await getCollection(collection);
|
||||
return posts.find((post) => post.slug === slug);
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -419,31 +418,35 @@ export async function getEntry(collection, slug) {
|
|||
import { getCollection } from '$lib/content';
|
||||
|
||||
export async function GET() {
|
||||
const posts = await getCollection('blog');
|
||||
const site = 'https://ulo.ad';
|
||||
|
||||
const xml = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
const posts = await getCollection('blog');
|
||||
const site = 'https://ulo.ad';
|
||||
|
||||
const xml = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<rss version="2.0">
|
||||
<channel>
|
||||
<title>uload Blog</title>
|
||||
<link>${site}/blog</link>
|
||||
<description>Insights über URLs, Marketing und Psychologie</description>
|
||||
${posts.map(post => `
|
||||
${posts
|
||||
.map(
|
||||
(post) => `
|
||||
<item>
|
||||
<title>${post.title}</title>
|
||||
<link>${site}/blog/${post.slug}</link>
|
||||
<description>${post.excerpt}</description>
|
||||
<pubDate>${new Date(post.date).toUTCString()}</pubDate>
|
||||
</item>
|
||||
`).join('')}
|
||||
`
|
||||
)
|
||||
.join('')}
|
||||
</channel>
|
||||
</rss>`;
|
||||
|
||||
return new Response(xml, {
|
||||
headers: {
|
||||
'Content-Type': 'application/xml'
|
||||
}
|
||||
});
|
||||
|
||||
return new Response(xml, {
|
||||
headers: {
|
||||
'Content-Type': 'application/xml',
|
||||
},
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -452,26 +455,30 @@ export async function GET() {
|
|||
```javascript
|
||||
// src/routes/sitemap.xml/+server.js
|
||||
export async function GET() {
|
||||
const posts = await getCollection('blog');
|
||||
const site = 'https://ulo.ad';
|
||||
|
||||
const xml = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
const posts = await getCollection('blog');
|
||||
const site = 'https://ulo.ad';
|
||||
|
||||
const xml = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
${posts.map(post => `
|
||||
${posts
|
||||
.map(
|
||||
(post) => `
|
||||
<url>
|
||||
<loc>${site}/blog/${post.slug}</loc>
|
||||
<lastmod>${new Date(post.date).toISOString()}</lastmod>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
`).join('')}
|
||||
`
|
||||
)
|
||||
.join('')}
|
||||
</urlset>`;
|
||||
|
||||
return new Response(xml, {
|
||||
headers: {
|
||||
'Content-Type': 'application/xml'
|
||||
}
|
||||
});
|
||||
|
||||
return new Response(xml, {
|
||||
headers: {
|
||||
'Content-Type': 'application/xml',
|
||||
},
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -486,10 +493,10 @@ npm install -D shiki
|
|||
import { codeToHtml } from 'shiki';
|
||||
|
||||
export async function highlightCode(code, lang = 'javascript') {
|
||||
return await codeToHtml(code, {
|
||||
lang,
|
||||
theme: 'github-dark'
|
||||
});
|
||||
return await codeToHtml(code, {
|
||||
lang,
|
||||
theme: 'github-dark',
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -498,15 +505,15 @@ export async function highlightCode(code, lang = 'javascript') {
|
|||
```javascript
|
||||
// src/lib/utils/reading-time.js
|
||||
export function calculateReadingTime(content) {
|
||||
const wordsPerMinute = 200;
|
||||
const words = content.split(/\s+/).length;
|
||||
const minutes = Math.ceil(words / wordsPerMinute);
|
||||
|
||||
return {
|
||||
minutes,
|
||||
words,
|
||||
text: `${minutes} Min. Lesezeit`
|
||||
};
|
||||
const wordsPerMinute = 200;
|
||||
const words = content.split(/\s+/).length;
|
||||
const minutes = Math.ceil(words / wordsPerMinute);
|
||||
|
||||
return {
|
||||
minutes,
|
||||
words,
|
||||
text: `${minutes} Min. Lesezeit`,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -515,19 +522,19 @@ export function calculateReadingTime(content) {
|
|||
```javascript
|
||||
// src/lib/utils/toc.js
|
||||
export function extractHeadings(html) {
|
||||
const regex = /<h([2-3])[^>]*id="([^"]*)"[^>]*>(.*?)<\/h\1>/g;
|
||||
const headings = [];
|
||||
let match;
|
||||
|
||||
while ((match = regex.exec(html)) !== null) {
|
||||
headings.push({
|
||||
level: parseInt(match[1]),
|
||||
id: match[2],
|
||||
text: match[3].replace(/<[^>]*>/g, '')
|
||||
});
|
||||
}
|
||||
|
||||
return headings;
|
||||
const regex = /<h([2-3])[^>]*id="([^"]*)"[^>]*>(.*?)<\/h\1>/g;
|
||||
const headings = [];
|
||||
let match;
|
||||
|
||||
while ((match = regex.exec(html)) !== null) {
|
||||
headings.push({
|
||||
level: parseInt(match[1]),
|
||||
id: match[2],
|
||||
text: match[3].replace(/<[^>]*>/g, ''),
|
||||
});
|
||||
}
|
||||
|
||||
return headings;
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -546,17 +553,17 @@ export const prerender = true; // Statisch zur Build-Zeit generieren
|
|||
|
||||
```svelte
|
||||
<script>
|
||||
import { inview } from 'svelte-inview';
|
||||
|
||||
let isInView = $state(false);
|
||||
import { inview } from 'svelte-inview';
|
||||
|
||||
let isInView = $state(false);
|
||||
</script>
|
||||
|
||||
<div use:inview on:inview_enter={() => isInView = true}>
|
||||
{#if isInView}
|
||||
<img src={image} alt={alt} loading="lazy" />
|
||||
{:else}
|
||||
<div class="skeleton h-64 bg-gray-200" />
|
||||
{/if}
|
||||
<div use:inview on:inview_enter={() => (isInView = true)}>
|
||||
{#if isInView}
|
||||
<img src={image} {alt} loading="lazy" />
|
||||
{:else}
|
||||
<div class="skeleton h-64 bg-gray-200" />
|
||||
{/if}
|
||||
</div>
|
||||
```
|
||||
|
||||
|
|
@ -568,16 +575,16 @@ const cache = new Map();
|
|||
const CACHE_DURATION = 1000 * 60 * 5; // 5 Minuten
|
||||
|
||||
export function getCached(key, fetcher) {
|
||||
const cached = cache.get(key);
|
||||
|
||||
if (cached && Date.now() - cached.timestamp < CACHE_DURATION) {
|
||||
return cached.data;
|
||||
}
|
||||
|
||||
const data = fetcher();
|
||||
cache.set(key, { data, timestamp: Date.now() });
|
||||
|
||||
return data;
|
||||
const cached = cache.get(key);
|
||||
|
||||
if (cached && Date.now() - cached.timestamp < CACHE_DURATION) {
|
||||
return cached.data;
|
||||
}
|
||||
|
||||
const data = fetcher();
|
||||
cache.set(key, { data, timestamp: Date.now() });
|
||||
|
||||
return data;
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -590,16 +597,16 @@ export function getCached(key, fetcher) {
|
|||
```javascript
|
||||
// vite.config.js
|
||||
export default defineConfig({
|
||||
build: {
|
||||
rollupOptions: {
|
||||
output: {
|
||||
manualChunks: {
|
||||
'markdown': ['marked', 'gray-matter'],
|
||||
'highlight': ['shiki']
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
build: {
|
||||
rollupOptions: {
|
||||
output: {
|
||||
manualChunks: {
|
||||
markdown: ['marked', 'gray-matter'],
|
||||
highlight: ['shiki'],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
|
|
@ -614,17 +621,17 @@ npm install -D @sveltejs/adapter-static
|
|||
import adapter from '@sveltejs/adapter-static';
|
||||
|
||||
export default {
|
||||
kit: {
|
||||
adapter: adapter({
|
||||
pages: 'build',
|
||||
assets: 'build',
|
||||
fallback: null,
|
||||
precompress: true
|
||||
}),
|
||||
prerender: {
|
||||
entries: ['*'] // Alle Seiten prerendern
|
||||
}
|
||||
}
|
||||
kit: {
|
||||
adapter: adapter({
|
||||
pages: 'build',
|
||||
assets: 'build',
|
||||
fallback: null,
|
||||
precompress: true,
|
||||
}),
|
||||
prerender: {
|
||||
entries: ['*'], // Alle Seiten prerendern
|
||||
},
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
|
|
@ -692,4 +699,4 @@ Besuche `http://localhost:5173/blog`
|
|||
|
||||
---
|
||||
|
||||
**Empfehlung**: Starte mit **mdsvex** (Ansatz 1) - es bietet die beste Balance zwischen Einfachheit und Features, plus du kannst Svelte-Components direkt in Markdown verwenden!
|
||||
**Empfehlung**: Starte mit **mdsvex** (Ansatz 1) - es bietet die beste Balance zwischen Einfachheit und Features, plus du kannst Svelte-Components direkt in Markdown verwenden!
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -30,7 +30,7 @@ In einer Welt, in der über 60% des Web-Traffics von mobilen Geräten kommt, sin
|
|||
|
||||
Die Cognitive Load Theory erklärt, warum kurze URLs so effektiv sind. Unser Gehirn ist darauf programmiert, Energie zu sparen – es ist evolutionär faul, aber auf eine intelligente Weise. Bei der Verarbeitung von Informationen sucht es immer nach dem Weg des geringsten Widerstands.
|
||||
|
||||
Wenn wir einen kurzen, klaren Link sehen, kann unser Gehirn ihn schnell verarbeiten und kategorisieren. Diese mühelose Verarbeitung erzeugt ein positives Gefühl – wir verbinden "einfach" automatisch mit "sicher" und "vertrauenswürdig".
|
||||
Wenn wir einen kurzen, klaren Link sehen, kann unser Gehirn ihn schnell verarbeiten und kategorisieren. Diese mühelose Verarbeitung erzeugt ein positives Gefühl – wir verbinden "einfach" automatisch mit "sicher" und "vertrauenswürdig".
|
||||
|
||||
### Der Halo-Effekt kurzer URLs
|
||||
|
||||
|
|
@ -43,6 +43,7 @@ Unsere Analyse von über 10.000 Link-Klicks hat vier Hauptfaktoren identifiziert
|
|||
### 1. Erkennbare Domain (60% Wichtigkeit)
|
||||
|
||||
Menschen wollen wissen, wo sie landen werden. Eine klare, erkennbare Domain ist der wichtigste Vertrauensfaktor. Das bedeutet:
|
||||
|
||||
- Verwenden Sie Ihre Marken-Domain wenn möglich
|
||||
- Bei Kurz-URLs: Wählen Sie einen Service mit gutem Ruf
|
||||
- Vermeiden Sie obskure URL-Shortener
|
||||
|
|
@ -50,6 +51,7 @@ Menschen wollen wissen, wo sie landen werden. Eine klare, erkennbare Domain ist
|
|||
### 2. Keine kryptischen Zeichen (25% Wichtigkeit)
|
||||
|
||||
Zufällige Zahlen-Buchstaben-Kombinationen wie "x7h9k2p" schrecken Nutzer ab. Stattdessen:
|
||||
|
||||
- Nutzen Sie sprechende Begriffe
|
||||
- Verwenden Sie relevante Keywords
|
||||
- Halten Sie es lesbar und merkbar
|
||||
|
|
@ -57,6 +59,7 @@ Zufällige Zahlen-Buchstaben-Kombinationen wie "x7h9k2p" schrecken Nutzer ab. St
|
|||
### 3. Optimale Länge (10% Wichtigkeit)
|
||||
|
||||
Die magische Grenze liegt bei etwa 50 Zeichen. Alles darüber hinaus wird als zu lang wahrgenommen. Studien zeigen:
|
||||
|
||||
- 15-30 Zeichen: Optimal für Social Media
|
||||
- 30-50 Zeichen: Ideal für E-Mail-Marketing
|
||||
- Über 50 Zeichen: Deutlicher Rückgang der Klickrate
|
||||
|
|
@ -81,6 +84,7 @@ Sonderzeichen wie %, &, = oder ? in der sichtbaren URL verwirren Nutzer und ersc
|
|||
### 3. Die 50-Zeichen-Regel
|
||||
|
||||
Halten Sie Ihre URLs unter 50 Zeichen. Das ist:
|
||||
|
||||
- Kurz genug für Twitter/X
|
||||
- Lesbar auf Mobilgeräten
|
||||
- Merkbar für Nutzer
|
||||
|
|
@ -89,12 +93,14 @@ Halten Sie Ihre URLs unter 50 Zeichen. Das ist:
|
|||
### 4. A/B-Testing ist Ihr Freund
|
||||
|
||||
Testen Sie verschiedene URL-Varianten:
|
||||
|
||||
- Kurz vs. deskriptiv
|
||||
- Mit Markenname vs. ohne
|
||||
- Verschiedene Keywords
|
||||
- Unterschiedliche Strukturen
|
||||
|
||||
Messen Sie dabei:
|
||||
|
||||
- Klickrate (CTR)
|
||||
- Conversion Rate
|
||||
- Bounce Rate
|
||||
|
|
@ -103,6 +109,7 @@ Messen Sie dabei:
|
|||
### 5. Performance-Tracking implementieren
|
||||
|
||||
Ohne Daten keine Optimierung. Moderne Link-Management-Tools bieten:
|
||||
|
||||
- Detaillierte Klick-Statistiken
|
||||
- Geografische Verteilung
|
||||
- Geräteerkennung
|
||||
|
|
@ -112,18 +119,22 @@ Ohne Daten keine Optimierung. Moderne Link-Management-Tools bieten:
|
|||
## Psychologische Trigger in URLs nutzen
|
||||
|
||||
### Urgency (Dringlichkeit)
|
||||
|
||||
- `ulo.ad/flash-sale-24h`
|
||||
- `ulo.ad/limited-offer`
|
||||
|
||||
### Curiosity (Neugier)
|
||||
|
||||
- `ulo.ad/geheimtipp-2024`
|
||||
- `ulo.ad/insider-trick`
|
||||
|
||||
### Value (Wert)
|
||||
|
||||
- `ulo.ad/gratis-guide`
|
||||
- `ulo.ad/50-prozent-rabatt`
|
||||
|
||||
### Social Proof
|
||||
|
||||
- `ulo.ad/bestseller-2024`
|
||||
- `ulo.ad/meistgelesen`
|
||||
|
||||
|
|
@ -139,7 +150,8 @@ Zu viele technische Parameter lassen einen Link "unmenschlich" wirken. Nutzer sp
|
|||
|
||||
### Mobile Usability Disaster
|
||||
|
||||
Auf Smartphones werden lange URLs oft abgeschnitten oder umbrechen. Das Resultat:
|
||||
Auf Smartphones werden lange URLs oft abgeschnitten oder umbrechen. Das Resultat:
|
||||
|
||||
- Unleserlichkeit
|
||||
- Professioneller Eindruck geht verloren
|
||||
- Nutzer können das Ziel nicht einschätzen
|
||||
|
|
@ -150,6 +162,7 @@ Auf Smartphones werden lange URLs oft abgeschnitten oder umbrechen. Das Resultat
|
|||
### E-Commerce: 67% mehr Conversions
|
||||
|
||||
Ein großer Online-Händler verkürzte seine Produkt-URLs von durchschnittlich 120 auf 45 Zeichen. Das Ergebnis:
|
||||
|
||||
- 67% höhere Conversion Rate
|
||||
- 42% mehr Social Shares
|
||||
- 31% niedrigere Bounce Rate
|
||||
|
|
@ -157,6 +170,7 @@ Ein großer Online-Händler verkürzte seine Produkt-URLs von durchschnittlich 1
|
|||
### Newsletter-Marketing: Verdoppelte Klickrate
|
||||
|
||||
Ein B2B-Unternehmen wechselte von langen Tracking-URLs zu personalisierten Kurz-URLs:
|
||||
|
||||
- Vorher: `company.com/newsletter/2024/march/article-5?utm_source=email&utm_medium=newsletter&subscriber=12345`
|
||||
- Nachher: `co.link/cloud-guide`
|
||||
- Resultat: 2,1x höhere Klickrate
|
||||
|
|
@ -164,6 +178,7 @@ Ein B2B-Unternehmen wechselte von langen Tracking-URLs zu personalisierten Kurz-
|
|||
### Social Media: 3x mehr Engagement
|
||||
|
||||
Ein Influencer nutzte branded Short-URLs statt generischer Affiliate-Links:
|
||||
|
||||
- Engagement stieg um 300%
|
||||
- Trust-Score verbesserte sich um 85%
|
||||
- Follower-Wachstum +45%
|
||||
|
|
@ -173,6 +188,7 @@ Ein Influencer nutzte branded Short-URLs statt generischer Affiliate-Links:
|
|||
### KI-optimierte Personalisierung
|
||||
|
||||
Moderne Systeme nutzen KI, um für jeden Nutzer die optimale URL-Variante zu generieren – basierend auf:
|
||||
|
||||
- Demografischen Daten
|
||||
- Bisherigem Klickverhalten
|
||||
- Kontext der Interaktion
|
||||
|
|
@ -181,6 +197,7 @@ Moderne Systeme nutzen KI, um für jeden Nutzer die optimale URL-Variante zu gen
|
|||
### Voice-First Optimization
|
||||
|
||||
Mit dem Aufstieg von Sprachassistenten werden "sprechbare" URLs wichtiger:
|
||||
|
||||
- Einfache Wörter statt Buchstaben-Zahlen-Kombinationen
|
||||
- Vermeidung ähnlich klingender Begriffe
|
||||
- Klare, eindeutige Aussprache
|
||||
|
|
@ -233,6 +250,7 @@ Die Psychologie kurzer URLs ist keine Raketenwissenschaft, aber ihre Auswirkunge
|
|||
### Ein Werkzeug, das hilft
|
||||
|
||||
Tools wie [uload](https://ulo.ad) wurden speziell entwickelt, um die Erkenntnisse der URL-Psychologie in die Praxis umzusetzen. Mit Features wie:
|
||||
|
||||
- Personalisierte Kurz-URLs
|
||||
- Detaillierte Analytics
|
||||
- A/B-Testing
|
||||
|
|
@ -246,10 +264,11 @@ können Sie sofort damit beginnen, Ihre Link-Performance zu optimieren.
|
|||
**Über diesen Artikel**: Basierend auf aktuellen Studien zur Nutzerpsychologie und realen Performance-Daten von über 10 Millionen Link-Klicks. Die präsentierten Strategien wurden in der Praxis getestet und validiert.
|
||||
|
||||
**Weiterführende Ressourcen**:
|
||||
|
||||
- [Cognitive Load Theory in UX Design](https://www.nngroup.com/articles/minimize-cognitive-load/)
|
||||
- [The Psychology of Web Performance](https://www.smashingmagazine.com/2024/01/psychology-web-performance/)
|
||||
- [Mobile-First URL Strategy Guide](https://moz.com/blog/mobile-first-indexing)
|
||||
|
||||
---
|
||||
|
||||
*Haben Sie Fragen oder möchten Sie Ihre eigenen Erfahrungen teilen? Kontaktieren Sie uns oder hinterlassen Sie einen Kommentar. Wir freuen uns auf den Austausch!*
|
||||
_Haben Sie Fragen oder möchten Sie Ihre eigenen Erfahrungen teilen? Kontaktieren Sie uns oder hinterlassen Sie einen Kommentar. Wir freuen uns auf den Austausch!_
|
||||
|
|
|
|||
|
|
@ -88,9 +88,9 @@ await cardsStore.createCard({
|
|||
id: 'header-1',
|
||||
type: 'header',
|
||||
props: { title: 'My Card' },
|
||||
order: 0
|
||||
}
|
||||
]
|
||||
order: 0,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// Advanced mode card
|
||||
|
|
@ -98,7 +98,7 @@ await cardsStore.createCard({
|
|||
mode: 'advanced',
|
||||
template: '<h1>{{title}}</h1>',
|
||||
variables: [{ name: 'title', type: 'text', label: 'Title' }],
|
||||
values: { title: 'My Title' }
|
||||
values: { title: 'My Title' },
|
||||
});
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -29,10 +29,10 @@ Das Card System in uload ist eine flexible, modulare Architektur für die Erstel
|
|||
type: 'header',
|
||||
props: {
|
||||
title: 'Meine Karte',
|
||||
subtitle: 'Eine Beispielkarte'
|
||||
}
|
||||
}
|
||||
]
|
||||
subtitle: 'Eine Beispielkarte',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -148,7 +148,7 @@ const theme = {
|
|||
text: '#111827',
|
||||
textMuted: '#6b7280',
|
||||
border: '#e5e7eb',
|
||||
hover: '#f3f4f6'
|
||||
hover: '#f3f4f6',
|
||||
},
|
||||
typography: {
|
||||
fontFamily: 'Inter, sans-serif',
|
||||
|
|
@ -157,23 +157,23 @@ const theme = {
|
|||
sm: '0.875rem',
|
||||
md: '1rem',
|
||||
lg: '1.125rem',
|
||||
xl: '1.25rem'
|
||||
}
|
||||
xl: '1.25rem',
|
||||
},
|
||||
},
|
||||
spacing: {
|
||||
xs: '0.25rem',
|
||||
sm: '0.5rem',
|
||||
md: '1rem',
|
||||
lg: '1.5rem',
|
||||
xl: '2rem'
|
||||
xl: '2rem',
|
||||
},
|
||||
borderRadius: {
|
||||
sm: '0.25rem',
|
||||
md: '0.5rem',
|
||||
lg: '0.75rem',
|
||||
xl: '1rem',
|
||||
full: '9999px'
|
||||
}
|
||||
full: '9999px',
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
|
|
@ -279,7 +279,7 @@ interface LinksCardProps {
|
|||
layout={{
|
||||
padding: '1.5rem',
|
||||
columns: 2,
|
||||
gap: '1rem'
|
||||
gap: '1rem',
|
||||
}}
|
||||
modules={[
|
||||
{
|
||||
|
|
@ -287,8 +287,8 @@ interface LinksCardProps {
|
|||
props: {
|
||||
title: 'Dashboard',
|
||||
subtitle: 'Übersicht deiner Aktivitäten',
|
||||
icon: '📊'
|
||||
}
|
||||
icon: '📊',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'stats',
|
||||
|
|
@ -296,21 +296,21 @@ interface LinksCardProps {
|
|||
stats: [
|
||||
{ label: 'Links', value: 42, icon: '🔗' },
|
||||
{ label: 'Clicks', value: '1.2k', icon: '👆' },
|
||||
{ label: 'Conversion', value: '24%', icon: '📈' }
|
||||
{ label: 'Conversion', value: '24%', icon: '📈' },
|
||||
],
|
||||
layout: 'grid'
|
||||
}
|
||||
layout: 'grid',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'actions',
|
||||
props: {
|
||||
actions: [
|
||||
{ label: 'Neuer Link', variant: 'primary' },
|
||||
{ label: 'Statistiken', variant: 'secondary' }
|
||||
{ label: 'Statistiken', variant: 'secondary' },
|
||||
],
|
||||
layout: 'horizontal'
|
||||
}
|
||||
}
|
||||
layout: 'horizontal',
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
```
|
||||
|
|
|
|||
|
|
@ -17,9 +17,9 @@
|
|||
props: {
|
||||
title: 'Meine erste Karte',
|
||||
subtitle: 'Ein einfaches Beispiel',
|
||||
icon: '🎉'
|
||||
}
|
||||
}
|
||||
icon: '🎉',
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
```
|
||||
|
|
@ -35,15 +35,15 @@
|
|||
order: 0,
|
||||
props: {
|
||||
title: 'Komplexere Karte',
|
||||
subtitle: 'Mit mehreren Modulen'
|
||||
}
|
||||
subtitle: 'Mit mehreren Modulen',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'content',
|
||||
order: 1,
|
||||
props: {
|
||||
text: 'Dies ist der Hauptinhalt der Karte. Hier kann beliebiger Text stehen.'
|
||||
}
|
||||
text: 'Dies ist der Hauptinhalt der Karte. Hier kann beliebiger Text stehen.',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'actions',
|
||||
|
|
@ -51,10 +51,10 @@
|
|||
props: {
|
||||
actions: [
|
||||
{ label: 'Mehr erfahren', variant: 'primary' },
|
||||
{ label: 'Schließen', variant: 'ghost' }
|
||||
]
|
||||
}
|
||||
}
|
||||
{ label: 'Schließen', variant: 'ghost' },
|
||||
],
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
```
|
||||
|
|
@ -71,7 +71,7 @@
|
|||
name: 'Max Mustermann',
|
||||
role: 'Software Developer',
|
||||
avatar: '/avatars/max.jpg',
|
||||
bio: 'Passionate about creating amazing web experiences.'
|
||||
bio: 'Passionate about creating amazing web experiences.',
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
@ -83,15 +83,15 @@
|
|||
props: {
|
||||
title: userData.name,
|
||||
subtitle: userData.role,
|
||||
avatar: userData.avatar
|
||||
}
|
||||
avatar: userData.avatar,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'content',
|
||||
props: {
|
||||
text: userData.bio
|
||||
}
|
||||
}
|
||||
text: userData.bio,
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
```
|
||||
|
|
@ -109,14 +109,14 @@
|
|||
title: 'Jane Doe',
|
||||
subtitle: 'UX Designer',
|
||||
avatar: '/avatars/jane.jpg',
|
||||
badge: 'PRO'
|
||||
}
|
||||
badge: 'PRO',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'content',
|
||||
props: {
|
||||
text: 'Creating beautiful and functional user experiences since 2015.'
|
||||
}
|
||||
text: 'Creating beautiful and functional user experiences since 2015.',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'stats',
|
||||
|
|
@ -124,10 +124,10 @@
|
|||
stats: [
|
||||
{ label: 'Projects', value: 127, icon: '📁' },
|
||||
{ label: 'Clients', value: 45, icon: '👥' },
|
||||
{ label: 'Awards', value: 8, icon: '🏆' }
|
||||
{ label: 'Awards', value: 8, icon: '🏆' },
|
||||
],
|
||||
layout: 'grid'
|
||||
}
|
||||
layout: 'grid',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'links',
|
||||
|
|
@ -135,13 +135,13 @@
|
|||
links: [
|
||||
{ label: 'Portfolio', href: 'https://portfolio.com', icon: '🎨' },
|
||||
{ label: 'LinkedIn', href: 'https://linkedin.com', icon: '💼' },
|
||||
{ label: 'GitHub', href: 'https://github.com', icon: '💻' }
|
||||
{ label: 'GitHub', href: 'https://github.com', icon: '💻' },
|
||||
],
|
||||
style: 'button',
|
||||
showIcon: true
|
||||
}
|
||||
}
|
||||
]
|
||||
showIcon: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
@ -160,8 +160,8 @@
|
|||
type: 'header',
|
||||
props: {
|
||||
title: 'Verkaufsstatistik',
|
||||
subtitle: 'Letzten 30 Tage'
|
||||
}
|
||||
subtitle: 'Letzten 30 Tage',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'stats',
|
||||
|
|
@ -169,11 +169,11 @@
|
|||
stats: [
|
||||
{ label: 'Umsatz', value: '€12.4k', change: 12, color: 'green' },
|
||||
{ label: 'Bestellungen', value: 234, change: -5, color: 'red' },
|
||||
{ label: 'Conversion', value: '3.2%', change: 8, color: 'blue' }
|
||||
{ label: 'Conversion', value: '3.2%', change: 8, color: 'blue' },
|
||||
],
|
||||
layout: 'list'
|
||||
}
|
||||
}
|
||||
layout: 'list',
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
```
|
||||
|
|
@ -188,8 +188,8 @@
|
|||
type: 'header',
|
||||
props: {
|
||||
title: 'Letzte Aktivitäten',
|
||||
icon: '📋'
|
||||
}
|
||||
icon: '📋',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'content',
|
||||
|
|
@ -198,10 +198,10 @@
|
|||
{ label: 'Neuer Benutzer', value: 'vor 5 Min.', icon: '👤' },
|
||||
{ label: 'Bestellung #1234', value: 'vor 15 Min.', icon: '🛒' },
|
||||
{ label: 'Kommentar erhalten', value: 'vor 1 Std.', icon: '💬' },
|
||||
{ label: 'System-Update', value: 'vor 2 Std.', icon: '🔄' }
|
||||
]
|
||||
}
|
||||
}
|
||||
{ label: 'System-Update', value: 'vor 2 Std.', icon: '🔄' },
|
||||
],
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
```
|
||||
|
|
@ -218,8 +218,8 @@
|
|||
type: 'header',
|
||||
props: {
|
||||
title: 'Folge mir',
|
||||
subtitle: 'auf Social Media'
|
||||
}
|
||||
subtitle: 'auf Social Media',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'links',
|
||||
|
|
@ -228,14 +228,14 @@
|
|||
{ label: 'Instagram', href: 'https://instagram.com/user', icon: '📷' },
|
||||
{ label: 'Twitter', href: 'https://twitter.com/user', icon: '🐦' },
|
||||
{ label: 'YouTube', href: 'https://youtube.com/user', icon: '📺' },
|
||||
{ label: 'TikTok', href: 'https://tiktok.com/@user', icon: '🎵' }
|
||||
{ label: 'TikTok', href: 'https://tiktok.com/@user', icon: '🎵' },
|
||||
],
|
||||
style: 'button',
|
||||
columns: 2,
|
||||
buttonVariant: 'secondary',
|
||||
showIcon: true
|
||||
}
|
||||
}
|
||||
showIcon: true,
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
```
|
||||
|
|
@ -249,8 +249,8 @@
|
|||
{
|
||||
type: 'header',
|
||||
props: {
|
||||
title: 'Nützliche Ressourcen'
|
||||
}
|
||||
title: 'Nützliche Ressourcen',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'links',
|
||||
|
|
@ -260,26 +260,26 @@
|
|||
label: 'Dokumentation',
|
||||
href: '/docs',
|
||||
icon: '📚',
|
||||
description: 'Vollständige API-Dokumentation'
|
||||
description: 'Vollständige API-Dokumentation',
|
||||
},
|
||||
{
|
||||
label: 'Tutorials',
|
||||
href: '/tutorials',
|
||||
icon: '🎓',
|
||||
description: 'Schritt-für-Schritt Anleitungen'
|
||||
description: 'Schritt-für-Schritt Anleitungen',
|
||||
},
|
||||
{
|
||||
label: 'Community Forum',
|
||||
href: '/forum',
|
||||
icon: '💬',
|
||||
description: 'Hilfe von der Community'
|
||||
}
|
||||
description: 'Hilfe von der Community',
|
||||
},
|
||||
],
|
||||
style: 'card',
|
||||
showDescription: true,
|
||||
showIcon: true
|
||||
}
|
||||
}
|
||||
showIcon: true,
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
```
|
||||
|
|
@ -295,8 +295,8 @@
|
|||
{
|
||||
type: 'header',
|
||||
props: {
|
||||
title: 'Projekt Screenshots'
|
||||
}
|
||||
title: 'Projekt Screenshots',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'media',
|
||||
|
|
@ -304,15 +304,15 @@
|
|||
type: 'image',
|
||||
src: '/screenshots/dashboard.png',
|
||||
alt: 'Dashboard Screenshot',
|
||||
aspectRatio: '16/9'
|
||||
}
|
||||
aspectRatio: '16/9',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'content',
|
||||
props: {
|
||||
text: 'Das neue Dashboard-Design mit verbesserter Benutzerführung.'
|
||||
}
|
||||
}
|
||||
text: 'Das neue Dashboard-Design mit verbesserter Benutzerführung.',
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
```
|
||||
|
|
@ -327,8 +327,8 @@
|
|||
type: 'header',
|
||||
props: {
|
||||
title: 'Mein QR-Code',
|
||||
subtitle: 'Scanne für Kontaktdaten'
|
||||
}
|
||||
subtitle: 'Scanne für Kontaktdaten',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'media',
|
||||
|
|
@ -336,9 +336,9 @@
|
|||
type: 'qr',
|
||||
qrData: 'https://example.com/contact',
|
||||
qrSize: 200,
|
||||
qrColor: '#000000'
|
||||
}
|
||||
}
|
||||
qrColor: '#000000',
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
```
|
||||
|
|
@ -361,8 +361,8 @@
|
|||
text: '#f9fafb',
|
||||
textMuted: '#9ca3af',
|
||||
border: '#374151',
|
||||
hover: '#374151'
|
||||
}
|
||||
hover: '#374151',
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
@ -374,9 +374,9 @@
|
|||
type: 'header',
|
||||
props: {
|
||||
title: 'Dark Mode Karte',
|
||||
subtitle: 'Mit custom Theme'
|
||||
}
|
||||
}
|
||||
subtitle: 'Mit custom Theme',
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
|
|
@ -392,8 +392,8 @@
|
|||
secondary: '#4ecdc4',
|
||||
accent: '#45b7d1',
|
||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
text: '#ffffff'
|
||||
}
|
||||
text: '#ffffff',
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
@ -405,9 +405,9 @@
|
|||
type: 'header',
|
||||
props: {
|
||||
title: 'Gradient Card',
|
||||
subtitle: 'Mit Farbverlauf'
|
||||
}
|
||||
}
|
||||
subtitle: 'Mit Farbverlauf',
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
```
|
||||
|
|
@ -495,28 +495,28 @@
|
|||
breakpoints: {
|
||||
sm: '640px',
|
||||
md: '768px',
|
||||
lg: '1024px'
|
||||
lg: '1024px',
|
||||
},
|
||||
mobileLayout: 'stack'
|
||||
mobileLayout: 'stack',
|
||||
}}
|
||||
modules={[
|
||||
{
|
||||
type: 'header',
|
||||
visibility: 'always',
|
||||
props: { title: 'Responsive Karte' }
|
||||
props: { title: 'Responsive Karte' },
|
||||
},
|
||||
{
|
||||
type: 'content',
|
||||
visibility: 'desktop', // Nur auf Desktop
|
||||
props: { text: 'Dieser Text ist nur auf Desktop sichtbar.' }
|
||||
props: { text: 'Dieser Text ist nur auf Desktop sichtbar.' },
|
||||
},
|
||||
{
|
||||
type: 'actions',
|
||||
visibility: 'mobile', // Nur auf Mobile
|
||||
props: {
|
||||
actions: [{ label: 'Mobile Action', variant: 'primary' }]
|
||||
}
|
||||
}
|
||||
actions: [{ label: 'Mobile Action', variant: 'primary' }],
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
```
|
||||
|
|
@ -532,16 +532,16 @@
|
|||
hover: true,
|
||||
entrance: 'slide',
|
||||
duration: 300,
|
||||
delay: 100
|
||||
delay: 100,
|
||||
}}
|
||||
modules={[
|
||||
{
|
||||
type: 'header',
|
||||
props: {
|
||||
title: 'Animierte Karte',
|
||||
subtitle: 'Mit Slide-In Effekt'
|
||||
}
|
||||
}
|
||||
subtitle: 'Mit Slide-In Effekt',
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
```
|
||||
|
|
|
|||
|
|
@ -217,7 +217,7 @@ const trafficDistribution = {
|
|||
control: 40, // Baseline
|
||||
a1: 20, // Value Test
|
||||
b1: 20, // Social Test
|
||||
c1: 20 // Feature Test
|
||||
c1: 20, // Feature Test
|
||||
};
|
||||
```
|
||||
|
||||
|
|
@ -230,7 +230,7 @@ const trafficDistribution = {
|
|||
control: 30, // Reduziert
|
||||
a2: 30, // Winner A (neue Version)
|
||||
b1: 20, // B bleibt
|
||||
c2: 20 // C iteriert
|
||||
c2: 20, // C iteriert
|
||||
};
|
||||
```
|
||||
|
||||
|
|
@ -241,7 +241,7 @@ const trafficDistribution = {
|
|||
champion: 70, // Bester Performer
|
||||
challenger1: 10, // Neue Idee
|
||||
challenger2: 10, // Neue Idee
|
||||
challenger3: 10 // Neue Idee
|
||||
challenger3: 10, // Neue Idee
|
||||
};
|
||||
```
|
||||
|
||||
|
|
@ -445,7 +445,7 @@ export class HashManager {
|
|||
control: 40,
|
||||
a1: 20,
|
||||
b1: 20,
|
||||
c1: 20
|
||||
c1: 20,
|
||||
};
|
||||
|
||||
getVariant(): string {
|
||||
|
|
@ -508,7 +508,7 @@ export class HashManager {
|
|||
control: HeroControl,
|
||||
a1: HeroA1,
|
||||
a2: HeroA2,
|
||||
b1: HeroB1
|
||||
b1: HeroB1,
|
||||
// ... more mappings
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
## Problemanalyse
|
||||
|
||||
Das uLoad-Projekt war kürzlich komplett down, was zu kritischen Problemen geführt hat:
|
||||
|
||||
- Hauptanwendung nicht erreichbar
|
||||
- Weiterleitungen funktionierten nicht
|
||||
- Benutzererfahrung stark beeinträchtigt
|
||||
|
|
@ -11,6 +12,7 @@ Das uLoad-Projekt war kürzlich komplett down, was zu kritischen Problemen gefü
|
|||
## Aktuelle Architektur-Analyse
|
||||
|
||||
### Technology Stack
|
||||
|
||||
- **Frontend:** SvelteKit 2.22 mit Svelte 5.0
|
||||
- **Backend:** PocketBase (https://pb.ulo.ad)
|
||||
- **Hosting:** Hetzner VPS mit Coolify
|
||||
|
|
@ -44,32 +46,34 @@ Das uLoad-Projekt war kürzlich komplett down, was zu kritischen Problemen gefü
|
|||
### 1. Verbesserte Error Handling & Fallbacks
|
||||
|
||||
#### PocketBase Connection Resilience
|
||||
|
||||
```typescript
|
||||
// src/lib/pocketbase-resilient.ts
|
||||
class ResilientPocketBase {
|
||||
private retryCount = 0;
|
||||
private maxRetries = 3;
|
||||
private backoffMs = 1000;
|
||||
private retryCount = 0;
|
||||
private maxRetries = 3;
|
||||
private backoffMs = 1000;
|
||||
|
||||
async withRetry<T>(operation: () => Promise<T>): Promise<T> {
|
||||
for (let i = 0; i <= this.maxRetries; i++) {
|
||||
try {
|
||||
return await operation();
|
||||
} catch (error) {
|
||||
if (i === this.maxRetries) throw error;
|
||||
await this.delay(this.backoffMs * Math.pow(2, i));
|
||||
}
|
||||
}
|
||||
throw new Error('Max retries exceeded');
|
||||
}
|
||||
async withRetry<T>(operation: () => Promise<T>): Promise<T> {
|
||||
for (let i = 0; i <= this.maxRetries; i++) {
|
||||
try {
|
||||
return await operation();
|
||||
} catch (error) {
|
||||
if (i === this.maxRetries) throw error;
|
||||
await this.delay(this.backoffMs * Math.pow(2, i));
|
||||
}
|
||||
}
|
||||
throw new Error('Max retries exceeded');
|
||||
}
|
||||
|
||||
private delay(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
private delay(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Graceful Degradation
|
||||
|
||||
- Cache-basierte Fallbacks für kritische Daten
|
||||
- Offline-Mode für Basis-Funktionalität
|
||||
- Error Boundaries in allen Komponenten
|
||||
|
|
@ -77,37 +81,40 @@ class ResilientPocketBase {
|
|||
### 2. Enhanced Monitoring
|
||||
|
||||
#### Health Check Verbesserung
|
||||
|
||||
```typescript
|
||||
// src/routes/health/+server.ts erweitern
|
||||
export const GET: RequestHandler = async () => {
|
||||
const health = {
|
||||
status: 'healthy',
|
||||
timestamp: new Date().toISOString(),
|
||||
environment: building ? 'build' : 'runtime',
|
||||
services: {
|
||||
sveltekit: 'running',
|
||||
pocketbase: await checkPocketBaseDetailed(),
|
||||
database: await checkDatabaseHealth(),
|
||||
memory: process.memoryUsage(),
|
||||
uptime: process.uptime()
|
||||
},
|
||||
checks: {
|
||||
canCreateLink: await testLinkCreation(),
|
||||
canAuthenticate: await testAuthentication(),
|
||||
canServeStatic: await testStaticFiles()
|
||||
}
|
||||
};
|
||||
const health = {
|
||||
status: 'healthy',
|
||||
timestamp: new Date().toISOString(),
|
||||
environment: building ? 'build' : 'runtime',
|
||||
services: {
|
||||
sveltekit: 'running',
|
||||
pocketbase: await checkPocketBaseDetailed(),
|
||||
database: await checkDatabaseHealth(),
|
||||
memory: process.memoryUsage(),
|
||||
uptime: process.uptime(),
|
||||
},
|
||||
checks: {
|
||||
canCreateLink: await testLinkCreation(),
|
||||
canAuthenticate: await testAuthentication(),
|
||||
canServeStatic: await testStaticFiles(),
|
||||
},
|
||||
};
|
||||
|
||||
const overallStatus = Object.values(health.services).every(s => s === 'running')
|
||||
? 'healthy' : 'degraded';
|
||||
const overallStatus = Object.values(health.services).every((s) => s === 'running')
|
||||
? 'healthy'
|
||||
: 'degraded';
|
||||
|
||||
return json(health, {
|
||||
status: overallStatus === 'healthy' ? 200 : 503
|
||||
});
|
||||
return json(health, {
|
||||
status: overallStatus === 'healthy' ? 200 : 503,
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
#### Externes Monitoring Setup
|
||||
|
||||
- Uptime Robot/Pingdom für externe Überwachung
|
||||
- Slack/Discord Webhooks für Alerts
|
||||
- Grafana Dashboard für Metriken
|
||||
|
|
@ -115,32 +122,34 @@ export const GET: RequestHandler = async () => {
|
|||
### 3. Improved Rate Limiting
|
||||
|
||||
#### Redis-Backend für Rate Limiting
|
||||
|
||||
```typescript
|
||||
// src/lib/server/redis-rate-limiter.ts
|
||||
import Redis from 'ioredis';
|
||||
|
||||
class RedisRateLimiter {
|
||||
private redis: Redis;
|
||||
private redis: Redis;
|
||||
|
||||
constructor() {
|
||||
this.redis = new Redis(process.env.REDIS_URL || 'redis://localhost:6379');
|
||||
}
|
||||
constructor() {
|
||||
this.redis = new Redis(process.env.REDIS_URL || 'redis://localhost:6379');
|
||||
}
|
||||
|
||||
async checkLimit(key: string, limit: number, windowMs: number): Promise<boolean> {
|
||||
const multi = this.redis.multi();
|
||||
multi.incr(key);
|
||||
multi.expire(key, Math.ceil(windowMs / 1000));
|
||||
const results = await multi.exec();
|
||||
|
||||
const count = results?.[0]?.[1] as number;
|
||||
return count <= limit;
|
||||
}
|
||||
async checkLimit(key: string, limit: number, windowMs: number): Promise<boolean> {
|
||||
const multi = this.redis.multi();
|
||||
multi.incr(key);
|
||||
multi.expire(key, Math.ceil(windowMs / 1000));
|
||||
const results = await multi.exec();
|
||||
|
||||
const count = results?.[0]?.[1] as number;
|
||||
return count <= limit;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Database Backup Strategy
|
||||
|
||||
#### Automatisierte PocketBase Backups
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# scripts/backup-pocketbase.sh
|
||||
|
|
@ -167,6 +176,7 @@ fi
|
|||
### 1. Infrastructure Redundancy
|
||||
|
||||
#### Load Balancer Setup
|
||||
|
||||
```yaml
|
||||
# docker-compose.prod-ha.yml
|
||||
version: '3.8'
|
||||
|
|
@ -174,8 +184,8 @@ services:
|
|||
nginx-lb:
|
||||
image: nginx:alpine
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
- '80:80'
|
||||
- '443:443'
|
||||
volumes:
|
||||
- ./nginx.conf:/etc/nginx/nginx.conf
|
||||
depends_on:
|
||||
|
|
@ -201,6 +211,7 @@ volumes:
|
|||
```
|
||||
|
||||
#### Multi-Region Deployment
|
||||
|
||||
- Hauptserver: Hetzner Deutschland
|
||||
- Backup Server: AWS/DigitalOcean anderer Region
|
||||
- DNS Failover mit niedrigem TTL (60 Sekunden)
|
||||
|
|
@ -208,6 +219,7 @@ volumes:
|
|||
### 2. Separated Services Architecture
|
||||
|
||||
#### PocketBase als separater Service
|
||||
|
||||
```yaml
|
||||
# docker-compose.services.yml
|
||||
services:
|
||||
|
|
@ -216,10 +228,10 @@ services:
|
|||
volumes:
|
||||
- ./pb_data:/pb/pb_data
|
||||
ports:
|
||||
- "8090:8090"
|
||||
- '8090:8090'
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8090/api/health"]
|
||||
test: ['CMD', 'curl', '-f', 'http://localhost:8090/api/health']
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
|
|
@ -237,32 +249,33 @@ services:
|
|||
### 3. Caching Layer
|
||||
|
||||
#### Redis für Caching & Sessions
|
||||
|
||||
```typescript
|
||||
// src/lib/cache/redis-cache.ts
|
||||
export class CacheManager {
|
||||
private redis: Redis;
|
||||
private redis: Redis;
|
||||
|
||||
async get<T>(key: string): Promise<T | null> {
|
||||
const cached = await this.redis.get(key);
|
||||
return cached ? JSON.parse(cached) : null;
|
||||
}
|
||||
async get<T>(key: string): Promise<T | null> {
|
||||
const cached = await this.redis.get(key);
|
||||
return cached ? JSON.parse(cached) : null;
|
||||
}
|
||||
|
||||
async set(key: string, value: any, ttlSeconds: number = 3600): Promise<void> {
|
||||
await this.redis.setex(key, ttlSeconds, JSON.stringify(value));
|
||||
}
|
||||
async set(key: string, value: any, ttlSeconds: number = 3600): Promise<void> {
|
||||
await this.redis.setex(key, ttlSeconds, JSON.stringify(value));
|
||||
}
|
||||
|
||||
// Cache häufig abgerufene Daten
|
||||
async getCachedLinks(userId: string): Promise<Link[]> {
|
||||
const cacheKey = `user:${userId}:links`;
|
||||
let links = await this.get<Link[]>(cacheKey);
|
||||
|
||||
if (!links) {
|
||||
links = await pb.collection('links').getFullList({ filter: `user_id="${userId}"` });
|
||||
await this.set(cacheKey, links, 300); // 5 Minuten Cache
|
||||
}
|
||||
|
||||
return links;
|
||||
}
|
||||
// Cache häufig abgerufene Daten
|
||||
async getCachedLinks(userId: string): Promise<Link[]> {
|
||||
const cacheKey = `user:${userId}:links`;
|
||||
let links = await this.get<Link[]>(cacheKey);
|
||||
|
||||
if (!links) {
|
||||
links = await pb.collection('links').getFullList({ filter: `user_id="${userId}"` });
|
||||
await this.set(cacheKey, links, 300); // 5 Minuten Cache
|
||||
}
|
||||
|
||||
return links;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -271,6 +284,7 @@ export class CacheManager {
|
|||
### 1. Database Migration Strategy
|
||||
|
||||
#### PostgreSQL als Primary Database
|
||||
|
||||
```typescript
|
||||
// Alternative zu PocketBase für bessere Skalierbarkeit
|
||||
// src/lib/database/postgresql.ts
|
||||
|
|
@ -289,6 +303,7 @@ export const db = drizzle(client);
|
|||
```
|
||||
|
||||
#### Database Cluster Setup
|
||||
|
||||
- Master-Slave Replication
|
||||
- Read Replicas für Analytics
|
||||
- Automated Failover
|
||||
|
|
@ -296,6 +311,7 @@ export const db = drizzle(client);
|
|||
### 2. CDN Integration
|
||||
|
||||
#### Cloudflare Setup
|
||||
|
||||
```typescript
|
||||
// src/app.html erweitern
|
||||
// DNS-Level Protection gegen DDoS
|
||||
|
|
@ -307,6 +323,7 @@ export const db = drizzle(client);
|
|||
### 3. Microservices Architecture
|
||||
|
||||
#### Service Separation
|
||||
|
||||
```
|
||||
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
||||
│ Frontend │ │ API Gateway │ │ Auth Service │
|
||||
|
|
@ -328,6 +345,7 @@ export const db = drizzle(client);
|
|||
### 1. CI/CD Pipeline Verbesserung
|
||||
|
||||
#### GitHub Actions Workflow
|
||||
|
||||
```yaml
|
||||
# .github/workflows/deploy.yml
|
||||
name: Deploy to Production
|
||||
|
|
@ -354,7 +372,7 @@ jobs:
|
|||
run: |
|
||||
# Coolify Deployment
|
||||
curl -X POST ${{ secrets.COOLIFY_WEBHOOK }}
|
||||
|
||||
|
||||
- name: Health Check
|
||||
run: |
|
||||
# Warte auf erfolgreiche Deployment
|
||||
|
|
@ -366,7 +384,7 @@ jobs:
|
|||
sleep 10
|
||||
done
|
||||
exit 1
|
||||
|
||||
|
||||
- name: Rollback on Failure
|
||||
if: failure()
|
||||
run: |
|
||||
|
|
@ -377,6 +395,7 @@ jobs:
|
|||
### 2. Monitoring & Alerting
|
||||
|
||||
#### Prometheus + Grafana Setup
|
||||
|
||||
```yaml
|
||||
# monitoring/docker-compose.yml
|
||||
services:
|
||||
|
|
@ -385,26 +404,27 @@ services:
|
|||
volumes:
|
||||
- ./prometheus.yml:/etc/prometheus/prometheus.yml
|
||||
ports:
|
||||
- "9090:9090"
|
||||
- '9090:9090'
|
||||
|
||||
grafana:
|
||||
image: grafana/grafana
|
||||
environment:
|
||||
- GF_SECURITY_ADMIN_PASSWORD=admin
|
||||
ports:
|
||||
- "3001:3000"
|
||||
- '3001:3000'
|
||||
volumes:
|
||||
- grafana-storage:/var/lib/grafana
|
||||
|
||||
alertmanager:
|
||||
image: prom/alertmanager
|
||||
ports:
|
||||
- "9093:9093"
|
||||
- '9093:9093'
|
||||
volumes:
|
||||
- ./alertmanager.yml:/etc/alertmanager/alertmanager.yml
|
||||
```
|
||||
|
||||
#### Custom Metrics
|
||||
|
||||
```typescript
|
||||
// src/lib/metrics/prometheus.ts
|
||||
import { register, collectDefaultMetrics, Counter, Histogram } from 'prom-client';
|
||||
|
|
@ -412,27 +432,28 @@ import { register, collectDefaultMetrics, Counter, Histogram } from 'prom-client
|
|||
collectDefaultMetrics();
|
||||
|
||||
export const httpRequestsTotal = new Counter({
|
||||
name: 'http_requests_total',
|
||||
help: 'Total number of HTTP requests',
|
||||
labelNames: ['method', 'route', 'status_code']
|
||||
name: 'http_requests_total',
|
||||
help: 'Total number of HTTP requests',
|
||||
labelNames: ['method', 'route', 'status_code'],
|
||||
});
|
||||
|
||||
export const httpRequestDuration = new Histogram({
|
||||
name: 'http_request_duration_seconds',
|
||||
help: 'Duration of HTTP requests in seconds',
|
||||
labelNames: ['method', 'route']
|
||||
name: 'http_request_duration_seconds',
|
||||
help: 'Duration of HTTP requests in seconds',
|
||||
labelNames: ['method', 'route'],
|
||||
});
|
||||
|
||||
export const linkRedirects = new Counter({
|
||||
name: 'link_redirects_total',
|
||||
help: 'Total number of link redirects',
|
||||
labelNames: ['short_code', 'success']
|
||||
name: 'link_redirects_total',
|
||||
help: 'Total number of link redirects',
|
||||
labelNames: ['short_code', 'success'],
|
||||
});
|
||||
```
|
||||
|
||||
### 3. Disaster Recovery Plan
|
||||
|
||||
#### Automated Recovery Scripts
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# scripts/disaster-recovery.sh
|
||||
|
|
@ -458,10 +479,10 @@ activate_backup() {
|
|||
restore_from_backup() {
|
||||
# Download latest backup
|
||||
aws s3 cp s3://$BACKUP_BUCKET/latest.tar.gz /tmp/restore.tar.gz
|
||||
|
||||
|
||||
# Extract and restore
|
||||
tar -xzf /tmp/restore.tar.gz -C /app/pb_data/
|
||||
|
||||
|
||||
# Restart services
|
||||
docker-compose restart
|
||||
}
|
||||
|
|
@ -470,7 +491,7 @@ restore_from_backup() {
|
|||
if ! check_services; then
|
||||
activate_backup
|
||||
restore_from_backup
|
||||
|
||||
|
||||
# Send alert
|
||||
curl -X POST $SLACK_WEBHOOK -d '{"text":"Disaster recovery activated for ulo.ad"}'
|
||||
fi
|
||||
|
|
@ -481,38 +502,40 @@ fi
|
|||
### 1. Chaos Engineering
|
||||
|
||||
#### Fault Injection Tests
|
||||
|
||||
```typescript
|
||||
// tests/chaos/network-failures.test.ts
|
||||
describe('Network Failure Scenarios', () => {
|
||||
test('should handle PocketBase timeout gracefully', async () => {
|
||||
// Simulate PocketBase timeout
|
||||
const mockPb = mockPocketBaseTimeout();
|
||||
|
||||
const response = await app.request('/api/links', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ url: 'https://example.com' })
|
||||
});
|
||||
|
||||
// Should return cached response or graceful error
|
||||
expect(response.status).toBeLessThan(500);
|
||||
});
|
||||
test('should handle PocketBase timeout gracefully', async () => {
|
||||
// Simulate PocketBase timeout
|
||||
const mockPb = mockPocketBaseTimeout();
|
||||
|
||||
test('should fallback to cached data when database is unavailable', async () => {
|
||||
// Simulate database outage
|
||||
mockDatabaseDown();
|
||||
|
||||
const response = await app.request('/my/links');
|
||||
|
||||
// Should serve from cache
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.headers.get('x-served-from')).toBe('cache');
|
||||
});
|
||||
const response = await app.request('/api/links', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ url: 'https://example.com' }),
|
||||
});
|
||||
|
||||
// Should return cached response or graceful error
|
||||
expect(response.status).toBeLessThan(500);
|
||||
});
|
||||
|
||||
test('should fallback to cached data when database is unavailable', async () => {
|
||||
// Simulate database outage
|
||||
mockDatabaseDown();
|
||||
|
||||
const response = await app.request('/my/links');
|
||||
|
||||
// Should serve from cache
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.headers.get('x-served-from')).toBe('cache');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### 2. Load Testing
|
||||
|
||||
#### Performance Benchmarks
|
||||
|
||||
```bash
|
||||
# scripts/load-test.sh
|
||||
#!/bin/bash
|
||||
|
|
@ -532,6 +555,7 @@ k6 run performance-tests/user-journey.js
|
|||
## Implementation Roadmap
|
||||
|
||||
### Phase 1 (Sofort - 1 Woche)
|
||||
|
||||
- [x] Analyse der aktuellen Architektur
|
||||
- [ ] Verbesserte Error Handling implementieren
|
||||
- [ ] Health Check Endpoints erweitern
|
||||
|
|
@ -539,6 +563,7 @@ k6 run performance-tests/user-journey.js
|
|||
- [ ] Backup-Scripts erstellen
|
||||
|
||||
### Phase 2 (2-4 Wochen)
|
||||
|
||||
- [ ] Redis für Rate Limiting & Caching
|
||||
- [ ] Load Balancer Setup
|
||||
- [ ] Service Separation (PocketBase)
|
||||
|
|
@ -546,6 +571,7 @@ k6 run performance-tests/user-journey.js
|
|||
- [ ] Disaster Recovery Scripts
|
||||
|
||||
### Phase 3 (1-3 Monate)
|
||||
|
||||
- [ ] PostgreSQL Migration evaluieren
|
||||
- [ ] CDN Integration (Cloudflare)
|
||||
- [ ] Microservices Architecture
|
||||
|
|
@ -555,6 +581,7 @@ k6 run performance-tests/user-journey.js
|
|||
## Kosten-Nutzen-Analyse
|
||||
|
||||
### Zusätzliche Infrastruktur-Kosten
|
||||
|
||||
- **Redis Server:** €5-10/Monat
|
||||
- **Backup Server:** €5-15/Monat
|
||||
- **Monitoring Tools:** €0-20/Monat (Uptime Robot Free, Grafana Cloud)
|
||||
|
|
@ -563,6 +590,7 @@ k6 run performance-tests/user-journey.js
|
|||
**Gesamtkosten:** €10-95/Monat zusätzlich
|
||||
|
||||
### Nutzen
|
||||
|
||||
- **99.9% Uptime** (vs. aktuell ~95%)
|
||||
- **Automatische Recovery** bei Ausfällen
|
||||
- **Bessere Performance** durch Caching
|
||||
|
|
@ -572,16 +600,19 @@ k6 run performance-tests/user-journey.js
|
|||
## Metriken & KPIs
|
||||
|
||||
### Verfügbarkeit
|
||||
|
||||
- **Target:** 99.9% Uptime
|
||||
- **MTTR (Mean Time To Recovery):** < 5 Minuten
|
||||
- **MTBF (Mean Time Between Failures):** > 30 Tage
|
||||
|
||||
### Performance
|
||||
|
||||
- **Response Time:** < 200ms (95th percentile)
|
||||
- **Redirect Time:** < 50ms
|
||||
- **Error Rate:** < 0.1%
|
||||
|
||||
### Monitoring
|
||||
|
||||
- **Alert Response Time:** < 2 Minuten
|
||||
- **Backup Success Rate:** 100%
|
||||
- **Health Check Success:** > 99.5%
|
||||
|
|
@ -591,9 +622,10 @@ k6 run performance-tests/user-journey.js
|
|||
Das aktuelle Single-Point-of-Failure Setup birgt erhebliche Risiken für die Verfügbarkeit von uLoad. Mit den vorgeschlagenen Maßnahmen kann die Infrastruktur deutlich robuster und ausfallsicherer gestaltet werden.
|
||||
|
||||
**Empfohlene Prioritäten:**
|
||||
|
||||
1. **Sofort:** Verbessertes Error Handling und Monitoring
|
||||
2. **Kurzfristig:** Service Separation und Backup-Strategie
|
||||
2. **Kurzfristig:** Service Separation und Backup-Strategie
|
||||
3. **Mittelfristig:** Load Balancing und Caching
|
||||
4. **Langfristig:** Microservices und Multi-Region
|
||||
|
||||
Der Plan bietet einen gestuften Ansatz, um die Ausfallsicherheit schrittweise zu erhöhen, ohne die laufende Entwicklung zu blockieren.
|
||||
Der Plan bietet einen gestuften Ansatz, um die Ausfallsicherheit schrittweise zu erhöhen, ohne die laufende Entwicklung zu blockieren.
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
# PocketBase Development Setup - Implementierungsplan
|
||||
|
||||
## 🎯 Ziel
|
||||
|
||||
Vollständige Trennung von Development und Production Datenbanken, ähnlich wie bei Redis.
|
||||
|
||||
## 📋 Aktuelle Situation
|
||||
|
||||
### Probleme:
|
||||
|
||||
- ❌ Beide Umgebungen nutzen Production DB (`https://pb.ulo.ad`)
|
||||
- ❌ Gefahr von Testdaten in Production
|
||||
- ❌ Keine lokale Entwicklungsumgebung
|
||||
|
|
@ -13,6 +15,7 @@ Vollständige Trennung von Development und Production Datenbanken, ähnlich wie
|
|||
- ❌ Inkonsistente Environment-Variable-Nutzung
|
||||
|
||||
### Vorhandene Ressourcen:
|
||||
|
||||
- ✅ PocketBase Binary bereits in `backend/` vorhanden
|
||||
- ✅ `npm run backend` Script existiert
|
||||
- ✅ Lokale DB-Dateien in `backend/pb_data/`
|
||||
|
|
@ -23,12 +26,14 @@ Vollständige Trennung von Development und Production Datenbanken, ähnlich wie
|
|||
### Phase 1: Lokale PocketBase einrichten (15 Min)
|
||||
|
||||
1. **PocketBase starten**
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
./pocketbase serve
|
||||
```
|
||||
|
||||
- Läuft auf http://localhost:8090
|
||||
- Admin UI: http://localhost:8090/_/
|
||||
- Admin UI: http://localhost:8090/\_/
|
||||
|
||||
2. **Admin Account erstellen**
|
||||
- Beim ersten Start wird Admin-Account angelegt
|
||||
|
|
@ -43,11 +48,12 @@ Vollständige Trennung von Development und Production Datenbanken, ähnlich wie
|
|||
### Phase 2: Environment Variables korrigieren (10 Min)
|
||||
|
||||
1. **.env.development anpassen**
|
||||
|
||||
```env
|
||||
# PocketBase Configuration (Local Development)
|
||||
PUBLIC_POCKETBASE_URL=http://localhost:8090
|
||||
POCKETBASE_URL=http://localhost:8090
|
||||
|
||||
|
||||
# PocketBase Admin (for local development)
|
||||
POCKETBASE_ADMIN_EMAIL=admin@localhost
|
||||
POCKETBASE_ADMIN_PASSWORD=localdevpassword123
|
||||
|
|
@ -63,12 +69,13 @@ Vollständige Trennung von Development und Production Datenbanken, ähnlich wie
|
|||
### Phase 3: Code-Anpassungen (20 Min)
|
||||
|
||||
1. **src/lib/pocketbase.ts**
|
||||
|
||||
```typescript
|
||||
import { dev } from '$app/environment';
|
||||
|
||||
|
||||
// Automatic environment detection
|
||||
const POCKETBASE_URL = import.meta.env.PUBLIC_POCKETBASE_URL ||
|
||||
(dev ? 'http://localhost:8090' : 'https://pb.ulo.ad');
|
||||
const POCKETBASE_URL =
|
||||
import.meta.env.PUBLIC_POCKETBASE_URL || (dev ? 'http://localhost:8090' : 'https://pb.ulo.ad');
|
||||
```
|
||||
|
||||
2. **Hardcoded URLs entfernen**
|
||||
|
|
@ -82,6 +89,7 @@ Vollständige Trennung von Development und Production Datenbanken, ähnlich wie
|
|||
### Phase 4: Daten-Migration (30 Min)
|
||||
|
||||
1. **Test-Daten erstellen**
|
||||
|
||||
```bash
|
||||
# Script für Sample-Daten
|
||||
npm run seed:dev
|
||||
|
|
@ -102,15 +110,16 @@ Vollständige Trennung von Development und Production Datenbanken, ähnlich wie
|
|||
### Phase 5: Development Workflow (10 Min)
|
||||
|
||||
1. **Start-Scripts optimieren**
|
||||
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"dev": "npm run dev:frontend",
|
||||
"dev:frontend": "vite dev",
|
||||
"dev:backend": "cd backend && ./pocketbase serve",
|
||||
"dev:all": "concurrently \"npm:dev:backend\" \"npm:dev:frontend\"",
|
||||
"dev:setup": "npm run dev:backend:setup && npm run dev:seed"
|
||||
}
|
||||
"scripts": {
|
||||
"dev": "npm run dev:frontend",
|
||||
"dev:frontend": "vite dev",
|
||||
"dev:backend": "cd backend && ./pocketbase serve",
|
||||
"dev:all": "concurrently \"npm:dev:backend\" \"npm:dev:frontend\"",
|
||||
"dev:setup": "npm run dev:backend:setup && npm run dev:seed"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -121,7 +130,7 @@ Vollständige Trennung von Development und Production Datenbanken, ähnlich wie
|
|||
pocketbase:
|
||||
image: ghcr.io/pocketbase/pocketbase:latest
|
||||
ports:
|
||||
- "8090:8090"
|
||||
- '8090:8090'
|
||||
volumes:
|
||||
- ./backend/pb_data:/pb_data
|
||||
```
|
||||
|
|
@ -150,7 +159,7 @@ Vollständige Trennung von Development und Production Datenbanken, ähnlich wie
|
|||
# 1. Backend starten
|
||||
npm run dev:backend
|
||||
|
||||
# 2. In neuem Terminal: Frontend starten
|
||||
# 2. In neuem Terminal: Frontend starten
|
||||
npm run dev:frontend
|
||||
|
||||
# Oder alles zusammen:
|
||||
|
|
@ -160,29 +169,32 @@ npm run dev:all
|
|||
## ⚠️ Wichtige Überlegungen
|
||||
|
||||
### Daten-Synchronisation
|
||||
|
||||
- **NICHT** Production-Daten lokal spiegeln
|
||||
- Nur Schema/Structure synchronisieren
|
||||
- Test-Daten separat verwalten
|
||||
|
||||
### Secrets Management
|
||||
|
||||
- Lokale Admin-Credentials nur für Dev
|
||||
- Keine Production-Secrets in .env.development
|
||||
- Stripe Test-Keys für lokale Entwicklung
|
||||
|
||||
### Backup Strategy
|
||||
|
||||
- Lokale DB regelmäßig committen? (ohne sensible Daten)
|
||||
- Schema-Änderungen als Migrations tracken
|
||||
- Production Backups separat
|
||||
|
||||
## 📊 Vergleich: Vorher vs Nachher
|
||||
|
||||
| Aspekt | Vorher | Nachher |
|
||||
|--------|---------|---------|
|
||||
| Dev Database | Production (pb.ulo.ad) | Lokal (localhost:8090) |
|
||||
| Test-Daten | In Production! | Isoliert lokal |
|
||||
| Performance | Netzwerk-Latenz | Instant (lokal) |
|
||||
| Sicherheit | Risiko für Prod-Daten | Vollständig getrennt |
|
||||
| Offline-Arbeit | Nicht möglich | Voll funktionsfähig |
|
||||
| Aspekt | Vorher | Nachher |
|
||||
| -------------- | ---------------------- | ---------------------- |
|
||||
| Dev Database | Production (pb.ulo.ad) | Lokal (localhost:8090) |
|
||||
| Test-Daten | In Production! | Isoliert lokal |
|
||||
| Performance | Netzwerk-Latenz | Instant (lokal) |
|
||||
| Sicherheit | Risiko für Prod-Daten | Vollständig getrennt |
|
||||
| Offline-Arbeit | Nicht möglich | Voll funktionsfähig |
|
||||
|
||||
## 🔄 Migration Checkliste
|
||||
|
||||
|
|
@ -210,4 +222,4 @@ npm run dev:all
|
|||
2. Team informieren über Änderungen
|
||||
3. Migration durchführen
|
||||
4. Dokumentation für Team erstellen
|
||||
5. CI/CD anpassen für neue Struktur
|
||||
5. CI/CD anpassen für neue Struktur
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ Da der Schema-Import nicht funktioniert (veraltetes Format), musst du die Collec
|
|||
|
||||
## 🎯 Admin Login
|
||||
|
||||
1. Gehe zu: http://localhost:8090/_/
|
||||
1. Gehe zu: http://localhost:8090/\_/
|
||||
2. Login mit:
|
||||
- Email: `till.schneider@memoro.ai`
|
||||
- Password: `p0ck3t-RAJ`
|
||||
|
|
@ -19,21 +19,22 @@ Da der Schema-Import nicht funktioniert (veraltetes Format), musst du die Collec
|
|||
|
||||
**Fields hinzufügen (+ New field):**
|
||||
|
||||
| Field Name | Type | Required | Options |
|
||||
|------------|------|----------|---------|
|
||||
| `short_code` | text | ✅ | Unique: ✅, Min: 3, Max: 50 |
|
||||
| `original_url` | url | ✅ | - |
|
||||
| `title` | text | ❌ | Max: 200 |
|
||||
| `description` | text | ❌ | Max: 500 |
|
||||
| `user_id` | relation | ❌ | Collection: users, Max select: 1, Cascade delete: ✅ |
|
||||
| `is_active` | bool | ❌ | - |
|
||||
| `password` | text | ❌ | - |
|
||||
| `max_clicks` | number | ❌ | Min: 0 |
|
||||
| `expires_at` | date | ❌ | - |
|
||||
| `click_count` | number | ❌ | - |
|
||||
| `tags` | json | ❌ | - |
|
||||
| Field Name | Type | Required | Options |
|
||||
| -------------- | -------- | -------- | ---------------------------------------------------- |
|
||||
| `short_code` | text | ✅ | Unique: ✅, Min: 3, Max: 50 |
|
||||
| `original_url` | url | ✅ | - |
|
||||
| `title` | text | ❌ | Max: 200 |
|
||||
| `description` | text | ❌ | Max: 500 |
|
||||
| `user_id` | relation | ❌ | Collection: users, Max select: 1, Cascade delete: ✅ |
|
||||
| `is_active` | bool | ❌ | - |
|
||||
| `password` | text | ❌ | - |
|
||||
| `max_clicks` | number | ❌ | Min: 0 |
|
||||
| `expires_at` | date | ❌ | - |
|
||||
| `click_count` | number | ❌ | - |
|
||||
| `tags` | json | ❌ | - |
|
||||
|
||||
**API Rules:**
|
||||
|
||||
- List/View rule: `` (leer = public)
|
||||
- Create rule: `@request.auth.id != ""`
|
||||
- Update rule: `@request.auth.id = user_id`
|
||||
|
|
@ -47,20 +48,21 @@ Da der Schema-Import nicht funktioniert (veraltetes Format), musst du die Collec
|
|||
|
||||
**Fields:**
|
||||
|
||||
| Field Name | Type | Required | Options |
|
||||
|------------|------|----------|---------|
|
||||
| `link_id` | relation | ✅ | Collection: links, Max select: 1, Cascade delete: ✅ |
|
||||
| `ip_hash` | text | ❌ | - |
|
||||
| `user_agent` | text | ❌ | - |
|
||||
| `referer` | text | ❌ | - |
|
||||
| `browser` | text | ❌ | - |
|
||||
| `device_type` | text | ❌ | - |
|
||||
| `os` | text | ❌ | - |
|
||||
| `country` | text | ❌ | - |
|
||||
| `city` | text | ❌ | - |
|
||||
| `clicked_at` | date | ❌ | - |
|
||||
| Field Name | Type | Required | Options |
|
||||
| ------------- | -------- | -------- | ---------------------------------------------------- |
|
||||
| `link_id` | relation | ✅ | Collection: links, Max select: 1, Cascade delete: ✅ |
|
||||
| `ip_hash` | text | ❌ | - |
|
||||
| `user_agent` | text | ❌ | - |
|
||||
| `referer` | text | ❌ | - |
|
||||
| `browser` | text | ❌ | - |
|
||||
| `device_type` | text | ❌ | - |
|
||||
| `os` | text | ❌ | - |
|
||||
| `country` | text | ❌ | - |
|
||||
| `city` | text | ❌ | - |
|
||||
| `clicked_at` | date | ❌ | - |
|
||||
|
||||
**API Rules:**
|
||||
|
||||
- List/View rule: `` (leer = public)
|
||||
- Create rule: `` (leer = public)
|
||||
- Update rule: `null` (keine Updates erlaubt)
|
||||
|
|
@ -74,20 +76,21 @@ Da der Schema-Import nicht funktioniert (veraltetes Format), musst du die Collec
|
|||
|
||||
**Fields:**
|
||||
|
||||
| Field Name | Type | Required | Options |
|
||||
|------------|------|----------|---------|
|
||||
| `name` | text | ✅ | - |
|
||||
| `owner` | relation | ✅ | Collection: users, Max select: 1 |
|
||||
| `members` | relation | ❌ | Collection: users, Multiple: ✅ |
|
||||
| `isActive` | bool | ❌ | - |
|
||||
| `planType` | select | ❌ | Values: free, team, enterprise |
|
||||
| `settings` | json | ❌ | - |
|
||||
| Field Name | Type | Required | Options |
|
||||
| ---------- | -------- | -------- | -------------------------------- |
|
||||
| `name` | text | ✅ | - |
|
||||
| `owner` | relation | ✅ | Collection: users, Max select: 1 |
|
||||
| `members` | relation | ❌ | Collection: users, Multiple: ✅ |
|
||||
| `isActive` | bool | ❌ | - |
|
||||
| `planType` | select | ❌ | Values: free, team, enterprise |
|
||||
| `settings` | json | ❌ | - |
|
||||
|
||||
## ✅ Nach dem Erstellen
|
||||
|
||||
1. **Speichern** nicht vergessen (Save button oben rechts)
|
||||
|
||||
2. **Test-Daten laden:**
|
||||
|
||||
```bash
|
||||
node scripts/seed-local-db.js
|
||||
```
|
||||
|
|
@ -100,6 +103,7 @@ Da der Schema-Import nicht funktioniert (veraltetes Format), musst du die Collec
|
|||
## 🎉 Fertig!
|
||||
|
||||
Deine lokale PocketBase ist jetzt bereit mit:
|
||||
|
||||
- Admin Account ✅
|
||||
- Collections ✅
|
||||
- Test-Daten (nach Seed-Script)
|
||||
|
|
@ -107,6 +111,7 @@ Deine lokale PocketBase ist jetzt bereit mit:
|
|||
## 📝 Test-URLs
|
||||
|
||||
Nach dem Seed-Script:
|
||||
|
||||
- http://localhost:5173/test1 - Normaler Link
|
||||
- http://localhost:5173/test2 - Link mit Click-Limit
|
||||
- http://localhost:5173/protected - Passwort: `secret123`
|
||||
|
|
@ -114,9 +119,11 @@ Nach dem Seed-Script:
|
|||
## 🔍 Troubleshooting
|
||||
|
||||
**"Collection not found" Fehler?**
|
||||
|
||||
- Stelle sicher, dass alle Collections erstellt und gespeichert wurden
|
||||
- Name muss exakt sein (case-sensitive)
|
||||
|
||||
**"Invalid relation" Fehler?**
|
||||
|
||||
- Erst Links Collection erstellen, dann Clicks
|
||||
- Users Collection existiert bereits (Standard Auth)
|
||||
- Users Collection existiert bereits (Standard Auth)
|
||||
|
|
|
|||
|
|
@ -3,13 +3,15 @@
|
|||
## ✅ Was wurde implementiert
|
||||
|
||||
### 1. Environment Separation
|
||||
|
||||
- **Development**: `http://localhost:8090` (lokal)
|
||||
- **Production**: `https://pb.ulo.ad` (Coolify)
|
||||
- Automatische Umgebungserkennung basierend auf `dev` Flag
|
||||
|
||||
### 2. Code-Änderungen
|
||||
|
||||
- ✅ `.env.development` - Lokale PocketBase URL
|
||||
- ✅ `.env.production` - Production PocketBase URL
|
||||
- ✅ `.env.production` - Production PocketBase URL
|
||||
- ✅ `src/lib/pocketbase.ts` - Dynamische URL-Auswahl
|
||||
- ✅ `src/routes/p/[username]/+page.server.ts` - Environment-basierte URL
|
||||
- ✅ `src/lib/scripts/update-links-collection.js` - Flexible URL
|
||||
|
|
@ -17,6 +19,7 @@
|
|||
- ✅ `src/hooks.server.ts` - CSP Headers für beide Umgebungen
|
||||
|
||||
### 3. Sicherheit
|
||||
|
||||
- Keine hardcoded Production URLs mehr
|
||||
- Vollständige Trennung von Dev/Prod Daten
|
||||
- Lokale Entwicklung ohne Internetverbindung möglich
|
||||
|
|
@ -36,19 +39,21 @@ cd backend
|
|||
|
||||
### 2. Admin Account erstellen
|
||||
|
||||
1. Öffne http://localhost:8090/_/
|
||||
1. Öffne http://localhost:8090/\_/
|
||||
2. Erstelle Admin Account (nur beim ersten Mal)
|
||||
3. Merke dir die Credentials
|
||||
|
||||
### 3. Schema importieren
|
||||
|
||||
**Option A: Manuell über UI**
|
||||
|
||||
1. Login als Admin
|
||||
2. Settings → Import/Export
|
||||
3. Load from JSON → `backend/pb_schema.json`
|
||||
4. Review → Confirm
|
||||
|
||||
**Option B: Automatisch (wenn Collections existieren)**
|
||||
|
||||
```bash
|
||||
# Schema ist bereits in backend/pb_migrations/
|
||||
cd backend
|
||||
|
|
@ -63,6 +68,7 @@ node scripts/seed-local-db.js
|
|||
```
|
||||
|
||||
Erstellt:
|
||||
|
||||
- 2 Test-User (test@localhost, demo@localhost)
|
||||
- 4 Test-Links (normal, protected, expired, limited)
|
||||
- Sample Click-Daten
|
||||
|
|
@ -80,21 +86,24 @@ npm run dev:all
|
|||
## 📝 Test-Credentials
|
||||
|
||||
### Users
|
||||
|
||||
```
|
||||
Email: test@localhost
|
||||
Password: test123456
|
||||
|
||||
Email: demo@localhost
|
||||
Email: demo@localhost
|
||||
Password: demo123456
|
||||
```
|
||||
|
||||
### Links
|
||||
|
||||
- `http://localhost:5173/test1` - Normaler Link
|
||||
- `http://localhost:5173/test2` - Mit Click-Limit (100)
|
||||
- `http://localhost:5173/protected` - Password: `secret123`
|
||||
- `http://localhost:5173/expired` - Abgelaufener Link
|
||||
|
||||
### Admin
|
||||
|
||||
```
|
||||
URL: http://localhost:8090/_/
|
||||
Email: [deine Admin Email]
|
||||
|
|
@ -104,6 +113,7 @@ Password: [dein Admin Password]
|
|||
## 🔍 Verification
|
||||
|
||||
### 1. Environment Check
|
||||
|
||||
```bash
|
||||
# In Browser Console sollte stehen:
|
||||
🔧 PocketBase URL: http://localhost:8090
|
||||
|
|
@ -112,12 +122,14 @@ Password: [dein Admin Password]
|
|||
```
|
||||
|
||||
### 2. API Health
|
||||
|
||||
```bash
|
||||
curl http://localhost:5173/health
|
||||
# sollte zeigen: "pocketbase": "running"
|
||||
```
|
||||
|
||||
### 3. Feature Tests
|
||||
|
||||
- [ ] User Registration
|
||||
- [ ] Login/Logout
|
||||
- [ ] Link erstellen
|
||||
|
|
@ -166,15 +178,15 @@ Falls Probleme: Admin UI → Settings → API Rules
|
|||
|
||||
## 📊 Development vs Production
|
||||
|
||||
| Aspekt | Development | Production |
|
||||
|--------|------------|------------|
|
||||
| Aspekt | Development | Production |
|
||||
| -------------- | --------------------- | ----------------- |
|
||||
| PocketBase URL | http://localhost:8090 | https://pb.ulo.ad |
|
||||
| Datenbank | Lokal (SQLite) | Cloud (Coolify) |
|
||||
| Auth | Test-Accounts | Echte User |
|
||||
| Stripe | Test-Keys | Live-Keys |
|
||||
| Redis | localhost:6379 | Coolify Redis |
|
||||
| SSL | Nein (HTTP) | Ja (HTTPS) |
|
||||
| CSP | Relaxed | Strict |
|
||||
| Datenbank | Lokal (SQLite) | Cloud (Coolify) |
|
||||
| Auth | Test-Accounts | Echte User |
|
||||
| Stripe | Test-Keys | Live-Keys |
|
||||
| Redis | localhost:6379 | Coolify Redis |
|
||||
| SSL | Nein (HTTP) | Ja (HTTPS) |
|
||||
| CSP | Relaxed | Strict |
|
||||
|
||||
## 🔄 Daten-Synchronisation
|
||||
|
||||
|
|
@ -212,6 +224,7 @@ cd backend && ./pocketbase serve
|
|||
## 🚢 Deployment
|
||||
|
||||
Beim Deployment nach Production:
|
||||
|
||||
1. Environment wird automatisch erkannt
|
||||
2. Production URLs werden verwendet
|
||||
3. Keine Code-Änderungen nötig!
|
||||
|
|
@ -230,4 +243,4 @@ npm run preview
|
|||
- [PocketBase Docs](https://pocketbase.io/docs/)
|
||||
- [Admin UI Guide](https://pocketbase.io/docs/admin-ui/)
|
||||
- [API Rules](https://pocketbase.io/docs/api-rules-and-filters/)
|
||||
- [Migrations](https://pocketbase.io/docs/migrations/)
|
||||
- [Migrations](https://pocketbase.io/docs/migrations/)
|
||||
|
|
|
|||
|
|
@ -57,7 +57,6 @@ npm run dev:all
|
|||
```bash
|
||||
/app/pocketbase superuser create till.schneider@memoro.ai p0ck3t-RA1N
|
||||
```
|
||||
|
||||
|
||||
### Über Web-Interface
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
# Profile Background Color Feature
|
||||
|
||||
## Übersicht
|
||||
|
||||
Nutzer können jetzt die Hintergrundfarbe ihrer Profilseite anpassen.
|
||||
|
||||
## So funktioniert's
|
||||
|
||||
### 1. Einstellung ändern
|
||||
|
||||
1. Gehe zu **Settings** (`/settings`)
|
||||
2. Scrolle zum Abschnitt **Profile Appearance**
|
||||
3. Wähle eine Farbe aus:
|
||||
|
|
@ -13,9 +15,11 @@ Nutzer können jetzt die Hintergrundfarbe ihrer Profilseite anpassen.
|
|||
- **Presets**: Wähle aus vordefinierten Farben (Light Gray, Light Blue, Light Green, etc.)
|
||||
|
||||
### 2. Vorschau
|
||||
|
||||
Die gewählte Farbe wird sofort auf deiner Profilseite (`/p/[username]`) angezeigt.
|
||||
|
||||
## Verfügbare Preset-Farben
|
||||
|
||||
- Light Gray (Standard): `#f9fafb`
|
||||
- Light Blue: `#dbeafe`
|
||||
- Light Green: `#dcfce7`
|
||||
|
|
@ -29,15 +33,18 @@ Die gewählte Farbe wird sofort auf deiner Profilseite (`/p/[username]`) angezei
|
|||
## Technische Details
|
||||
|
||||
### Datenbank
|
||||
|
||||
- Neues Feld: `profileBackground` (String) in der `users` Collection
|
||||
- Standard: `#f9fafb` (Light Gray)
|
||||
|
||||
### Implementation
|
||||
|
||||
- Settings-Seite: Color Picker + Preset-Auswahl
|
||||
- Profile-Seite: Dynamisches CSS mit inline `style` Attribut
|
||||
- Für dunkle Farben wird ein leichter Gradient angewendet
|
||||
|
||||
### Dateien geändert
|
||||
|
||||
- `/src/routes/(app)/settings/+page.server.ts` - Server-Action für Speicherung
|
||||
- `/src/routes/(app)/settings/+page.svelte` - UI für Farbauswahl
|
||||
- `/src/routes/p/[username]/+page.server.ts` - Lade Hintergrundfarbe
|
||||
|
|
@ -45,11 +52,9 @@ Die gewählte Farbe wird sofort auf deiner Profilseite (`/p/[username]`) angezei
|
|||
- `/src/lib/pocketbase.ts` - User Type aktualisiert
|
||||
|
||||
## Beispiel
|
||||
|
||||
```html
|
||||
<div
|
||||
class="min-h-screen transition-colors duration-300"
|
||||
style="background: #dbeafe"
|
||||
>
|
||||
<!-- Profile content -->
|
||||
<div class="min-h-screen transition-colors duration-300" style="background: #dbeafe">
|
||||
<!-- Profile content -->
|
||||
</div>
|
||||
```
|
||||
```
|
||||
|
|
|
|||
|
|
@ -1,396 +1,411 @@
|
|||
Redis Cache für uLoad - Was bringt es
|
||||
wirklich?
|
||||
Redis Cache für uLoad - Was bringt es
|
||||
wirklich?
|
||||
|
||||
Das Hauptproblem aktuell
|
||||
Das Hauptproblem aktuell
|
||||
|
||||
Jedes Mal wenn jemand einen deiner
|
||||
Short-Links klickt, muss deine App:
|
||||
1. PocketBase fragen "welche URL gehört zu
|
||||
diesem Short-Code?"
|
||||
2. Warten bis PocketBase in der SQLite
|
||||
Datei sucht
|
||||
3. Antwort zurückgeben und weiterleiten
|
||||
Jedes Mal wenn jemand einen deiner
|
||||
Short-Links klickt, muss deine App:
|
||||
|
||||
Das dauert 50-100ms pro Klick. Bei vielen
|
||||
Klicks wird PocketBase zum Flaschenhals.
|
||||
1. PocketBase fragen "welche URL gehört zu
|
||||
diesem Short-Code?"
|
||||
2. Warten bis PocketBase in der SQLite
|
||||
Datei sucht
|
||||
3. Antwort zurückgeben und weiterleiten
|
||||
|
||||
Was Redis ändern würde
|
||||
Das dauert 50-100ms pro Klick. Bei vielen
|
||||
Klicks wird PocketBase zum Flaschenhals.
|
||||
|
||||
Redis ist wie ein ultra-schneller
|
||||
Zwischenspeicher. Statt jedes Mal
|
||||
PocketBase zu fragen, schaust du erst in
|
||||
Redis nach. Das ist 20-50x schneller!
|
||||
Was Redis ändern würde
|
||||
|
||||
Die 3 größten Verbesserungen
|
||||
Redis ist wie ein ultra-schneller
|
||||
Zwischenspeicher. Statt jedes Mal
|
||||
PocketBase zu fragen, schaust du erst in
|
||||
Redis nach. Das ist 20-50x schneller!
|
||||
|
||||
1. Link-Redirects (90% deines Traffics)
|
||||
Die 3 größten Verbesserungen
|
||||
|
||||
Problem: Jeder Klick = PocketBase Query =
|
||||
langsam
|
||||
Lösung: Beliebte Links werden in Redis
|
||||
gespeichert
|
||||
1. Link-Redirects (90% deines Traffics)
|
||||
|
||||
Beispiel:
|
||||
- Dein Link "ulo.ad/meeting" wird 100x am
|
||||
Problem: Jeder Klick = PocketBase Query =
|
||||
langsam
|
||||
Lösung: Beliebte Links werden in Redis
|
||||
gespeichert
|
||||
|
||||
Beispiel:
|
||||
|
||||
- Dein Link "ulo.ad/meeting" wird 100x am
|
||||
Tag geklickt
|
||||
- Ohne Redis: 100x PocketBase Query (100x
|
||||
- Ohne Redis: 100x PocketBase Query (100x
|
||||
80ms = 8 Sekunden Serverzeit)
|
||||
- Mit Redis: 1x PocketBase + 99x Redis (1x
|
||||
- Mit Redis: 1x PocketBase + 99x Redis (1x
|
||||
80ms + 99x 2ms = 0.28 Sekunden)
|
||||
|
||||
Ergebnis: 30x schneller, 95% weniger
|
||||
PocketBase Last
|
||||
Ergebnis: 30x schneller, 95% weniger
|
||||
PocketBase Last
|
||||
|
||||
2. Dashboard Performance
|
||||
2. Dashboard Performance
|
||||
|
||||
Problem: Dein Dashboard lädt alle deine
|
||||
Links aus PocketBase
|
||||
Lösung: Links werden für 5 Minuten gecacht
|
||||
Problem: Dein Dashboard lädt alle deine
|
||||
Links aus PocketBase
|
||||
Lösung: Links werden für 5 Minuten gecacht
|
||||
|
||||
Beispiel:
|
||||
- Du hast 50 Links, checkst Dashboard 20x
|
||||
Beispiel:
|
||||
|
||||
- Du hast 50 Links, checkst Dashboard 20x
|
||||
am Tag
|
||||
- Ohne Redis: 20x komplette Liste laden =
|
||||
- Ohne Redis: 20x komplette Liste laden =
|
||||
langsam
|
||||
- Mit Redis: 1x laden, 19x aus Cache =
|
||||
- Mit Redis: 1x laden, 19x aus Cache =
|
||||
blitzschnell
|
||||
|
||||
3. Analytics & Statistiken
|
||||
3. Analytics & Statistiken
|
||||
|
||||
Problem: Click-Zahlen berechnen ist langsam
|
||||
(viele Datenbankabfragen)
|
||||
Lösung: Statistiken werden live berechnet
|
||||
und gecacht
|
||||
Problem: Click-Zahlen berechnen ist langsam
|
||||
(viele Datenbankabfragen)
|
||||
Lösung: Statistiken werden live berechnet
|
||||
und gecacht
|
||||
|
||||
Beispiel:
|
||||
- "Zeige Clicks der letzten 7 Tage" braucht
|
||||
normalerweise komplizierte Queries
|
||||
- Mit Redis: Zahlen werden kontinuierlich
|
||||
Beispiel:
|
||||
|
||||
- "Zeige Clicks der letzten 7 Tage" braucht
|
||||
normalerweise komplizierte Queries
|
||||
- Mit Redis: Zahlen werden kontinuierlich
|
||||
aktualisiert, sofort verfügbar
|
||||
|
||||
Rate Limiting Verbesserung
|
||||
Rate Limiting Verbesserung
|
||||
|
||||
Aktuell: Dein Rate Limiter vergisst alles
|
||||
bei Server-Restart
|
||||
Mit Redis: Rate Limits bleiben bestehen,
|
||||
auch über Restarts hinweg
|
||||
Aktuell: Dein Rate Limiter vergisst alles
|
||||
bei Server-Restart
|
||||
Mit Redis: Rate Limits bleiben bestehen,
|
||||
auch über Restarts hinweg
|
||||
|
||||
Das bedeutet: Angreifer können nicht mehr
|
||||
durch Server-Restart deine Limits umgehen.
|
||||
Das bedeutet: Angreifer können nicht mehr
|
||||
durch Server-Restart deine Limits umgehen.
|
||||
|
||||
Konkrete Zahlen für dich
|
||||
Konkrete Zahlen für dich
|
||||
|
||||
Traffic-Verteilung schätzen:
|
||||
Traffic-Verteilung schätzen:
|
||||
|
||||
- 90% Redirects (Leute klicken deine Links)
|
||||
- 8% Dashboard/API (Du managst deine Links)
|
||||
- 90% Redirects (Leute klicken deine Links)
|
||||
- 8% Dashboard/API (Du managst deine Links)
|
||||
|
||||
- 2% Sonstiges (Login, Settings, etc.)
|
||||
- 2% Sonstiges (Login, Settings, etc.)
|
||||
|
||||
Performance ohne Redis:
|
||||
Performance ohne Redis:
|
||||
|
||||
- Redirect: 80ms durchschnittlich
|
||||
- Dashboard laden: 300ms
|
||||
- Bei 1000 Klicks/Tag: PocketBase ist
|
||||
- Redirect: 80ms durchschnittlich
|
||||
- Dashboard laden: 300ms
|
||||
- Bei 1000 Klicks/Tag: PocketBase ist
|
||||
gestresst
|
||||
|
||||
Performance mit Redis:
|
||||
Performance mit Redis:
|
||||
|
||||
- Redirect: 5ms (95% aus Cache)
|
||||
- Dashboard laden: 20ms (meist aus Cache)
|
||||
- Bei 10.000 Klicks/Tag: PocketBase
|
||||
- Redirect: 5ms (95% aus Cache)
|
||||
- Dashboard laden: 20ms (meist aus Cache)
|
||||
- Bei 10.000 Klicks/Tag: PocketBase
|
||||
entspannt
|
||||
|
||||
Wann merkst du den Unterschied?
|
||||
Wann merkst du den Unterschied?
|
||||
|
||||
Sofort spürbar:
|
||||
Sofort spürbar:
|
||||
|
||||
- Dashboard lädt viel schneller (besonders
|
||||
- Dashboard lädt viel schneller (besonders
|
||||
mit vielen Links)
|
||||
- Redirects sind blitzschnell
|
||||
- Analytics laden sofort statt zu "denken"
|
||||
- Redirects sind blitzschnell
|
||||
- Analytics laden sofort statt zu "denken"
|
||||
|
||||
Bei mehr Traffic:
|
||||
Bei mehr Traffic:
|
||||
|
||||
- Server bleibt entspannt auch bei
|
||||
- Server bleibt entspannt auch bei
|
||||
Viral-Links
|
||||
- Keine Timeouts mehr bei hoher Last
|
||||
- PocketBase überlastet nicht
|
||||
- Keine Timeouts mehr bei hoher Last
|
||||
- PocketBase überlastet nicht
|
||||
|
||||
Skalierung:
|
||||
Skalierung:
|
||||
|
||||
- Ohne Redis: ~500 Redirects/Minute machbar
|
||||
- Mit Redis: ~5000+ Redirects/Minute
|
||||
- Ohne Redis: ~500 Redirects/Minute machbar
|
||||
- Mit Redis: ~5000+ Redirects/Minute
|
||||
machbar
|
||||
|
||||
Kosten vs. Nutzen
|
||||
Kosten vs. Nutzen
|
||||
|
||||
Option 1: Kostenlos starten
|
||||
Option 1: Kostenlos starten
|
||||
|
||||
- Upstash Redis (10k Commands/Tag gratis)
|
||||
- Perfekt zum Testen
|
||||
- Upgrade wenn nötig
|
||||
- Upstash Redis (10k Commands/Tag gratis)
|
||||
- Perfekt zum Testen
|
||||
- Upgrade wenn nötig
|
||||
|
||||
Option 2: Minimale Kosten
|
||||
Option 2: Minimale Kosten
|
||||
|
||||
- €5-15/Monat für ordentlichen Redis
|
||||
- Dafür 10x bessere Performance
|
||||
- €5-15/Monat für ordentlichen Redis
|
||||
- Dafür 10x bessere Performance
|
||||
|
||||
Was du sparst:
|
||||
Was du sparst:
|
||||
|
||||
- Weniger Server-Stress = längere
|
||||
- Weniger Server-Stress = längere
|
||||
Hardware-Lebensdauer
|
||||
- Bessere User Experience = mehr zufriedene
|
||||
Nutzer
|
||||
- Skalierung möglich ohne neuen Server
|
||||
- Bessere User Experience = mehr zufriedene
|
||||
Nutzer
|
||||
- Skalierung möglich ohne neuen Server
|
||||
|
||||
Die Realität für uLoad
|
||||
Die Realität für uLoad
|
||||
|
||||
Jetzt: Bei 100 Klicks gleichzeitig würde
|
||||
dein Server wahrscheinlich strugglen
|
||||
Mit Redis: Bei 1000 Klicks gleichzeitig
|
||||
läuft alles smooth
|
||||
Jetzt: Bei 100 Klicks gleichzeitig würde
|
||||
dein Server wahrscheinlich strugglen
|
||||
Mit Redis: Bei 1000 Klicks gleichzeitig
|
||||
läuft alles smooth
|
||||
|
||||
Jetzt: Dashboard mit 100 Links braucht eine
|
||||
Sekunde zum Laden
|
||||
Mit Redis: Dashboard lädt in 0.1 Sekunden
|
||||
Jetzt: Dashboard mit 100 Links braucht eine
|
||||
Sekunde zum Laden
|
||||
Mit Redis: Dashboard lädt in 0.1 Sekunden
|
||||
|
||||
Jetzt: Click-Statistiken berechnen dauert
|
||||
ewig
|
||||
Mit Redis: Statistiken sind immer aktuell
|
||||
und sofort da
|
||||
Jetzt: Click-Statistiken berechnen dauert
|
||||
ewig
|
||||
Mit Redis: Statistiken sind immer aktuell
|
||||
und sofort da
|
||||
|
||||
Meine ehrliche Einschätzung
|
||||
Meine ehrliche Einschätzung
|
||||
|
||||
Für uLoad ist Redis ein No-Brainer:
|
||||
- Minimaler Aufwand (2-3 Tage Setup)
|
||||
- Maximaler Nutzen (10x Performance)
|
||||
- Kosten fast null (kann gratis starten)
|
||||
Für uLoad ist Redis ein No-Brainer:
|
||||
|
||||
Der größte Gewinn ist bei Redirects - das
|
||||
ist dein Kernfeature und wird damit
|
||||
ultra-schnell. Plus du bist ready für
|
||||
Viral-Links die plötzlich 1000x geklickt
|
||||
werden.
|
||||
- Minimaler Aufwand (2-3 Tage Setup)
|
||||
- Maximaler Nutzen (10x Performance)
|
||||
- Kosten fast null (kann gratis starten)
|
||||
|
||||
Bottom Line: Redis macht aus deinem "hobby
|
||||
project" eine professionelle, skalierbare
|
||||
Anwendung für praktisch null Zusatzkosten.
|
||||
Das ist vermutlich das beste ROI was du
|
||||
kriegen kannst! 🎯
|
||||
Der größte Gewinn ist bei Redirects - das
|
||||
ist dein Kernfeature und wird damit
|
||||
ultra-schnell. Plus du bist ready für
|
||||
Viral-Links die plötzlich 1000x geklickt
|
||||
werden.
|
||||
|
||||
> super danke. wir können ja reddis auch
|
||||
einfach auf unserem hetzter vps mit coolify
|
||||
hochfahren oder? was brucht es an
|
||||
reccourcen? was mus man beachten? sollte es
|
||||
lieber auf einem adnere server liegen?
|
||||
sollte es im gleichen project in coolify
|
||||
liegen?
|
||||
Bottom Line: Redis macht aus deinem "hobby
|
||||
project" eine professionelle, skalierbare
|
||||
Anwendung für praktisch null Zusatzkosten.
|
||||
Das ist vermutlich das beste ROI was du
|
||||
kriegen kannst! 🎯
|
||||
|
||||
⏺ Redis auf Hetzner VPS mit Coolify -
|
||||
Perfekte Idee!
|
||||
> super danke. wir können ja reddis auch
|
||||
> einfach auf unserem hetzter vps mit coolify
|
||||
> hochfahren oder? was brucht es an
|
||||
> reccourcen? was mus man beachten? sollte es
|
||||
> lieber auf einem adnere server liegen?
|
||||
> sollte es im gleichen project in coolify
|
||||
> liegen?
|
||||
|
||||
Ja, absolut machbar und sinnvoll!
|
||||
⏺ Redis auf Hetzner VPS mit Coolify -
|
||||
Perfekte Idee!
|
||||
|
||||
Dein aktueller Hetzner VPS kann Redis
|
||||
problemlos mit hosten. Coolify macht das
|
||||
super einfach.
|
||||
Ja, absolut machbar und sinnvoll!
|
||||
|
||||
Resource-Bedarf von Redis
|
||||
Dein aktueller Hetzner VPS kann Redis
|
||||
problemlos mit hosten. Coolify macht das
|
||||
super einfach.
|
||||
|
||||
RAM (das Wichtigste):
|
||||
Resource-Bedarf von Redis
|
||||
|
||||
- Redis lädt alles in den RAM - das macht
|
||||
RAM (das Wichtigste):
|
||||
|
||||
- Redis lädt alles in den RAM - das macht
|
||||
es so schnell
|
||||
- Für uLoad geschätzt: 50-200MB RAM
|
||||
- Link Cache: ~1MB pro 10.000 Links
|
||||
- Session Cache: ~10MB für 1000 aktive
|
||||
User
|
||||
- Analytics: ~20-50MB je nach History
|
||||
- Rate Limiting: ~5-10MB
|
||||
- Für uLoad geschätzt: 50-200MB RAM
|
||||
- Link Cache: ~1MB pro 10.000 Links
|
||||
- Session Cache: ~10MB für 1000 aktive
|
||||
User
|
||||
- Analytics: ~20-50MB je nach History
|
||||
- Rate Limiting: ~5-10MB
|
||||
|
||||
CPU:
|
||||
CPU:
|
||||
|
||||
- Minimal! Redis ist extrem CPU-effizient
|
||||
- Für uLoad: <1% CPU Auslastung
|
||||
- Nur bei komplexen Operationen mehr
|
||||
- Minimal! Redis ist extrem CPU-effizient
|
||||
- Für uLoad: <1% CPU Auslastung
|
||||
- Nur bei komplexen Operationen mehr
|
||||
|
||||
Disk:
|
||||
Disk:
|
||||
|
||||
- Persistence: 100-500MB für Dumps
|
||||
- Logs: 10-50MB
|
||||
- Total: <1GB
|
||||
- Persistence: 100-500MB für Dumps
|
||||
- Logs: 10-50MB
|
||||
- Total: <1GB
|
||||
|
||||
Dein CX21 VPS Kapazität:
|
||||
Dein CX21 VPS Kapazität:
|
||||
|
||||
- RAM: 8GB total, aktuell ~2GB verwendet →
|
||||
- RAM: 8GB total, aktuell ~2GB verwendet →
|
||||
6GB frei!
|
||||
- CPU: 2 vCPUs, aktuell <20% → Mehr als
|
||||
- CPU: 2 vCPUs, aktuell <20% → Mehr als
|
||||
genug
|
||||
- Disk: 40GB, Redis braucht <1GB → Kein
|
||||
- Disk: 40GB, Redis braucht <1GB → Kein
|
||||
Problem
|
||||
|
||||
Fazit: Dein Server kann Redis locker
|
||||
verkraften! 🎯
|
||||
Fazit: Dein Server kann Redis locker
|
||||
verkraften! 🎯
|
||||
|
||||
Coolify Setup - zwei Optionen
|
||||
Coolify Setup - zwei Optionen
|
||||
|
||||
Option 1: Gleiches Projekt (Empfohlen für
|
||||
Start)
|
||||
Option 1: Gleiches Projekt (Empfohlen für
|
||||
Start)
|
||||
|
||||
# Einfach Redis Service zu deinem
|
||||
bestehenden docker-compose hinzufügen
|
||||
services:
|
||||
app:
|
||||
# Deine bestehende App
|
||||
# Einfach Redis Service zu deinem
|
||||
|
||||
bestehenden docker-compose hinzufügen
|
||||
services:
|
||||
app: # Deine bestehende App
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
command: redis-server --appendonly yes
|
||||
--maxmemory 512mb --maxmemory-policy
|
||||
allkeys-lru
|
||||
volumes:
|
||||
- redis-data:/data
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
|
||||
volumes:
|
||||
redis-data:
|
||||
--maxmemory 512mb --maxmemory-policy
|
||||
allkeys-lru
|
||||
volumes: - redis-data:/data
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
|
||||
Option 2: Separates Projekt in Coolify
|
||||
volumes:
|
||||
redis-data:
|
||||
|
||||
- Vorteil: Unabhängiges Deployment von
|
||||
Option 2: Separates Projekt in Coolify
|
||||
|
||||
- Vorteil: Unabhängiges Deployment von
|
||||
deiner App
|
||||
- Nachteil: Etwas komplizierter zu
|
||||
- Nachteil: Etwas komplizierter zu
|
||||
verwalten
|
||||
|
||||
Ich empfehle Option 1 - Redis ist so
|
||||
stabil, das macht keinen Unterschied.
|
||||
Ich empfehle Option 1 - Redis ist so
|
||||
stabil, das macht keinen Unterschied.
|
||||
|
||||
Coolify Konfiguration
|
||||
Coolify Konfiguration
|
||||
|
||||
Redis Environment Variables:
|
||||
Redis Environment Variables:
|
||||
|
||||
# In Coolify für Redis Service setzen:
|
||||
REDIS_PASSWORD=dein-sicheres-passwort-hier
|
||||
REDIS_MAXMEMORY=512mb
|
||||
REDIS_SAVE_INTERVAL=900 # Auto-save alle
|
||||
15 Min
|
||||
# In Coolify für Redis Service setzen:
|
||||
|
||||
Deine App erweitern:
|
||||
REDIS_PASSWORD=dein-sicheres-passwort-hier
|
||||
REDIS_MAXMEMORY=512mb
|
||||
REDIS_SAVE_INTERVAL=900 # Auto-save alle
|
||||
15 Min
|
||||
|
||||
# Environment Variable für deine App
|
||||
hinzufügen:
|
||||
REDIS_URL=redis://:dein-passwort@redis:6379
|
||||
Deine App erweitern:
|
||||
|
||||
Networking in Coolify:
|
||||
# Environment Variable für deine App
|
||||
|
||||
- Redis und App sind automatisch im
|
||||
hinzufügen:
|
||||
REDIS_URL=redis://:dein-passwort@redis:6379
|
||||
|
||||
Networking in Coolify:
|
||||
|
||||
- Redis und App sind automatisch im
|
||||
gleichen Docker Network
|
||||
- Deine App kann Redis über redis:6379
|
||||
- Deine App kann Redis über redis:6379
|
||||
erreichen
|
||||
- Kein Port nach außen öffnen nötig
|
||||
- Kein Port nach außen öffnen nötig
|
||||
(Sicherheit!)
|
||||
|
||||
Wichtige Redis Konfiguration
|
||||
Wichtige Redis Konfiguration
|
||||
|
||||
Memory Management:
|
||||
Memory Management:
|
||||
|
||||
# Redis Config (über Command Parameter):
|
||||
--maxmemory 512mb # Begrenze RAM Nutzung
|
||||
--maxmemory-policy allkeys-lru # Alte Keys
|
||||
löschen wenn voll
|
||||
# Redis Config (über Command Parameter):
|
||||
|
||||
Persistence:
|
||||
--maxmemory 512mb # Begrenze RAM Nutzung
|
||||
--maxmemory-policy allkeys-lru # Alte Keys
|
||||
löschen wenn voll
|
||||
|
||||
# Zwei Optionen kombinieren:
|
||||
--appendonly yes # AOF für Durability
|
||||
--save 900 1 # RDB Snapshot alle 15
|
||||
Min
|
||||
Persistence:
|
||||
|
||||
Security:
|
||||
# Zwei Optionen kombinieren:
|
||||
|
||||
# Passwort setzen:
|
||||
--requirepass dein-starkes-passwort
|
||||
--appendonly yes # AOF für Durability
|
||||
--save 900 1 # RDB Snapshot alle 15
|
||||
Min
|
||||
|
||||
# Gefährliche Commands deaktivieren:
|
||||
--rename-command FLUSHDB ""
|
||||
--rename-command FLUSHALL ""
|
||||
Security:
|
||||
|
||||
Monitoring in Coolify
|
||||
# Passwort setzen:
|
||||
|
||||
Redis Logs überwachen:
|
||||
--requirepass dein-starkes-passwort
|
||||
|
||||
- Coolify zeigt Redis Logs in Echtzeit
|
||||
- Achte auf Memory Warnings
|
||||
- Connection Errors beobachten
|
||||
# Gefährliche Commands deaktivieren:
|
||||
|
||||
Wichtige Redis Metriken:
|
||||
--rename-command FLUSHDB ""
|
||||
--rename-command FLUSHALL ""
|
||||
|
||||
# Commands zum Monitoring (über redis-cli):
|
||||
INFO memory # RAM Nutzung
|
||||
INFO stats # Operations/sec
|
||||
INFO clients # Verbindungen
|
||||
MONITOR # Live Commands (nur
|
||||
zum Debugging!)
|
||||
Monitoring in Coolify
|
||||
|
||||
Backup Strategy
|
||||
Redis Logs überwachen:
|
||||
|
||||
Automatische Redis Backups:
|
||||
- Coolify zeigt Redis Logs in Echtzeit
|
||||
- Achte auf Memory Warnings
|
||||
- Connection Errors beobachten
|
||||
|
||||
# Script in deinem Container:
|
||||
#!/bin/bash
|
||||
# redis-backup.sh
|
||||
DATE=$(date +%Y%m%d_%H%M%S)
|
||||
Wichtige Redis Metriken:
|
||||
|
||||
# Commands zum Monitoring (über redis-cli):
|
||||
|
||||
INFO memory # RAM Nutzung
|
||||
INFO stats # Operations/sec
|
||||
INFO clients # Verbindungen
|
||||
MONITOR # Live Commands (nur
|
||||
zum Debugging!)
|
||||
|
||||
Backup Strategy
|
||||
|
||||
Automatische Redis Backups:
|
||||
|
||||
# Script in deinem Container:
|
||||
|
||||
#!/bin/bash
|
||||
|
||||
# redis-backup.sh
|
||||
|
||||
DATE=$(date +%Y%m%d_%H%M%S)
|
||||
redis-cli --rdb
|
||||
/backups/redis_backup_$DATE.rdb
|
||||
|
||||
# In Coolify als Cron Job alle 6 Stunden
|
||||
# In Coolify als Cron Job alle 6 Stunden
|
||||
|
||||
Oder einfacher:
|
||||
Oder einfacher:
|
||||
|
||||
# Volume Mount für Persistence reicht:
|
||||
volumes:
|
||||
- redis-data:/data # Redis saved
|
||||
automatisch hier
|
||||
# Volume Mount für Persistence reicht:
|
||||
|
||||
Separater Server - wann sinnvoll?
|
||||
volumes: - redis-data:/data # Redis saved
|
||||
automatisch hier
|
||||
|
||||
Bleib bei einem Server wenn:
|
||||
Separater Server - wann sinnvoll?
|
||||
|
||||
- <10.000 Requests/Tag (definitiv dein
|
||||
Bleib bei einem Server wenn:
|
||||
|
||||
- <10.000 Requests/Tag (definitiv dein
|
||||
Fall)
|
||||
- <1000 gleichzeitige User
|
||||
- Development/Early Stage (definitiv dein
|
||||
- <1000 gleichzeitige User
|
||||
- Development/Early Stage (definitiv dein
|
||||
Fall)
|
||||
|
||||
Separater Redis Server nur bei:
|
||||
Separater Redis Server nur bei:
|
||||
|
||||
- >100.000 Requests/Tag
|
||||
- Multi-Region Setup
|
||||
- Dedicated Redis Cluster mit Failover
|
||||
- > 100.000 Requests/Tag
|
||||
- Multi-Region Setup
|
||||
- Dedicated Redis Cluster mit Failover
|
||||
|
||||
Für uLoad: Ein Server reicht völlig!
|
||||
Für uLoad: Ein Server reicht völlig!
|
||||
|
||||
Coolify Deployment Steps
|
||||
Coolify Deployment Steps
|
||||
|
||||
1. Docker Compose erweitern:
|
||||
1. Docker Compose erweitern:
|
||||
|
||||
# Zu deiner bestehenden docker-compose.yml
|
||||
hinzufügen:
|
||||
services:
|
||||
# ... deine bestehenden Services
|
||||
# Zu deiner bestehenden docker-compose.yml
|
||||
|
||||
hinzufügen:
|
||||
services: # ... deine bestehenden Services
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
command: >
|
||||
redis-server
|
||||
--appendonly yes
|
||||
--maxmemory 512mb
|
||||
redis-server
|
||||
--appendonly yes
|
||||
--maxmemory 512mb
|
||||
--maxmemory-policy allkeys-lru
|
||||
--requirepass ${REDIS_PASSWORD}
|
||||
volumes:
|
||||
|
|
@ -400,92 +415,99 @@ Redis Cache für uLoad - Was bringt es
|
|||
- app-network
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "-a",
|
||||
"${REDIS_PASSWORD}", "ping"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
|
||||
volumes:
|
||||
redis-data:
|
||||
"${REDIS_PASSWORD}", "ping"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
|
||||
networks:
|
||||
app-network:
|
||||
driver: bridge
|
||||
volumes:
|
||||
redis-data:
|
||||
|
||||
2. Environment Variables in Coolify:
|
||||
networks:
|
||||
app-network:
|
||||
driver: bridge
|
||||
|
||||
# Für Redis:
|
||||
REDIS_PASSWORD=SuperSecurePassword123!
|
||||
2. Environment Variables in Coolify:
|
||||
|
||||
# Für deine App:
|
||||
REDIS_URL=redis://:SuperSecurePassword123!@
|
||||
redis:6379
|
||||
# Für Redis:
|
||||
|
||||
3. Deploy & Test:
|
||||
REDIS_PASSWORD=SuperSecurePassword123!
|
||||
|
||||
- Coolify deployed automatisch
|
||||
- Teste Connection aus deiner App
|
||||
- Check Redis Logs für Errors
|
||||
# Für deine App:
|
||||
|
||||
Was beachten?
|
||||
REDIS_URL=redis://:SuperSecurePassword123!@
|
||||
redis:6379
|
||||
|
||||
Security:
|
||||
3. Deploy & Test:
|
||||
|
||||
- Passwort setzen (Redis hat standardmäßig
|
||||
- Coolify deployed automatisch
|
||||
- Teste Connection aus deiner App
|
||||
- Check Redis Logs für Errors
|
||||
|
||||
Was beachten?
|
||||
|
||||
Security:
|
||||
|
||||
- Passwort setzen (Redis hat standardmäßig
|
||||
keins!)
|
||||
- Port nicht öffentlich machen (nur intern)
|
||||
- Command Renaming für gefährliche Commands
|
||||
- Port nicht öffentlich machen (nur intern)
|
||||
- Command Renaming für gefährliche Commands
|
||||
|
||||
Performance:
|
||||
Performance:
|
||||
|
||||
- Memory Limit setzen (512MB reicht für
|
||||
- Memory Limit setzen (512MB reicht für
|
||||
Start)
|
||||
- LRU Policy aktivieren (alte Keys werden
|
||||
- LRU Policy aktivieren (alte Keys werden
|
||||
gelöscht)
|
||||
- Connection Pooling in deiner App
|
||||
- Connection Pooling in deiner App
|
||||
|
||||
Monitoring:
|
||||
Monitoring:
|
||||
|
||||
- Redis Logs in Coolify beobachten
|
||||
- Memory Usage im Auge behalten
|
||||
- Hit Rate messen (wie oft Cache getroffen
|
||||
- Redis Logs in Coolify beobachten
|
||||
- Memory Usage im Auge behalten
|
||||
- Hit Rate messen (wie oft Cache getroffen
|
||||
wird)
|
||||
|
||||
Troubleshooting
|
||||
Troubleshooting
|
||||
|
||||
Häufige Probleme:
|
||||
Häufige Probleme:
|
||||
|
||||
1. Connection refused: Redis Service nicht
|
||||
gestartet
|
||||
2. Auth failed: Passwort falsch oder nicht
|
||||
gesetzt
|
||||
3. Memory full: Maxmemory erreicht, LRU
|
||||
Policy prüfen
|
||||
1. Connection refused: Redis Service nicht
|
||||
gestartet
|
||||
2. Auth failed: Passwort falsch oder nicht
|
||||
gesetzt
|
||||
3. Memory full: Maxmemory erreicht, LRU
|
||||
Policy prüfen
|
||||
|
||||
Debug Commands:
|
||||
Debug Commands:
|
||||
|
||||
# In Coolify Terminal (Redis Container):
|
||||
redis-cli -a dein-passwort info
|
||||
redis-cli -a dein-passwort ping
|
||||
redis-cli -a dein-passwort client list
|
||||
# In Coolify Terminal (Redis Container):
|
||||
|
||||
Meine Empfehlung
|
||||
redis-cli -a dein-passwort info
|
||||
redis-cli -a dein-passwort ping
|
||||
redis-cli -a dein-passwort client list
|
||||
|
||||
Setup für uLoad:
|
||||
1. Gleicher Server (Hetzner VPS) ✅
|
||||
2. Gleiches Coolify Projekt ✅
|
||||
3. 512MB Memory Limit ✅
|
||||
4. Passwort-geschützt ✅
|
||||
5. Persistence aktiviert ✅
|
||||
Meine Empfehlung
|
||||
|
||||
Vorteile:
|
||||
- Einfachste Lösung
|
||||
- Keine Extra-Kosten
|
||||
- Latenz minimal (gleicher Server)
|
||||
- Easy zu verwalten
|
||||
Setup für uLoad:
|
||||
|
||||
Nachteile:
|
||||
- Keine (für deine Use Case)
|
||||
1. Gleicher Server (Hetzner VPS) ✅
|
||||
2. Gleiches Coolify Projekt ✅
|
||||
3. 512MB Memory Limit ✅
|
||||
4. Passwort-geschützt ✅
|
||||
5. Persistence aktiviert ✅
|
||||
|
||||
Redis auf deinem bestehenden Setup ist der
|
||||
perfekte nächste Schritt! 🚀
|
||||
Vorteile:
|
||||
|
||||
- Einfachste Lösung
|
||||
- Keine Extra-Kosten
|
||||
- Latenz minimal (gleicher Server)
|
||||
- Easy zu verwalten
|
||||
|
||||
Nachteile:
|
||||
|
||||
- Keine (für deine Use Case)
|
||||
|
||||
Redis auf deinem bestehenden Setup ist der
|
||||
perfekte nächste Schritt! 🚀
|
||||
|
|
|
|||
|
|
@ -1,180 +1,185 @@
|
|||
Redis Cache für uLoad - Was bringt es
|
||||
wirklich?
|
||||
Redis Cache für uLoad - Was bringt es
|
||||
wirklich?
|
||||
|
||||
Das Hauptproblem aktuell
|
||||
Das Hauptproblem aktuell
|
||||
|
||||
Jedes Mal wenn jemand einen deiner
|
||||
Short-Links klickt, muss deine App:
|
||||
1. PocketBase fragen "welche URL gehört zu
|
||||
diesem Short-Code?"
|
||||
2. Warten bis PocketBase in der SQLite
|
||||
Datei sucht
|
||||
3. Antwort zurückgeben und weiterleiten
|
||||
Jedes Mal wenn jemand einen deiner
|
||||
Short-Links klickt, muss deine App:
|
||||
|
||||
Das dauert 50-100ms pro Klick. Bei vielen
|
||||
Klicks wird PocketBase zum Flaschenhals.
|
||||
1. PocketBase fragen "welche URL gehört zu
|
||||
diesem Short-Code?"
|
||||
2. Warten bis PocketBase in der SQLite
|
||||
Datei sucht
|
||||
3. Antwort zurückgeben und weiterleiten
|
||||
|
||||
Was Redis ändern würde
|
||||
Das dauert 50-100ms pro Klick. Bei vielen
|
||||
Klicks wird PocketBase zum Flaschenhals.
|
||||
|
||||
Redis ist wie ein ultra-schneller
|
||||
Zwischenspeicher. Statt jedes Mal
|
||||
PocketBase zu fragen, schaust du erst in
|
||||
Redis nach. Das ist 20-50x schneller!
|
||||
Was Redis ändern würde
|
||||
|
||||
Die 3 größten Verbesserungen
|
||||
Redis ist wie ein ultra-schneller
|
||||
Zwischenspeicher. Statt jedes Mal
|
||||
PocketBase zu fragen, schaust du erst in
|
||||
Redis nach. Das ist 20-50x schneller!
|
||||
|
||||
1. Link-Redirects (90% deines Traffics)
|
||||
Die 3 größten Verbesserungen
|
||||
|
||||
Problem: Jeder Klick = PocketBase Query =
|
||||
langsam
|
||||
Lösung: Beliebte Links werden in Redis
|
||||
gespeichert
|
||||
1. Link-Redirects (90% deines Traffics)
|
||||
|
||||
Beispiel:
|
||||
- Dein Link "ulo.ad/meeting" wird 100x am
|
||||
Problem: Jeder Klick = PocketBase Query =
|
||||
langsam
|
||||
Lösung: Beliebte Links werden in Redis
|
||||
gespeichert
|
||||
|
||||
Beispiel:
|
||||
|
||||
- Dein Link "ulo.ad/meeting" wird 100x am
|
||||
Tag geklickt
|
||||
- Ohne Redis: 100x PocketBase Query (100x
|
||||
- Ohne Redis: 100x PocketBase Query (100x
|
||||
80ms = 8 Sekunden Serverzeit)
|
||||
- Mit Redis: 1x PocketBase + 99x Redis (1x
|
||||
- Mit Redis: 1x PocketBase + 99x Redis (1x
|
||||
80ms + 99x 2ms = 0.28 Sekunden)
|
||||
|
||||
Ergebnis: 30x schneller, 95% weniger
|
||||
PocketBase Last
|
||||
Ergebnis: 30x schneller, 95% weniger
|
||||
PocketBase Last
|
||||
|
||||
2. Dashboard Performance
|
||||
2. Dashboard Performance
|
||||
|
||||
Problem: Dein Dashboard lädt alle deine
|
||||
Links aus PocketBase
|
||||
Lösung: Links werden für 5 Minuten gecacht
|
||||
Problem: Dein Dashboard lädt alle deine
|
||||
Links aus PocketBase
|
||||
Lösung: Links werden für 5 Minuten gecacht
|
||||
|
||||
Beispiel:
|
||||
- Du hast 50 Links, checkst Dashboard 20x
|
||||
Beispiel:
|
||||
|
||||
- Du hast 50 Links, checkst Dashboard 20x
|
||||
am Tag
|
||||
- Ohne Redis: 20x komplette Liste laden =
|
||||
- Ohne Redis: 20x komplette Liste laden =
|
||||
langsam
|
||||
- Mit Redis: 1x laden, 19x aus Cache =
|
||||
- Mit Redis: 1x laden, 19x aus Cache =
|
||||
blitzschnell
|
||||
|
||||
3. Analytics & Statistiken
|
||||
3. Analytics & Statistiken
|
||||
|
||||
Problem: Click-Zahlen berechnen ist langsam
|
||||
(viele Datenbankabfragen)
|
||||
Lösung: Statistiken werden live berechnet
|
||||
und gecacht
|
||||
Problem: Click-Zahlen berechnen ist langsam
|
||||
(viele Datenbankabfragen)
|
||||
Lösung: Statistiken werden live berechnet
|
||||
und gecacht
|
||||
|
||||
Beispiel:
|
||||
- "Zeige Clicks der letzten 7 Tage" braucht
|
||||
normalerweise komplizierte Queries
|
||||
- Mit Redis: Zahlen werden kontinuierlich
|
||||
Beispiel:
|
||||
|
||||
- "Zeige Clicks der letzten 7 Tage" braucht
|
||||
normalerweise komplizierte Queries
|
||||
- Mit Redis: Zahlen werden kontinuierlich
|
||||
aktualisiert, sofort verfügbar
|
||||
|
||||
Rate Limiting Verbesserung
|
||||
Rate Limiting Verbesserung
|
||||
|
||||
Aktuell: Dein Rate Limiter vergisst alles
|
||||
bei Server-Restart
|
||||
Mit Redis: Rate Limits bleiben bestehen,
|
||||
auch über Restarts hinweg
|
||||
Aktuell: Dein Rate Limiter vergisst alles
|
||||
bei Server-Restart
|
||||
Mit Redis: Rate Limits bleiben bestehen,
|
||||
auch über Restarts hinweg
|
||||
|
||||
Das bedeutet: Angreifer können nicht mehr
|
||||
durch Server-Restart deine Limits umgehen.
|
||||
Das bedeutet: Angreifer können nicht mehr
|
||||
durch Server-Restart deine Limits umgehen.
|
||||
|
||||
Konkrete Zahlen für dich
|
||||
Konkrete Zahlen für dich
|
||||
|
||||
Traffic-Verteilung schätzen:
|
||||
Traffic-Verteilung schätzen:
|
||||
|
||||
- 90% Redirects (Leute klicken deine Links)
|
||||
- 8% Dashboard/API (Du managst deine Links)
|
||||
- 90% Redirects (Leute klicken deine Links)
|
||||
- 8% Dashboard/API (Du managst deine Links)
|
||||
|
||||
- 2% Sonstiges (Login, Settings, etc.)
|
||||
- 2% Sonstiges (Login, Settings, etc.)
|
||||
|
||||
Performance ohne Redis:
|
||||
Performance ohne Redis:
|
||||
|
||||
- Redirect: 80ms durchschnittlich
|
||||
- Dashboard laden: 300ms
|
||||
- Bei 1000 Klicks/Tag: PocketBase ist
|
||||
- Redirect: 80ms durchschnittlich
|
||||
- Dashboard laden: 300ms
|
||||
- Bei 1000 Klicks/Tag: PocketBase ist
|
||||
gestresst
|
||||
|
||||
Performance mit Redis:
|
||||
Performance mit Redis:
|
||||
|
||||
- Redirect: 5ms (95% aus Cache)
|
||||
- Dashboard laden: 20ms (meist aus Cache)
|
||||
- Bei 10.000 Klicks/Tag: PocketBase
|
||||
- Redirect: 5ms (95% aus Cache)
|
||||
- Dashboard laden: 20ms (meist aus Cache)
|
||||
- Bei 10.000 Klicks/Tag: PocketBase
|
||||
entspannt
|
||||
|
||||
Wann merkst du den Unterschied?
|
||||
Wann merkst du den Unterschied?
|
||||
|
||||
Sofort spürbar:
|
||||
Sofort spürbar:
|
||||
|
||||
- Dashboard lädt viel schneller (besonders
|
||||
- Dashboard lädt viel schneller (besonders
|
||||
mit vielen Links)
|
||||
- Redirects sind blitzschnell
|
||||
- Analytics laden sofort statt zu "denken"
|
||||
- Redirects sind blitzschnell
|
||||
- Analytics laden sofort statt zu "denken"
|
||||
|
||||
Bei mehr Traffic:
|
||||
Bei mehr Traffic:
|
||||
|
||||
- Server bleibt entspannt auch bei
|
||||
- Server bleibt entspannt auch bei
|
||||
Viral-Links
|
||||
- Keine Timeouts mehr bei hoher Last
|
||||
- PocketBase überlastet nicht
|
||||
- Keine Timeouts mehr bei hoher Last
|
||||
- PocketBase überlastet nicht
|
||||
|
||||
Skalierung:
|
||||
Skalierung:
|
||||
|
||||
- Ohne Redis: ~500 Redirects/Minute machbar
|
||||
- Mit Redis: ~5000+ Redirects/Minute
|
||||
- Ohne Redis: ~500 Redirects/Minute machbar
|
||||
- Mit Redis: ~5000+ Redirects/Minute
|
||||
machbar
|
||||
|
||||
Kosten vs. Nutzen
|
||||
Kosten vs. Nutzen
|
||||
|
||||
Option 1: Kostenlos starten
|
||||
Option 1: Kostenlos starten
|
||||
|
||||
- Upstash Redis (10k Commands/Tag gratis)
|
||||
- Perfekt zum Testen
|
||||
- Upgrade wenn nötig
|
||||
- Upstash Redis (10k Commands/Tag gratis)
|
||||
- Perfekt zum Testen
|
||||
- Upgrade wenn nötig
|
||||
|
||||
Option 2: Minimale Kosten
|
||||
Option 2: Minimale Kosten
|
||||
|
||||
- €5-15/Monat für ordentlichen Redis
|
||||
- Dafür 10x bessere Performance
|
||||
- €5-15/Monat für ordentlichen Redis
|
||||
- Dafür 10x bessere Performance
|
||||
|
||||
Was du sparst:
|
||||
Was du sparst:
|
||||
|
||||
- Weniger Server-Stress = längere
|
||||
- Weniger Server-Stress = längere
|
||||
Hardware-Lebensdauer
|
||||
- Bessere User Experience = mehr zufriedene
|
||||
Nutzer
|
||||
- Skalierung möglich ohne neuen Server
|
||||
- Bessere User Experience = mehr zufriedene
|
||||
Nutzer
|
||||
- Skalierung möglich ohne neuen Server
|
||||
|
||||
Die Realität für uLoad
|
||||
Die Realität für uLoad
|
||||
|
||||
Jetzt: Bei 100 Klicks gleichzeitig würde
|
||||
dein Server wahrscheinlich strugglen
|
||||
Mit Redis: Bei 1000 Klicks gleichzeitig
|
||||
läuft alles smooth
|
||||
Jetzt: Bei 100 Klicks gleichzeitig würde
|
||||
dein Server wahrscheinlich strugglen
|
||||
Mit Redis: Bei 1000 Klicks gleichzeitig
|
||||
läuft alles smooth
|
||||
|
||||
Jetzt: Dashboard mit 100 Links braucht eine
|
||||
Sekunde zum Laden
|
||||
Mit Redis: Dashboard lädt in 0.1 Sekunden
|
||||
Jetzt: Dashboard mit 100 Links braucht eine
|
||||
Sekunde zum Laden
|
||||
Mit Redis: Dashboard lädt in 0.1 Sekunden
|
||||
|
||||
Jetzt: Click-Statistiken berechnen dauert
|
||||
ewig
|
||||
Mit Redis: Statistiken sind immer aktuell
|
||||
und sofort da
|
||||
Jetzt: Click-Statistiken berechnen dauert
|
||||
ewig
|
||||
Mit Redis: Statistiken sind immer aktuell
|
||||
und sofort da
|
||||
|
||||
Meine ehrliche Einschätzung
|
||||
Meine ehrliche Einschätzung
|
||||
|
||||
Für uLoad ist Redis ein No-Brainer:
|
||||
- Minimaler Aufwand (2-3 Tage Setup)
|
||||
- Maximaler Nutzen (10x Performance)
|
||||
- Kosten fast null (kann gratis starten)
|
||||
Für uLoad ist Redis ein No-Brainer:
|
||||
|
||||
Der größte Gewinn ist bei Redirects - das
|
||||
ist dein Kernfeature und wird damit
|
||||
ultra-schnell. Plus du bist ready für
|
||||
Viral-Links die plötzlich 1000x geklickt
|
||||
werden.
|
||||
- Minimaler Aufwand (2-3 Tage Setup)
|
||||
- Maximaler Nutzen (10x Performance)
|
||||
- Kosten fast null (kann gratis starten)
|
||||
|
||||
Bottom Line: Redis macht aus deinem "hobby
|
||||
project" eine professionelle, skalierbare
|
||||
Anwendung für praktisch null Zusatzkosten.
|
||||
Das ist vermutlich das beste ROI was du
|
||||
kriegen kannst! 🎯
|
||||
Der größte Gewinn ist bei Redirects - das
|
||||
ist dein Kernfeature und wird damit
|
||||
ultra-schnell. Plus du bist ready für
|
||||
Viral-Links die plötzlich 1000x geklickt
|
||||
werden.
|
||||
|
||||
Bottom Line: Redis macht aus deinem "hobby
|
||||
project" eine professionelle, skalierbare
|
||||
Anwendung für praktisch null Zusatzkosten.
|
||||
Das ist vermutlich das beste ROI was du
|
||||
kriegen kannst! 🎯
|
||||
|
|
|
|||
|
|
@ -9,11 +9,13 @@ Nach einigen Herausforderungen haben wir Redis erfolgreich auf Coolify zum Laufe
|
|||
### Redis Service in Coolify
|
||||
|
||||
#### 1. Redis als Database Service hinzufügen
|
||||
|
||||
- **Type:** Redis Database
|
||||
- **Image:** redis:7.2
|
||||
- **Name:** redis-database-[generated-id]
|
||||
|
||||
#### 2. General Settings
|
||||
|
||||
```
|
||||
Username: default
|
||||
Password: [Sicheres Passwort generieren]
|
||||
|
|
@ -23,6 +25,7 @@ Custom Docker Options: --protected-mode no --bind 0.0.0.0
|
|||
**Wichtig:** Die Custom Docker Options sind KRITISCH! Ohne diese wird Redis Verbindungen ablehnen.
|
||||
|
||||
#### 3. Network Configuration
|
||||
|
||||
```
|
||||
Ports Mappings: 6379:6379
|
||||
Redis URL (internal): [wird automatisch generiert]
|
||||
|
|
@ -33,6 +36,7 @@ Redis URL (internal): [wird automatisch generiert]
|
|||
### Hauptanwendung Environment Variables
|
||||
|
||||
#### Funktionierende Konfiguration:
|
||||
|
||||
```bash
|
||||
REDIS_HOST=ycsoowwsc84s0s8gc8oooosk # Der Container-Name (NICHT der Service-Name!)
|
||||
REDIS_PORT=6379
|
||||
|
|
@ -47,6 +51,7 @@ REDIS_PASSWORD=[Das gleiche Passwort wie im Redis Service]
|
|||
**Problem:** Der Coolify Service Name funktioniert nicht für die interne Kommunikation.
|
||||
|
||||
**Lösung:** Verwende den tatsächlichen Container-Namen:
|
||||
|
||||
- ❌ FALSCH: `redis-database-ycsoowwsc84s0s8gc8oooosk`
|
||||
- ❌ FALSCH: `redis-database-ycsoowwsc84s0s8gc8oooosk.coolify`
|
||||
- ✅ RICHTIG: `ycsoowwsc84s0s8gc8oooosk`
|
||||
|
|
@ -58,6 +63,7 @@ Der Container-Name findest du in den Redis Logs oder beim Container Start.
|
|||
**Problem:** "Connection is closed" Fehler trotz korrekter Credentials.
|
||||
|
||||
**Lösung:** Redis Protected Mode deaktivieren:
|
||||
|
||||
```bash
|
||||
--protected-mode no --bind 0.0.0.0
|
||||
```
|
||||
|
|
@ -68,7 +74,8 @@ Diese Optionen MÜSSEN in "Custom Docker Options" gesetzt werden!
|
|||
|
||||
**Problem:** REDIS_HOST wurde mit kompletter URL statt nur Hostname gesetzt.
|
||||
|
||||
**Lösung:**
|
||||
**Lösung:**
|
||||
|
||||
- ❌ FALSCH: `REDIS_HOST=redis://default:password@host:6379`
|
||||
- ✅ RICHTIG: `REDIS_HOST=ycsoowwsc84s0s8gc8oooosk`
|
||||
|
||||
|
|
@ -79,6 +86,7 @@ REDIS_HOST darf NUR der Hostname sein, keine URL!
|
|||
**Problem:** Falscher Port (5432 statt 6379) wurde gemappt.
|
||||
|
||||
**Lösung:**
|
||||
|
||||
- Port 6379 ist Redis
|
||||
- Port 5432 ist PostgreSQL
|
||||
- Immer 6379:6379 für Redis verwenden
|
||||
|
|
@ -104,6 +112,7 @@ REDIS_HOST darf NUR der Hostname sein, keine URL!
|
|||
### Schritt 3: Hauptapp konfigurieren
|
||||
|
||||
Environment Variables in deiner Hauptapp:
|
||||
|
||||
```bash
|
||||
REDIS_HOST=[Container-Name aus Schritt 2]
|
||||
REDIS_PORT=6379
|
||||
|
|
@ -114,36 +123,40 @@ REDIS_PASSWORD=[Passwort aus Redis Service]
|
|||
### Schritt 4: Testen
|
||||
|
||||
Erstelle einen Test-Endpoint in deiner App:
|
||||
|
||||
```typescript
|
||||
// src/routes/test-redis/+server.ts
|
||||
import { json } from '@sveltejs/kit';
|
||||
import Redis from 'ioredis';
|
||||
|
||||
export async function GET() {
|
||||
const redis = new Redis({
|
||||
host: process.env.REDIS_HOST,
|
||||
port: parseInt(process.env.REDIS_PORT || '6379'),
|
||||
username: process.env.REDIS_USERNAME,
|
||||
password: process.env.REDIS_PASSWORD
|
||||
});
|
||||
const redis = new Redis({
|
||||
host: process.env.REDIS_HOST,
|
||||
port: parseInt(process.env.REDIS_PORT || '6379'),
|
||||
username: process.env.REDIS_USERNAME,
|
||||
password: process.env.REDIS_PASSWORD,
|
||||
});
|
||||
|
||||
try {
|
||||
await redis.ping();
|
||||
await redis.set('test', 'Hello Redis!');
|
||||
const value = await redis.get('test');
|
||||
redis.disconnect();
|
||||
|
||||
return json({
|
||||
success: true,
|
||||
value,
|
||||
host: process.env.REDIS_HOST
|
||||
});
|
||||
} catch (error) {
|
||||
return json({
|
||||
success: false,
|
||||
error: error.message
|
||||
}, { status: 500 });
|
||||
}
|
||||
try {
|
||||
await redis.ping();
|
||||
await redis.set('test', 'Hello Redis!');
|
||||
const value = await redis.get('test');
|
||||
redis.disconnect();
|
||||
|
||||
return json({
|
||||
success: true,
|
||||
value,
|
||||
host: process.env.REDIS_HOST,
|
||||
});
|
||||
} catch (error) {
|
||||
return json(
|
||||
{
|
||||
success: false,
|
||||
error: error.message,
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -152,16 +165,19 @@ export async function GET() {
|
|||
Nach erfolgreicher Redis-Integration:
|
||||
|
||||
### Link Redirects
|
||||
|
||||
- **Vorher:** 50-100ms (PocketBase Query)
|
||||
- **Nachher:** 2-5ms (Redis Cache)
|
||||
- **Verbesserung:** 20-50x schneller!
|
||||
|
||||
### Dashboard Loading
|
||||
|
||||
- **Vorher:** 200-400ms
|
||||
- **Nachher:** 10-20ms
|
||||
- **Verbesserung:** 10-20x schneller!
|
||||
|
||||
### Analytics
|
||||
|
||||
- **Vorher:** 500-1500ms
|
||||
- **Nachher:** 20-50ms
|
||||
- **Verbesserung:** 10-30x schneller!
|
||||
|
|
@ -169,16 +185,19 @@ Nach erfolgreicher Redis-Integration:
|
|||
## 🐛 Troubleshooting
|
||||
|
||||
### "Connection is closed" Error
|
||||
|
||||
1. Check Custom Docker Options: `--protected-mode no --bind 0.0.0.0`
|
||||
2. Verify Container Name (nicht Service Name!)
|
||||
3. Check Password ist korrekt
|
||||
|
||||
### "ECONNREFUSED" Error
|
||||
|
||||
1. Redis Service läuft nicht
|
||||
2. Falscher Host/Port
|
||||
3. Network Isolation Problem
|
||||
|
||||
### "NOAUTH Authentication required"
|
||||
|
||||
1. Password nicht gesetzt in Environment Variables
|
||||
2. Falsches Password
|
||||
3. Username fehlt (sollte "default" sein)
|
||||
|
|
@ -186,6 +205,7 @@ Nach erfolgreicher Redis-Integration:
|
|||
### Debug Commands
|
||||
|
||||
Im Redis Container (via Coolify Terminal):
|
||||
|
||||
```bash
|
||||
# Test Redis läuft
|
||||
redis-cli ping
|
||||
|
|
@ -201,23 +221,27 @@ redis-cli -a [password] CONFIG GET protected-mode
|
|||
## 💡 Best Practices
|
||||
|
||||
### 1. Resource Limits
|
||||
|
||||
```bash
|
||||
--maxmemory 512mb
|
||||
--maxmemory-policy allkeys-lru
|
||||
```
|
||||
|
||||
### 2. Persistence
|
||||
|
||||
```bash
|
||||
--appendonly yes
|
||||
--save 900 1 # Save every 15 min if at least 1 key changed
|
||||
```
|
||||
|
||||
### 3. Security
|
||||
|
||||
- Niemals Redis Port öffentlich exponieren
|
||||
- Starkes Passwort verwenden
|
||||
- Protected Mode nur intern deaktivieren
|
||||
|
||||
### 4. Monitoring
|
||||
|
||||
- Memory Usage im Auge behalten
|
||||
- Hit Rate tracken
|
||||
- Slow Queries monitoren
|
||||
|
|
@ -225,6 +249,7 @@ redis-cli -a [password] CONFIG GET protected-mode
|
|||
## 📊 Resource-Bedarf
|
||||
|
||||
Für uLoad auf Hetzner CX21:
|
||||
|
||||
- **RAM:** 50-200MB (von 8GB verfügbar)
|
||||
- **CPU:** <1% (von 2 vCPUs)
|
||||
- **Disk:** <1GB (von 40GB)
|
||||
|
|
@ -251,4 +276,4 @@ Mit dieser Konfiguration läuft Redis stabil und performant auf dem gleichen Het
|
|||
|
||||
---
|
||||
|
||||
*Dokumentiert nach erfolgreicher Redis-Integration für uLoad auf Coolify, August 2025*
|
||||
_Dokumentiert nach erfolgreicher Redis-Integration für uLoad auf Coolify, August 2025_
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ Redis is used in uload to dramatically improve link redirect performance by cach
|
|||
## Prerequisites
|
||||
|
||||
- macOS with Homebrew installed
|
||||
- Node.js 18+
|
||||
- Node.js 18+
|
||||
- Running uload development environment
|
||||
|
||||
## Installation
|
||||
|
|
@ -51,6 +51,7 @@ REDIS_HOST=localhost REDIS_PORT=6379 npm run dev
|
|||
```
|
||||
|
||||
Check the console output for:
|
||||
|
||||
```
|
||||
✅ Redis: Connected successfully
|
||||
```
|
||||
|
|
@ -64,14 +65,15 @@ curl http://localhost:5173/api/redis-status | jq
|
|||
```
|
||||
|
||||
Expected response:
|
||||
|
||||
```json
|
||||
{
|
||||
"connected": true,
|
||||
"host": "localhost",
|
||||
"enabled": true,
|
||||
"available": true,
|
||||
"cachedLinks": 0,
|
||||
"error": null
|
||||
"connected": true,
|
||||
"host": "localhost",
|
||||
"enabled": true,
|
||||
"available": true,
|
||||
"cachedLinks": 0,
|
||||
"error": null
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -80,16 +82,20 @@ Expected response:
|
|||
### Cache Flow
|
||||
|
||||
1. **First Visit (Cache MISS)**
|
||||
|
||||
```
|
||||
User → Short Link → Check Redis (miss) → Query Database → Redirect → Cache Result
|
||||
```
|
||||
|
||||
- Takes ~100-200ms
|
||||
- Stores result in Redis for future requests
|
||||
|
||||
2. **Subsequent Visits (Cache HIT)**
|
||||
|
||||
```
|
||||
User → Short Link → Check Redis (hit) → Redirect
|
||||
```
|
||||
|
||||
- Takes ~10-20ms
|
||||
- Skips database query entirely
|
||||
|
||||
|
|
@ -139,6 +145,7 @@ redis-cli del "redirect:abc123"
|
|||
### 4. Debug Cache Hits/Misses
|
||||
|
||||
Enable verbose logging in your browser console:
|
||||
|
||||
1. Visit a short link
|
||||
2. Check browser console for:
|
||||
- "Cache MISS - fetching from PocketBase"
|
||||
|
|
@ -170,13 +177,14 @@ node test-redis-cache.mjs
|
|||
Edit `src/lib/server/linkCache.ts`:
|
||||
|
||||
```typescript
|
||||
const CACHE_TTL = 86400; // 24 hours for popular links
|
||||
const SHORT_TTL = 300; // 5 minutes for normal links
|
||||
const CACHE_TTL = 86400; // 24 hours for popular links
|
||||
const SHORT_TTL = 300; // 5 minutes for normal links
|
||||
```
|
||||
|
||||
### Fallback Behavior
|
||||
|
||||
The app automatically handles Redis unavailability:
|
||||
|
||||
- If Redis is down, the app continues working without cache
|
||||
- No errors shown to users
|
||||
- Graceful degradation to database-only mode
|
||||
|
|
@ -202,11 +210,13 @@ tail -f /opt/homebrew/var/log/redis.log
|
|||
### Connection Refused
|
||||
|
||||
1. Verify Redis is running:
|
||||
|
||||
```bash
|
||||
redis-cli ping
|
||||
```
|
||||
|
||||
2. Check port availability:
|
||||
|
||||
```bash
|
||||
lsof -i :6379
|
||||
```
|
||||
|
|
@ -220,11 +230,13 @@ tail -f /opt/homebrew/var/log/redis.log
|
|||
### Cache Not Working
|
||||
|
||||
1. Check Redis connection in app:
|
||||
|
||||
```bash
|
||||
curl http://localhost:5173/api/redis-status
|
||||
```
|
||||
|
||||
2. Verify Redis has memory available:
|
||||
|
||||
```bash
|
||||
redis-cli info memory
|
||||
```
|
||||
|
|
@ -234,11 +246,13 @@ tail -f /opt/homebrew/var/log/redis.log
|
|||
### Performance Issues
|
||||
|
||||
1. Monitor Redis latency:
|
||||
|
||||
```bash
|
||||
redis-cli --latency
|
||||
```
|
||||
|
||||
2. Check Redis memory usage:
|
||||
|
||||
```bash
|
||||
redis-cli info memory | grep used_memory_human
|
||||
```
|
||||
|
|
@ -300,4 +314,4 @@ redis-cli slowlog get # Show slow queries
|
|||
|
||||
- [Production Redis Setup](./redis-production-setup.md)
|
||||
- [Cache Strategy Guide](./cache-strategy.md)
|
||||
- [Performance Optimization](./performance.md)
|
||||
- [Performance Optimization](./performance.md)
|
||||
|
|
|
|||
|
|
@ -59,10 +59,11 @@ redis-cli ping
|
|||
Visit: http://localhost:5173/api/redis-status
|
||||
|
||||
Should see:
|
||||
|
||||
```json
|
||||
{
|
||||
"connected": true,
|
||||
"available": true
|
||||
"connected": true,
|
||||
"available": true
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -105,17 +106,20 @@ brew services stop redis
|
|||
## 🆘 Help
|
||||
|
||||
**Redis won't start?**
|
||||
|
||||
```bash
|
||||
brew services restart redis
|
||||
```
|
||||
|
||||
**Connection refused?**
|
||||
|
||||
```bash
|
||||
# Check if running
|
||||
ps aux | grep redis
|
||||
```
|
||||
|
||||
**Cache not working?**
|
||||
|
||||
- Check console for Redis connection message
|
||||
- Verify environment variables are loaded
|
||||
- Try explicit env vars: `REDIS_HOST=localhost npm run dev`
|
||||
- Try explicit env vars: `REDIS_HOST=localhost npm run dev`
|
||||
|
|
|
|||
|
|
@ -52,16 +52,19 @@ Die DSGVO und moderne Browser-Technologien schaffen zusätzliche Hürden:
|
|||
Branch.io nutzt eine Kombination aus verschiedenen Matching-Technologien:
|
||||
|
||||
**Deterministic Matching** (100% Genauigkeit):
|
||||
|
||||
- Verwendet eindeutige Identifikatoren wie IDFA (iOS) oder GAID (Android)
|
||||
- Click-IDs in URL-Parametern
|
||||
- Login-basiertes Matching über User-Accounts
|
||||
|
||||
**Probabilistic Matching** (70-90% Genauigkeit):
|
||||
|
||||
- Kombiniert IP-Adresse, User-Agent, Zeitstempel und Geräteinformationen
|
||||
- Erstellt einen "Fingerprint" für wahrscheinliche Zuordnung
|
||||
- Nutzt ein Zeitfenster von typischerweise 24-72 Stunden
|
||||
|
||||
**Deferred Deep Linking**:
|
||||
|
||||
- Speichert die Ziel-Information vor der App-Installation
|
||||
- Nach Installation fragt die App den Branch-Server nach pending Deep Links
|
||||
- Nutzer wird automatisch zum richtigen Inhalt in der App geleitet
|
||||
|
|
@ -71,16 +74,19 @@ Branch.io nutzt eine Kombination aus verschiedenen Matching-Technologien:
|
|||
Diese Plattformen setzen auf:
|
||||
|
||||
**Server-to-Server Integration**:
|
||||
|
||||
- Direkte API-Kommunikation zwischen App-Backend und Attribution-Plattform
|
||||
- Umgeht Browser-Restriktionen komplett
|
||||
- Höhere Datenqualität durch serverseitige Validierung
|
||||
|
||||
**SKAdNetwork Integration** (iOS):
|
||||
|
||||
- Apples Privacy-freundliche Attribution-API
|
||||
- Aggregierte Daten ohne Nutzer-Identifikation
|
||||
- 24-48 Stunden Verzögerung in Reporting
|
||||
|
||||
**Multi-Touch Attribution Models**:
|
||||
|
||||
- First-Touch: Kredite gehen an ersten Touchpoint
|
||||
- Last-Touch: Kredite gehen an letzten Touchpoint vor Conversion
|
||||
- Linear: Gleichmäßige Verteilung auf alle Touchpoints
|
||||
|
|
@ -101,18 +107,21 @@ Diese Plattformen setzen auf:
|
|||
**Konzept**: Jeder Link-Ersteller erhält einen eindeutigen Referral-Code, der in der URL mitgegeben wird. Dieser Code wird durch die gesamte Journey hindurch weitergereicht.
|
||||
|
||||
**Funktionsweise**:
|
||||
|
||||
- Link-Ersteller teilt: `https://ulo.ad/download?ref=USER123`
|
||||
- Code wird auf Download-Seite im LocalStorage gespeichert
|
||||
- App liest Code beim ersten Start aus und meldet ihn zurück
|
||||
- Keine personenbezogenen Daten des Endnutzers werden getrackt
|
||||
|
||||
**Vorteile**:
|
||||
|
||||
- 100% DSGVO-konform, da nur der Referrer getrackt wird
|
||||
- 100% genaue Attribution möglich
|
||||
- Keine Cookies oder Fingerprinting erforderlich
|
||||
- Funktioniert plattformübergreifend
|
||||
|
||||
**Nachteile**:
|
||||
|
||||
- Erfordert aktive Weitergabe des Codes durch die App
|
||||
- Kann bei App-Store-Downloads verloren gehen
|
||||
- Keine Multi-Touch-Attribution möglich
|
||||
|
|
@ -122,6 +131,7 @@ Diese Plattformen setzen auf:
|
|||
**Konzept**: Temporäre Sessions mit kurzer Lebensdauer und anonymisierten Daten.
|
||||
|
||||
**Funktionsweise**:
|
||||
|
||||
- Bei Klick wird Session-ID generiert (30 Minuten Gültigkeit)
|
||||
- Nur aggregierte Daten ohne Personenbezug werden gespeichert
|
||||
- Täglicher Salt für Hashing verhindert langfristige Nachverfolgung
|
||||
|
|
@ -145,11 +155,13 @@ Diese Plattformen setzen auf:
|
|||
- Langzeit-Analyse
|
||||
|
||||
**Vorteile**:
|
||||
|
||||
- Flexibles Privacy-Modell
|
||||
- Bessere Insights bei vorhandenem Consent
|
||||
- Basis-Funktionalität ohne Einwilligung
|
||||
|
||||
**Nachteile**:
|
||||
|
||||
- Komplexere Implementierung
|
||||
- Reduzierte Genauigkeit ohne Consent
|
||||
- Consent-Banner kann Conversion beeinträchtigen
|
||||
|
|
@ -159,18 +171,21 @@ Diese Plattformen setzen auf:
|
|||
**Konzept**: Nutzer gibt E-Mail-Adresse ein und erhält personalisierten Download-Link.
|
||||
|
||||
**Funktionsweise**:
|
||||
|
||||
- Nutzer gibt E-Mail auf Landing Page ein
|
||||
- System sendet Magic Link mit verschlüsseltem Token
|
||||
- Token enthält Attribution-Informationen
|
||||
- Nach Download kann Nutzer direkt zugeordnet werden
|
||||
|
||||
**Vorteile**:
|
||||
|
||||
- Explizite Einwilligung durch E-Mail-Eingabe
|
||||
- 100% genaue Attribution
|
||||
- Möglichkeit für Follow-up-Kommunikation
|
||||
- DSGVO-konform mit Double-Opt-In
|
||||
|
||||
**Nachteile**:
|
||||
|
||||
- Zusätzlicher Schritt im Conversion-Funnel
|
||||
- Nicht alle Nutzer wollen E-Mail angeben
|
||||
- Verzögerung durch E-Mail-Versand
|
||||
|
|
@ -180,18 +195,21 @@ Diese Plattformen setzen auf:
|
|||
**Konzept**: Gamification-Ansatz mit manueller Code-Eingabe.
|
||||
|
||||
**Funktionsweise**:
|
||||
|
||||
- Jeder Link enthält prominenten Promo-Code
|
||||
- Nutzer wird incentiviert, Code in App einzugeben
|
||||
- Code aktiviert Vorteile (Rabatte, Features)
|
||||
- Attribution erfolgt bei Code-Einlösung
|
||||
|
||||
**Vorteile**:
|
||||
|
||||
- Kein technisches Tracking erforderlich
|
||||
- Nutzer hat volle Kontrolle
|
||||
- Zusätzlicher Conversion-Anreiz
|
||||
- 100% DSGVO-konform
|
||||
|
||||
**Nachteile**:
|
||||
|
||||
- Erfordert manuelle Aktion des Nutzers
|
||||
- Nicht alle werden Code eingeben
|
||||
- Zusätzliche Incentive-Kosten
|
||||
|
|
@ -203,12 +221,14 @@ Diese Plattformen setzen auf:
|
|||
**Ziel**: Schnelle Basis-Implementation ohne komplexe Infrastruktur
|
||||
|
||||
**Umsetzung**:
|
||||
|
||||
- Integration von Referral-Codes in bestehende Link-Struktur
|
||||
- Weitergabe der Codes über URL-Parameter
|
||||
- Einfaches Dashboard für Link-Ersteller
|
||||
- Server-to-Server Webhook für App-Events
|
||||
|
||||
**Metriken**:
|
||||
|
||||
- Anzahl Klicks pro Referral-Code
|
||||
- Conversions (Download, Registrierung, Kauf)
|
||||
- Conversion-Rate pro Link-Ersteller
|
||||
|
|
@ -218,12 +238,14 @@ Diese Plattformen setzen auf:
|
|||
**Ziel**: Verbessertes Tracking mit Privacy-First-Ansatz
|
||||
|
||||
**Umsetzung**:
|
||||
|
||||
- Session-basiertes Tracking mit täglichem Salt
|
||||
- Aggregierte Analytics ohne Personenbezug
|
||||
- A/B-Testing verschiedener Attribution-Methoden
|
||||
- Implementierung eines Consent-Banners für erweiterte Features
|
||||
|
||||
**Neue Features**:
|
||||
|
||||
- Conversion-Funnels
|
||||
- Zeitbasierte Analysen
|
||||
- Geografische Verteilung
|
||||
|
|
@ -234,12 +256,14 @@ Diese Plattformen setzen auf:
|
|||
**Ziel**: Vollständige Customer Journey Analyse
|
||||
|
||||
**Umsetzung**:
|
||||
|
||||
- Kombination mehrerer Tracking-Methoden
|
||||
- Machine Learning für Probabilistic Matching
|
||||
- Integration mit externen Analytics-Tools
|
||||
- Advanced Reporting und Insights
|
||||
|
||||
**Erweiterte Funktionen**:
|
||||
|
||||
- Attribution-Modelle (First-Touch, Last-Touch, Linear)
|
||||
- Kohorten-Analyse
|
||||
- Lifetime-Value-Berechnung
|
||||
|
|
@ -250,6 +274,7 @@ Diese Plattformen setzen auf:
|
|||
**Ziel**: Professionelle Attribution-Plattform
|
||||
|
||||
**Umsetzung**:
|
||||
|
||||
- Fraud-Detection-System
|
||||
- Real-Time Dashboards
|
||||
- API für Drittanbieter-Integration
|
||||
|
|
@ -260,16 +285,19 @@ Diese Plattformen setzen auf:
|
|||
### Datenmodell
|
||||
|
||||
**tracking_sessions**:
|
||||
|
||||
- Temporäre Sessions mit kurzer Lebensdauer
|
||||
- Anonymisierte Nutzer-Identifikatoren
|
||||
- Verknüpfung zu Links und Referrern
|
||||
|
||||
**conversion_events**:
|
||||
|
||||
- Event-Stream aller Conversions
|
||||
- Attribution zu Sessions und Referral-Codes
|
||||
- Zeitstempel und Event-Typen
|
||||
|
||||
**attribution_analytics**:
|
||||
|
||||
- Aggregierte Metriken
|
||||
- Keine personenbezogenen Daten
|
||||
- Optimiert für schnelle Abfragen
|
||||
|
|
@ -277,11 +305,13 @@ Diese Plattformen setzen auf:
|
|||
### API-Design
|
||||
|
||||
**Tracking-Endpoints**:
|
||||
|
||||
- POST /api/track/click - Initiale Klick-Erfassung
|
||||
- POST /api/track/event - Event-Tracking (Download, Register, Purchase)
|
||||
- GET /api/analytics/attribution - Attribution-Reports
|
||||
|
||||
**Webhook-System**:
|
||||
|
||||
- Empfang von App-Events
|
||||
- Validierung und Zuordnung
|
||||
- Asynchrone Verarbeitung
|
||||
|
|
@ -289,12 +319,14 @@ Diese Plattformen setzen auf:
|
|||
### Sicherheit und Datenschutz
|
||||
|
||||
**Datenschutz-Maßnahmen**:
|
||||
|
||||
- Verschlüsselung aller Daten in Transit und at Rest
|
||||
- Automatische Datenlöschung nach definierten Zeiträumen
|
||||
- Pseudonymisierung von Nutzer-Daten
|
||||
- Audit-Logs für alle Datenzugriffe
|
||||
|
||||
**Compliance**:
|
||||
|
||||
- DSGVO-konforme Datenverarbeitung
|
||||
- Privacy-by-Design-Prinzipien
|
||||
- Transparente Datenschutzerklärung
|
||||
|
|
@ -328,28 +360,35 @@ Diese Plattformen setzen auf:
|
|||
### Technische Risiken
|
||||
|
||||
**Datenverlust bei Platform-Wechsel**:
|
||||
|
||||
- Mitigation: Redundante Tracking-Methoden, Fallback-Mechanismen
|
||||
|
||||
**Ungenauigkeit bei Probabilistic Matching**:
|
||||
|
||||
- Mitigation: Konservative Zuordnung, manuelle Validierung bei hochwertigen Conversions
|
||||
|
||||
**Skalierungsprobleme**:
|
||||
|
||||
- Mitigation: Event-Streaming-Architektur, horizontale Skalierung
|
||||
|
||||
### Rechtliche Risiken
|
||||
|
||||
**DSGVO-Verstöße**:
|
||||
|
||||
- Mitigation: Privacy-by-Design, regelmäßige Audits, Datenschutzbeauftragter
|
||||
|
||||
**Internationale Datenschutzgesetze**:
|
||||
|
||||
- Mitigation: Geo-basierte Compliance-Rules, lokale Datenspeicherung
|
||||
|
||||
### Geschäftliche Risiken
|
||||
|
||||
**Nutzer-Akzeptanz**:
|
||||
|
||||
- Mitigation: Transparente Kommunikation, Opt-in-Incentives
|
||||
|
||||
**Kosten-Nutzen-Verhältnis**:
|
||||
|
||||
- Mitigation: Phasenweise Einführung, kontinuierliche ROI-Messung
|
||||
|
||||
## Zusammenfassung und Ausblick
|
||||
|
|
@ -369,4 +408,4 @@ Langfristig positioniert sich uload damit nicht nur als Link-Shortener, sondern
|
|||
5. **Pilotphase**: Test mit ausgewählten Nutzern
|
||||
6. **Rollout**: Schrittweise Einführung für alle Nutzer
|
||||
|
||||
Der Erfolg des Systems wird maßgeblich davon abhängen, wie gut es gelingt, die Balance zwischen Tracking-Genauigkeit und Datenschutz zu finden. Mit dem vorgeschlagenen mehrstufigen Ansatz ist uload bestens positioniert, um diese Herausforderung zu meistern und einen neuen Standard für datenschutzfreundliches Attribution-Tracking zu setzen.
|
||||
Der Erfolg des Systems wird maßgeblich davon abhängen, wie gut es gelingt, die Balance zwischen Tracking-Genauigkeit und Datenschutz zu finden. Mit dem vorgeschlagenen mehrstufigen Ansatz ist uload bestens positioniert, um diese Herausforderung zu meistern und einen neuen Standard für datenschutzfreundliches Attribution-Tracking zu setzen.
|
||||
|
|
|
|||
|
|
@ -80,16 +80,19 @@ Nach erfolgreicher App-Installation:
|
|||
#### Datenfluss und technische Architektur
|
||||
|
||||
**Frontend-Komponenten:**
|
||||
|
||||
```
|
||||
Landing Page → JavaScript-Tracker → Storage APIs → Redirect Handler
|
||||
```
|
||||
|
||||
**Backend-Komponenten:**
|
||||
|
||||
```
|
||||
Link-Generator → Code-Database → Attribution-Engine → Analytics-Aggregator
|
||||
```
|
||||
|
||||
**Datenstrukturen:**
|
||||
|
||||
- Referral-Codes: Key-Value-Store mit TTL
|
||||
- Click-Events: Time-Series-Database
|
||||
- Attributions: Relationale Datenbank
|
||||
|
|
@ -200,6 +203,7 @@ Nach erfolgreicher Validierung:
|
|||
**1. Maximale Conversion-Rate**
|
||||
|
||||
Das größte Plus ist die Reibungslosigkeit. Studien zeigen, dass jeder zusätzliche Schritt im Conversion-Funnel zu einem Verlust von 10-30% der Nutzer führt. Das automatische System eliminiert diese Hürde komplett. Nutzer müssen:
|
||||
|
||||
- Nichts merken
|
||||
- Nichts eingeben
|
||||
- Keine zusätzliche Entscheidung treffen
|
||||
|
|
@ -207,6 +211,7 @@ Das größte Plus ist die Reibungslosigkeit. Studien zeigen, dass jeder zusätzl
|
|||
**2. Universelle Anwendbarkeit**
|
||||
|
||||
Das System funktioniert überall:
|
||||
|
||||
- Bei impulsiven Käufen
|
||||
- In Situationen mit wenig Zeit
|
||||
- Für weniger technikaffine Nutzer
|
||||
|
|
@ -237,6 +242,7 @@ Das System funktioniert überall:
|
|||
**1. Fehlende Nutzer-Awareness**
|
||||
|
||||
Nutzer wissen nicht, dass sie getrackt werden:
|
||||
|
||||
- Keine bewusste Verbindung zum Referrer
|
||||
- Keine Wertschätzung für den "Deal"
|
||||
- Verpasste Marketing-Opportunity
|
||||
|
|
@ -275,6 +281,7 @@ Nutzer wissen nicht, dass sie getrackt werden:
|
|||
**1. Perfekte Attribution (nahezu 100%)**
|
||||
|
||||
Wenn ein Code eingegeben wird, ist die Zuordnung eindeutig:
|
||||
|
||||
- Keine technischen Unsicherheiten
|
||||
- Keine False Positives
|
||||
- Klare Kausalität
|
||||
|
|
@ -283,6 +290,7 @@ Wenn ein Code eingegeben wird, ist die Zuordnung eindeutig:
|
|||
**2. Marketing-Multiplikator**
|
||||
|
||||
Codes sind selbst Marketing-Instrumente:
|
||||
|
||||
- Virales Potenzial durch Weitergabe
|
||||
- Gesprächsthema in Communities
|
||||
- Social-Media-Content
|
||||
|
|
@ -291,6 +299,7 @@ Codes sind selbst Marketing-Instrumente:
|
|||
**3. Nutzer-Engagement und -Bindung**
|
||||
|
||||
Die aktive Eingabe schafft Commitment:
|
||||
|
||||
- Bewusste Entscheidung für Produkt
|
||||
- Positive Assoziation durch Belohnung
|
||||
- Höhere Wertschätzung
|
||||
|
|
@ -322,6 +331,7 @@ Die aktive Eingabe schafft Commitment:
|
|||
**1. Conversion-Friction**
|
||||
|
||||
Der größte Nachteil ist die zusätzliche Hürde:
|
||||
|
||||
- 20-40% niedrigere Conversion-Rate typisch
|
||||
- Nutzer vergessen Codes
|
||||
- Nutzer sind zu faul für Eingabe
|
||||
|
|
@ -362,16 +372,19 @@ Der größte Nachteil ist die zusätzliche Hürde:
|
|||
**Unconscious Processing**
|
||||
|
||||
Das automatische System nutzt das Prinzip des "Unconscious Processing":
|
||||
|
||||
- Nutzer treffen Entscheidungen ohne bewusste Überlegung
|
||||
- Reduzierte kognitive Last führt zu schnelleren Entscheidungen
|
||||
- "System 1 Thinking" nach Kahneman
|
||||
|
||||
**Vorteile:**
|
||||
|
||||
- Höhere Spontankäufe
|
||||
- Weniger Entscheidungsmüdigkeit
|
||||
- Natürlicher Flow
|
||||
|
||||
**Nachteile:**
|
||||
|
||||
- Keine emotionale Verbindung
|
||||
- Geringere Erinnerung
|
||||
- Weniger Wertschätzung
|
||||
|
|
@ -400,6 +413,7 @@ Die manuelle Code-Eingabe aktiviert mehrere psychologische Mechanismen:
|
|||
**1. Niedrigpreisige Impulskäufe**
|
||||
|
||||
Bei Apps oder Services unter 10€ ist jede Hürde fatal:
|
||||
|
||||
- Casual Games
|
||||
- Utility-Apps
|
||||
- Content-Subscriptions
|
||||
|
|
@ -410,6 +424,7 @@ Bei Apps oder Services unter 10€ ist jede Hürde fatal:
|
|||
**2. Zeitkritische Aktionen**
|
||||
|
||||
Wenn Nutzer schnell handeln müssen:
|
||||
|
||||
- Flash Sales
|
||||
- Live-Events
|
||||
- Breaking News Apps
|
||||
|
|
@ -420,6 +435,7 @@ Wenn Nutzer schnell handeln müssen:
|
|||
**3. Technisch weniger versierte Zielgruppen**
|
||||
|
||||
Für Nutzer, die mit Code-Eingabe überfordert wären:
|
||||
|
||||
- Senioren-Apps
|
||||
- Kinder-Produkte (Eltern als Käufer)
|
||||
- Mainstream-Utility-Apps
|
||||
|
|
@ -427,6 +443,7 @@ Für Nutzer, die mit Code-Eingabe überfordert wären:
|
|||
**4. Hochvolumige Virale Kampagnen**
|
||||
|
||||
Wenn Masse über Qualität geht:
|
||||
|
||||
- Social-Media-Challenges
|
||||
- Influencer-Kampagnen mit Millionen-Reichweite
|
||||
- Viral-Marketing-Stunts
|
||||
|
|
@ -436,6 +453,7 @@ Wenn Masse über Qualität geht:
|
|||
**1. Premium-Produkte und Services**
|
||||
|
||||
Bei höheren Preispunkten ist die Extra-Motivation wertvoll:
|
||||
|
||||
- B2B-Software
|
||||
- Premium-Subscriptions (>20€/Monat)
|
||||
- Online-Kurse
|
||||
|
|
@ -446,6 +464,7 @@ Bei höheren Preispunkten ist die Extra-Motivation wertvoll:
|
|||
**2. Community-getriebene Produkte**
|
||||
|
||||
Wenn Codes Teil der Community-Kultur werden:
|
||||
|
||||
- Gaming-Communities
|
||||
- Fitness-Apps mit Gruppen
|
||||
- Lern-Plattformen
|
||||
|
|
@ -456,6 +475,7 @@ Wenn Codes Teil der Community-Kultur werden:
|
|||
**3. Saisonale und Event-basierte Kampagnen**
|
||||
|
||||
Codes passen perfekt zu zeitlichen Events:
|
||||
|
||||
- Black Friday (BLACK2024)
|
||||
- Weihnachten (XMAS-DEAL)
|
||||
- Produktlaunches (LAUNCH50)
|
||||
|
|
@ -464,6 +484,7 @@ Codes passen perfekt zu zeitlichen Events:
|
|||
**4. Partnership und Kooperationen**
|
||||
|
||||
Codes sind ideal für Partnerschaften:
|
||||
|
||||
- Corporate Benefits
|
||||
- Influencer-Kooperationen
|
||||
- Cross-Promotions
|
||||
|
|
@ -485,7 +506,7 @@ Codes sind ideal für Partnerschaften:
|
|||
- Cookie als Fallback
|
||||
- URL-Parameter als Backup
|
||||
|
||||
3. **Communication-Layer**:
|
||||
3. **Communication-Layer**:
|
||||
- Beacon-API für Analytics
|
||||
- Fetch für Attribution-Events
|
||||
- WebSocket für Real-Time
|
||||
|
|
@ -598,12 +619,14 @@ Eine elegante Lösung kombiniert beide Systeme:
|
|||
2. **Bonus-Layer**: Optionale Code-Eingabe für zusätzliche Vorteile
|
||||
|
||||
**Beispiel-Flow:**
|
||||
|
||||
- Nutzer klickt Link → Automatisches Tracking aktiv
|
||||
- Landing-Page zeigt: "Bonus-Code für Extra-Vorteile: SPECIAL20"
|
||||
- Nutzer kann ignorieren → Normale Conversion mit Attribution
|
||||
- Oder Code eingeben → Extra-Vorteile + verstärkte Attribution
|
||||
|
||||
**Vorteile:**
|
||||
|
||||
- Basis-Attribution immer gesichert
|
||||
- Zusätzliche Motivation durch Codes
|
||||
- Selbst-Selektion von engaged Users
|
||||
|
|
@ -625,6 +648,7 @@ Dies erlaubt organisches Wachstum der Code-Kultur.
|
|||
**Konzept: Intelligente, kontextabhängige Codes**
|
||||
|
||||
Codes, die sich adaptiv verhalten:
|
||||
|
||||
- Gleicher Code, unterschiedliche Vorteile je nach Kontext
|
||||
- Zeit-basierte Vorteile (morgens anders als abends)
|
||||
- Geo-basierte Anpassungen
|
||||
|
|
@ -632,6 +656,7 @@ Codes, die sich adaptiv verhalten:
|
|||
|
||||
**Beispiel:**
|
||||
Code "SMART2024" gibt:
|
||||
|
||||
- Neue Nutzer: 50% Rabatt ersten Monat
|
||||
- Bestehende Nutzer: Extra-Features
|
||||
- Premium-Nutzer: Gratis-Monat für Freund
|
||||
|
|
@ -690,31 +715,34 @@ Code "SMART2024" gibt:
|
|||
|
||||
### Vergleichende Metriken
|
||||
|
||||
| Metrik | Automatisches System | Voucher-System |
|
||||
|--------|---------------------|----------------|
|
||||
| **Setup-Komplexität** | Hoch (Technik) | Niedrig (Business) |
|
||||
| **Attribution-Genauigkeit** | 60-80% | 95-100% |
|
||||
| **Conversion-Rate-Impact** | 0% (neutral) | -20 bis -40% |
|
||||
| **Nutzer-Engagement** | Niedrig | Hoch |
|
||||
| **Viral-Potenzial** | Niedrig | Hoch |
|
||||
| **Wartungsaufwand** | Niedrig | Mittel |
|
||||
| **Skalierbarkeit** | Exzellent | Gut |
|
||||
| **Kosten pro Conversion** | Niedrig | Mittel-Hoch |
|
||||
| Metrik | Automatisches System | Voucher-System |
|
||||
| --------------------------- | -------------------- | ------------------ |
|
||||
| **Setup-Komplexität** | Hoch (Technik) | Niedrig (Business) |
|
||||
| **Attribution-Genauigkeit** | 60-80% | 95-100% |
|
||||
| **Conversion-Rate-Impact** | 0% (neutral) | -20 bis -40% |
|
||||
| **Nutzer-Engagement** | Niedrig | Hoch |
|
||||
| **Viral-Potenzial** | Niedrig | Hoch |
|
||||
| **Wartungsaufwand** | Niedrig | Mittel |
|
||||
| **Skalierbarkeit** | Exzellent | Gut |
|
||||
| **Kosten pro Conversion** | Niedrig | Mittel-Hoch |
|
||||
|
||||
## Reale Fallstudien und Learnings
|
||||
|
||||
### Fallstudie 1: Gaming-App mit automatischem Tracking
|
||||
|
||||
**Ausgangslage:**
|
||||
|
||||
- Casual Mobile Game, Free-to-Play
|
||||
- Zielgruppe: 18-35 Jahre
|
||||
- Monetarisierung durch In-App-Käufe
|
||||
|
||||
**Implementation:**
|
||||
|
||||
- Automatisches Referral-Tracking
|
||||
- Keine Promo-Codes initially
|
||||
|
||||
**Ergebnisse:**
|
||||
|
||||
- 73% Attribution-Rate
|
||||
- 2.3x höhere Install-Rate vs. Promo-Codes
|
||||
- Aber: 40% niedrigerer Lifetime-Value
|
||||
|
|
@ -725,15 +753,18 @@ Automatisches Tracking maximiert Volume, aber Qualität leidet.
|
|||
### Fallstudie 2: Fitness-App mit Voucher-System
|
||||
|
||||
**Ausgangslage:**
|
||||
|
||||
- Premium Fitness-App, 19,99€/Monat
|
||||
- Zielgruppe: 25-45 Jahre, gesundheitsbewusst
|
||||
- Influencer-Marketing-Fokus
|
||||
|
||||
**Implementation:**
|
||||
|
||||
- Personalisierte Influencer-Codes
|
||||
- 30% Rabatt für 3 Monate
|
||||
|
||||
**Ergebnisse:**
|
||||
|
||||
- 98% Attribution-Genauigkeit
|
||||
- 24% Redemption-Rate
|
||||
- 3.2x höherer LTV vs. organische Nutzer
|
||||
|
|
@ -745,15 +776,18 @@ Codes schaffen Commitment und Community.
|
|||
### Fallstudie 3: B2B-SaaS mit Hybrid-Modell
|
||||
|
||||
**Ausgangslage:**
|
||||
|
||||
- Projektmanagement-Tool
|
||||
- 50-500€/Monat je nach Plan
|
||||
- Lange Sales-Cycles
|
||||
|
||||
**Implementation:**
|
||||
|
||||
- Automatisches Tracking für Trial-Signups
|
||||
- Voucher-Codes für Paid-Conversions
|
||||
|
||||
**Ergebnisse:**
|
||||
|
||||
- 89% Combined Attribution
|
||||
- 45% höhere Trial-to-Paid-Rate mit Codes
|
||||
- Vereinfachtes Partner-Programm
|
||||
|
|
@ -766,12 +800,14 @@ Hybrid-Modelle können das Beste aus beiden Welten vereinen.
|
|||
### Automatisches Referral-System
|
||||
|
||||
**Einmalige Kosten:**
|
||||
|
||||
- Entwicklung: 15.000-30.000€
|
||||
- Testing: 3.000-5.000€
|
||||
- Integration: 5.000-10.000€
|
||||
- **Gesamt: 23.000-45.000€**
|
||||
|
||||
**Laufende Kosten (monatlich):**
|
||||
|
||||
- Server/Infrastructure: 200-1.000€
|
||||
- Maintenance: 500-1.000€
|
||||
- Monitoring: 100-300€
|
||||
|
|
@ -779,6 +815,7 @@ Hybrid-Modelle können das Beste aus beiden Welten vereinen.
|
|||
|
||||
**ROI-Berechnung:**
|
||||
Bei 10.000 Conversions/Monat und 70% Attribution:
|
||||
|
||||
- 7.000 zugeordnete Conversions
|
||||
- Bei 10€ Provision: 70.000€ korrekt zugeordnet
|
||||
- ROI-Breakeven: 1-2 Monate
|
||||
|
|
@ -786,12 +823,14 @@ Bei 10.000 Conversions/Monat und 70% Attribution:
|
|||
### Voucher-System
|
||||
|
||||
**Einmalige Kosten:**
|
||||
|
||||
- Entwicklung: 8.000-15.000€
|
||||
- Design/UX: 3.000-5.000€
|
||||
- Integration: 2.000-5.000€
|
||||
- **Gesamt: 13.000-25.000€**
|
||||
|
||||
**Laufende Kosten (monatlich):**
|
||||
|
||||
- Rabatte/Incentives: 5-30% des Umsatzes
|
||||
- Code-Management: 300-500€
|
||||
- Support: 500-1.500€
|
||||
|
|
@ -800,6 +839,7 @@ Bei 10.000 Conversions/Monat und 70% Attribution:
|
|||
|
||||
**ROI-Berechnung:**
|
||||
Bei 10.000 Sichtkontakten und 20% Redemption:
|
||||
|
||||
- 2.000 Code-Nutzer
|
||||
- Bei 30% höherem LTV (+20€): 40.000€ Mehrwert
|
||||
- Minus 20% Rabattkosten: 32.000€ Netto
|
||||
|
|
@ -873,16 +913,16 @@ Bei 10.000 Sichtkontakten und 20% Redemption:
|
|||
|
||||
### Entscheidungsmatrix
|
||||
|
||||
| Faktor | Gewicht | Automatisch | Voucher |
|
||||
|--------|---------|-------------|----------|
|
||||
| **Technische Komplexität** | 15% | 3/10 | 8/10 |
|
||||
| **Attribution-Genauigkeit** | 20% | 7/10 | 10/10 |
|
||||
| **Conversion-Rate** | 25% | 10/10 | 6/10 |
|
||||
| **Nutzer-Engagement** | 15% | 3/10 | 9/10 |
|
||||
| **Skalierbarkeit** | 10% | 10/10 | 7/10 |
|
||||
| **DSGVO-Compliance** | 10% | 10/10 | 10/10 |
|
||||
| **Kosten** | 5% | 8/10 | 5/10 |
|
||||
| **Gewichteter Score** | 100% | **7.5/10** | **7.8/10** |
|
||||
| Faktor | Gewicht | Automatisch | Voucher |
|
||||
| --------------------------- | ------- | ----------- | ---------- |
|
||||
| **Technische Komplexität** | 15% | 3/10 | 8/10 |
|
||||
| **Attribution-Genauigkeit** | 20% | 7/10 | 10/10 |
|
||||
| **Conversion-Rate** | 25% | 10/10 | 6/10 |
|
||||
| **Nutzer-Engagement** | 15% | 3/10 | 9/10 |
|
||||
| **Skalierbarkeit** | 10% | 10/10 | 7/10 |
|
||||
| **DSGVO-Compliance** | 10% | 10/10 | 10/10 |
|
||||
| **Kosten** | 5% | 8/10 | 5/10 |
|
||||
| **Gewichteter Score** | 100% | **7.5/10** | **7.8/10** |
|
||||
|
||||
### Entscheidungsbaum
|
||||
|
||||
|
|
@ -915,6 +955,7 @@ Start: Was ist das primäre Ziel?
|
|||
**Start mit Voucher-System für schnelle Validierung:**
|
||||
|
||||
**Begründung:**
|
||||
|
||||
1. **Geringere technische Komplexität** ermöglicht schnelleren Start
|
||||
2. **100% Attribution** liefert klare Daten für Investoren/Partner
|
||||
3. **Marketing-Story** ("Exklusive Codes für Early Adopters")
|
||||
|
|
@ -922,6 +963,7 @@ Start: Was ist das primäre Ziel?
|
|||
5. **Flexibilität** für Experimente mit Incentive-Höhen
|
||||
|
||||
**Konkrete Umsetzung:**
|
||||
|
||||
- Einfache Code-Struktur: CREATOR-KAMPAGNE (z.B. PETER-LAUNCH)
|
||||
- Initial hohe Incentives (30-50%) für Momentum
|
||||
- Fokus auf Influencer und Early Adopters
|
||||
|
|
@ -932,16 +974,19 @@ Start: Was ist das primäre Ziel?
|
|||
**Migration zu Hybrid-System:**
|
||||
|
||||
**Phase 1:** Voucher-System optimieren
|
||||
|
||||
- A/B-Tests verschiedener Incentive-Höhen
|
||||
- Optimierung der Code-Präsentation
|
||||
- Aufbau einer Code-Kultur in der Community
|
||||
|
||||
**Phase 2:** Automatisches Tracking ergänzen
|
||||
|
||||
- Parallelbetrieb beider Systeme
|
||||
- Vouchers als Premium-Option
|
||||
- Automatisches Tracking als Fallback
|
||||
|
||||
**Phase 3:** Datengetriebene Optimierung
|
||||
|
||||
- Analyse welches System für welche Zielgruppe funktioniert
|
||||
- Segmentierung der Ansätze nach Kampagnen-Typ
|
||||
- Machine Learning für optimale System-Auswahl
|
||||
|
|
@ -951,6 +996,7 @@ Start: Was ist das primäre Ziel?
|
|||
**Intelligentes Attribution-Ecosystem:**
|
||||
|
||||
Ein System, das kontextabhängig entscheidet:
|
||||
|
||||
- **AI-gesteuerte Methodenwahl**: Automatische Auswahl basierend auf User-Signals
|
||||
- **Progressive Enhancement**: Start simpel, wird sophistizierter mit User-Reife
|
||||
- **Multi-Touch-Attribution**: Kombination beider Methoden für vollständiges Bild
|
||||
|
|
@ -969,12 +1015,14 @@ Die Entscheidung zwischen automatischem Referral-Tracking und manuellem Voucher-
|
|||
**Für uload spezifisch empfehle ich:**
|
||||
|
||||
**Start mit Voucher-System**, weil:
|
||||
|
||||
- Schnellere Time-to-Market
|
||||
- Perfekte Attribution für Investor-Demos
|
||||
- Community-Building-Potenzial
|
||||
- Geringeres technisches Risiko
|
||||
|
||||
**Evolution zum Hybrid-System**, weil:
|
||||
|
||||
- Best of Both Worlds
|
||||
- Maximale Flexibilität
|
||||
- Datengetriebene Optimierung möglich
|
||||
|
|
@ -982,4 +1030,4 @@ Die Entscheidung zwischen automatischem Referral-Tracking und manuellem Voucher-
|
|||
|
||||
Die Voucher-Codes sollten nicht als technische Limitation gesehen werden, sondern als Feature, das die Marke stärkt und eine engagierte Community aufbaut. Der kurzfristige Conversion-Verlust wird durch höhere Nutzer-Qualität und bessere Attribution mehr als kompensiert.
|
||||
|
||||
Langfristig wird die Kombination beider Ansätze uload ermöglichen, verschiedene Nutzer-Segmente optimal anzusprechen und dabei sowohl Wachstum als auch Profitabilität zu maximieren.
|
||||
Langfristig wird die Kombination beider Ansätze uload ermöglichen, verschiedene Nutzer-Segmente optimal anzusprechen und dabei sowohl Wachstum als auch Profitabilität zu maximieren.
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ Voucher-Codes sind auf den ersten Blick simple Rabattcodes - doch bei genauerer
|
|||
Voucher-Codes funktionieren als **soziale Währung**. Menschen teilen Codes nicht nur, um anderen einen Gefallen zu tun, sondern auch, um ihren eigenen sozialen Status zu erhöhen. "Ich habe einen exklusiven Code für dich" ist eine Form von sozialem Kapital. Der Teilende positioniert sich als Insider, als jemand mit Zugang zu besonderen Vorteilen.
|
||||
|
||||
Diese Dynamik ist besonders stark, wenn:
|
||||
|
||||
- Codes limitiert oder exklusiv sind
|
||||
- Der Teilende selbst einen Vorteil erhält (Win-Win)
|
||||
- Die Codes personalisiert sind (PETERS-SPECIAL vs. GENERIC123)
|
||||
|
|
@ -44,16 +45,19 @@ Das Eingeben eines Codes wird zur bewussten, befriedigenden Handlung - wie das E
|
|||
Voucher-Codes können zu einem integralen Teil der Community-Identität werden. Beispiele aus erfolgreichen Communities:
|
||||
|
||||
**Gaming-Communities:**
|
||||
|
||||
- Streamer haben eigene Codes, die zur "Crew" dazugehören
|
||||
- Codes werden in Chat-Nachrichten wie Insider-Witze verwendet
|
||||
- "Bist du Team NINJA oder Team VIKING?" (verschiedene Creator-Codes)
|
||||
|
||||
**Fitness-Communities:**
|
||||
|
||||
- Trainer-spezifische Codes schaffen Zugehörigkeit
|
||||
- "Train with Team ALEX20" wird zum Motto
|
||||
- Challenges verbunden mit speziellen Event-Codes
|
||||
|
||||
**Bildungs-Communities:**
|
||||
|
||||
- Dozenten-Codes für ihre Studenten
|
||||
- Kurs-spezifische Codes für Lerngruppen
|
||||
- Alumni-Codes als dauerhafter Benefit
|
||||
|
|
@ -63,21 +67,25 @@ Voucher-Codes können zu einem integralen Teil der Community-Identität werden.
|
|||
Ein gestaffeltes Code-System schafft natürliche Community-Hierarchien:
|
||||
|
||||
**Öffentliche Ebene:**
|
||||
|
||||
- Allgemeine Codes mit kleinen Rabatten (10%)
|
||||
- Für jeden zugänglich
|
||||
- Einstieg in die Community
|
||||
|
||||
**Member-Ebene:**
|
||||
|
||||
- Bessere Codes für registrierte Community-Mitglieder (20%)
|
||||
- Erfordert Anmeldung/Engagement
|
||||
- Gefühl der Zugehörigkeit
|
||||
|
||||
**VIP-Ebene:**
|
||||
|
||||
- Exklusive Codes für Top-Contributors (30-50%)
|
||||
- Früher Zugang zu neuen Features
|
||||
- Status-Symbol in der Community
|
||||
|
||||
**Inner Circle:**
|
||||
|
||||
- Persönliche Codes für engste Supporter
|
||||
- Unlimited Benefits
|
||||
- Co-Creation Möglichkeiten
|
||||
|
|
@ -87,16 +95,19 @@ Ein gestaffeltes Code-System schafft natürliche Community-Hierarchien:
|
|||
Voucher-Codes sind perfekte Katalysatoren für Community-Events:
|
||||
|
||||
**Flash-Mobs und Challenges:**
|
||||
|
||||
- "Die ersten 100, die Code FLASH24 nutzen..."
|
||||
- Zeitlich begrenzte Community-Aktionen
|
||||
- Gemeinsame Ziele ("10.000 Code-Nutzungen = Feature X für alle")
|
||||
|
||||
**Seasonal Campaigns:**
|
||||
|
||||
- Weihnachts-Codes als Geschenk-Tradition
|
||||
- Geburtstags-Codes für Community-Jubiläen
|
||||
- Back-to-School Codes für Bildungs-Apps
|
||||
|
||||
**Collaborative Unlocks:**
|
||||
|
||||
- Codes, die erst ab X Nutzungen aktiviert werden
|
||||
- Community muss zusammenarbeiten
|
||||
- Geteilter Erfolg stärkt Zusammenhalt
|
||||
|
|
@ -108,6 +119,7 @@ Voucher-Codes sind perfekte Katalysatoren für Community-Events:
|
|||
**Transparenz und Vertrauen:**
|
||||
|
||||
Affiliates sehen in Echtzeit:
|
||||
|
||||
- Wie oft ihr Code genutzt wurde
|
||||
- Welche Conversions daraus resultierten
|
||||
- Ihre exakte Provision
|
||||
|
|
@ -117,6 +129,7 @@ Diese Transparenz schafft Vertrauen und motiviert zu mehr Engagement. Im Gegensa
|
|||
**Einfachheit der Integration:**
|
||||
|
||||
Affiliates müssen:
|
||||
|
||||
- Keine komplexen Tracking-Links generieren
|
||||
- Keine technischen Integrationen vornehmen
|
||||
- Keine Cookies oder Pixel einbauen
|
||||
|
|
@ -126,6 +139,7 @@ Ein einfacher Code reicht. Das senkt die Einstiegshürde dramatisch.
|
|||
**Flexibilität in der Vermarktung:**
|
||||
|
||||
Codes funktionieren überall:
|
||||
|
||||
- Social Media Posts und Stories
|
||||
- YouTube-Video-Beschreibungen
|
||||
- Podcast-Erwähnungen
|
||||
|
|
@ -136,6 +150,7 @@ Codes funktionieren überall:
|
|||
### Strukturierung eines Affiliate-Programms
|
||||
|
||||
**Tier 1: Micro-Influencer (100-1.000 Follower)**
|
||||
|
||||
- Persönlicher Code: NAME20
|
||||
- Benefit für Nutzer: 20% Rabatt
|
||||
- Provision: 15% auf Lifetime des Kunden
|
||||
|
|
@ -143,6 +158,7 @@ Codes funktionieren überall:
|
|||
- Support: Self-Service Dashboard
|
||||
|
||||
**Tier 2: Mid-Tier Influencer (1.000-10.000 Follower)**
|
||||
|
||||
- Custom Code nach Wahl
|
||||
- Benefit für Nutzer: 25% Rabatt
|
||||
- Provision: 20% auf Lifetime
|
||||
|
|
@ -150,6 +166,7 @@ Codes funktionieren überall:
|
|||
- Support: Monatliche Check-ins
|
||||
|
||||
**Tier 3: Macro-Influencer (10.000+ Follower)**
|
||||
|
||||
- Multiple Codes für verschiedene Kampagnen
|
||||
- Benefit für Nutzer: 30% Rabatt + Extras
|
||||
- Provision: 25% auf Lifetime
|
||||
|
|
@ -157,6 +174,7 @@ Codes funktionieren überall:
|
|||
- Support: Dedicated Account Manager
|
||||
|
||||
**Tier 4: Strategic Partners**
|
||||
|
||||
- White-Label-Codes
|
||||
- Custom Benefits
|
||||
- Provision: Individual verhandelt
|
||||
|
|
@ -168,6 +186,7 @@ Codes funktionieren überall:
|
|||
**A/B-Testing von Codes:**
|
||||
|
||||
Affiliates können verschiedene Codes testen:
|
||||
|
||||
- SAVE20 vs. DISCOUNT20 vs. DEAL20
|
||||
- Verschiedene Rabatthöhen
|
||||
- Zeitlimitierte vs. dauerhafte Codes
|
||||
|
|
@ -176,6 +195,7 @@ Affiliates können verschiedene Codes testen:
|
|||
**Saisonale und Event-Codes:**
|
||||
|
||||
Spezielle Codes für maximale Conversion:
|
||||
|
||||
- BLACK50 für Black Friday
|
||||
- SUMMER30 für Sommerferien
|
||||
- NEWYEAR für Neujahrsvorsätze
|
||||
|
|
@ -184,6 +204,7 @@ Spezielle Codes für maximale Conversion:
|
|||
**Personalisierte Landing Pages:**
|
||||
|
||||
Jeder Code führt zu angepasster Experience:
|
||||
|
||||
- Begrüßung mit Affiliate-Namen
|
||||
- Angepasste Value Proposition
|
||||
- Social Proof des Affiliates
|
||||
|
|
@ -198,6 +219,7 @@ Jeder Code führt zu angepasster Experience:
|
|||
Die Struktur der Codes sollte sowohl menschenfreundlich als auch systemoptimiert sein:
|
||||
|
||||
**Format-Optionen:**
|
||||
|
||||
1. **Personalisiert-einfach**: PETER, SARAH, TECH
|
||||
2. **Personalisiert-komplex**: PETER2024, SARAH-APP
|
||||
3. **Kampagnen-basiert**: LAUNCH50, SUMMER30
|
||||
|
|
@ -206,16 +228,19 @@ Die Struktur der Codes sollte sowohl menschenfreundlich als auch systemoptimiert
|
|||
**Generierungs-Strategien:**
|
||||
|
||||
**Manuelle Generierung** für VIP-Partner:
|
||||
|
||||
- Persönliche Abstimmung
|
||||
- Wunsch-Codes möglich
|
||||
- Marken-Alignment
|
||||
|
||||
**Semi-automatisch** für reguläre Affiliates:
|
||||
|
||||
- Template-basiert: {NAME}-{CAMPAIGN}
|
||||
- Validierung gegen Blacklist
|
||||
- Eindeutigkeitsprüfung
|
||||
|
||||
**Voll-automatisch** für Massen-Kampagnen:
|
||||
|
||||
- Algorithmus-generiert
|
||||
- Collision-Vermeidung
|
||||
- Batch-Generierung
|
||||
|
|
@ -234,17 +259,20 @@ Die Struktur der Codes sollte sowohl menschenfreundlich als auch systemoptimiert
|
|||
**Sicherheitsmaßnahmen:**
|
||||
|
||||
**Rate-Limiting:**
|
||||
|
||||
- Max. 5 Code-Versuche pro Minute
|
||||
- IP-basierte Beschränkungen
|
||||
- Device-Fingerprinting
|
||||
|
||||
**Fraud-Prevention:**
|
||||
|
||||
- Velocity-Checks (zu viele Nutzungen zu schnell)
|
||||
- Geo-Blocking (Codes nur in bestimmten Ländern)
|
||||
- Device-Limits (ein Code pro Gerät)
|
||||
- Referrer-Validation
|
||||
|
||||
**Abuse-Handling:**
|
||||
|
||||
- Automatische Code-Deaktivierung bei Missbrauch
|
||||
- Blacklisting von Nutzern/IPs
|
||||
- Alert-System für Anomalien
|
||||
|
|
@ -254,18 +282,21 @@ Die Struktur der Codes sollte sowohl menschenfreundlich als auch systemoptimiert
|
|||
**Affiliate-Dashboard Features:**
|
||||
|
||||
**Real-Time Metrics:**
|
||||
|
||||
- Live-Counter für Code-Nutzungen
|
||||
- Conversion-Funnel-Visualisierung
|
||||
- Earnings-Ticker
|
||||
- Trend-Analysen
|
||||
|
||||
**Performance-Insights:**
|
||||
|
||||
- Beste Tageszeiten für Conversions
|
||||
- Geo-Performance
|
||||
- Device-Breakdown
|
||||
- Vergleich mit Peer-Group
|
||||
|
||||
**Optimization-Tools:**
|
||||
|
||||
- A/B-Test-Results
|
||||
- Suggested Improvements
|
||||
- Best-Practice-Tipps
|
||||
|
|
@ -274,18 +305,21 @@ Die Struktur der Codes sollte sowohl menschenfreundlich als auch systemoptimiert
|
|||
**Admin-Dashboard Features:**
|
||||
|
||||
**Code-Management:**
|
||||
|
||||
- Bulk-Creation und -Editing
|
||||
- Regelbasierte Automation
|
||||
- Quick-Actions (Pause, Extend, Modify)
|
||||
- Template-Library
|
||||
|
||||
**Affiliate-Management:**
|
||||
|
||||
- Performance-Rankings
|
||||
- Automatic Tier-Upgrades
|
||||
- Commission-Berechnungen
|
||||
- Auszahlungs-Management
|
||||
|
||||
**Business Intelligence:**
|
||||
|
||||
- Customer-Lifetime-Value nach Code
|
||||
- Churn-Rate nach Acquisition-Channel
|
||||
- ROI-Berechnungen
|
||||
|
|
@ -298,6 +332,7 @@ Die Struktur der Codes sollte sowohl menschenfreundlich als auch systemoptimiert
|
|||
**Blog-Posts und Artikel:**
|
||||
|
||||
"Die 10 besten Promo-Codes für [Produkt] im November 2024"
|
||||
|
||||
- SEO-optimiert für Code-Suchen
|
||||
- Regelmäßig aktualisiert
|
||||
- User-Generated-Content einbinden
|
||||
|
|
@ -305,6 +340,7 @@ Die Struktur der Codes sollte sowohl menschenfreundlich als auch systemoptimiert
|
|||
**Video-Content:**
|
||||
|
||||
"Unboxing mit exklusivem Code"
|
||||
|
||||
- Code als Call-to-Action
|
||||
- Visual einblenden
|
||||
- Beschreibung und Kommentare
|
||||
|
|
@ -312,6 +348,7 @@ Die Struktur der Codes sollte sowohl menschenfreundlich als auch systemoptimiert
|
|||
**Podcast-Sponsoring:**
|
||||
|
||||
"Nutze Code PODCAST für 20% Rabatt"
|
||||
|
||||
- Einfach zu merken
|
||||
- Mündlich kommunizierbar
|
||||
- Tracking von Audio-Conversions
|
||||
|
|
@ -319,24 +356,28 @@ Die Struktur der Codes sollte sowohl menschenfreundlich als auch systemoptimiert
|
|||
### Social Media Strategien
|
||||
|
||||
**Instagram:**
|
||||
|
||||
- Story-Sticker mit Codes
|
||||
- Swipe-Up mit vorausgefülltem Code
|
||||
- IGTV-Tutorials mit Code-Reveals
|
||||
- Reels mit Code-Challenges
|
||||
|
||||
**TikTok:**
|
||||
|
||||
- Viral Challenges mit Codes
|
||||
- Duett-Videos mit Code-Sharing
|
||||
- Live-Streams mit Flash-Codes
|
||||
- Comment-Codes für Engagement
|
||||
|
||||
**Twitter/X:**
|
||||
|
||||
- Thread-Kampagnen mit Code am Ende
|
||||
- Retweet-für-Code Aktionen
|
||||
- Space-exklusive Codes
|
||||
- Trending-Hashtags mit Codes
|
||||
|
||||
**LinkedIn:**
|
||||
|
||||
- B2B-Codes für Professionals
|
||||
- Artikel mit Industry-Insights und Codes
|
||||
- Company-Page-Updates
|
||||
|
|
@ -345,18 +386,21 @@ Die Struktur der Codes sollte sowohl menschenfreundlich als auch systemoptimiert
|
|||
### E-Mail-Marketing
|
||||
|
||||
**Welcome-Series:**
|
||||
|
||||
1. Tag 1: Welcome + WELCOME20
|
||||
2. Tag 3: Feature-Highlight + TRYIT15
|
||||
3. Tag 7: Success-Stories + SUCCESS25
|
||||
4. Tag 14: Urgency + LASTCHANCE30
|
||||
|
||||
**Segmentierte Kampagnen:**
|
||||
|
||||
- Inaktive Nutzer: COMEBACK50
|
||||
- Power-User: VIP-EXCLUSIVE
|
||||
- Trial-Auslauf: STAYWITHUS40
|
||||
- Geburtstage: BIRTHDAY-GIFT
|
||||
|
||||
**Referral-Programme:**
|
||||
|
||||
- "Gib deinen Freunden FRIEND30"
|
||||
- "Für jede Nutzung erhältst du..."
|
||||
- Leaderboards in Newsletter
|
||||
|
|
@ -366,24 +410,28 @@ Die Struktur der Codes sollte sowohl menschenfreundlich als auch systemoptimiert
|
|||
### Code-Naming-Psychologie
|
||||
|
||||
**Emotionale Trigger:**
|
||||
|
||||
- WINNING (Gewinner-Gefühl)
|
||||
- EXCLUSIVE (Exklusivität)
|
||||
- SMART (Intelligente Entscheidung)
|
||||
- FREEDOM (Freiheit/Unabhängigkeit)
|
||||
|
||||
**Urgency-Codes:**
|
||||
|
||||
- LAST24 (Zeitdruck)
|
||||
- ENDING (Knappheit)
|
||||
- NOW50 (Sofort-Handlung)
|
||||
- TODAY (Tagesaktion)
|
||||
|
||||
**Value-Kommunikation:**
|
||||
|
||||
- SAVE50 (Klarer Sparvorteil)
|
||||
- HALFPRICE (Einfache Mathematik)
|
||||
- BUNDLE (Mehrwert)
|
||||
- PREMIUM (Upgrade-Gefühl)
|
||||
|
||||
**Community-Codes:**
|
||||
|
||||
- FAMILY (Zugehörigkeit)
|
||||
- TEAM (Gemeinschaft)
|
||||
- CLUB (Exklusivität)
|
||||
|
|
@ -473,16 +521,19 @@ Die Struktur der Codes sollte sowohl menschenfreundlich als auch systemoptimiert
|
|||
### Fallstudie 1: Fitness-App "FitLife"
|
||||
|
||||
**Ausgangslage:**
|
||||
|
||||
- Premium-Fitness-App, 29€/Monat
|
||||
- Zielgruppe: 25-45 Jahre
|
||||
- Problem: Hohe Akquisitionskosten
|
||||
|
||||
**Voucher-Strategie:**
|
||||
|
||||
- Personal-Trainer erhalten individuelle Codes
|
||||
- 30% Rabatt für erste 3 Monate
|
||||
- Trainer erhalten 20% recurring commission
|
||||
|
||||
**Ergebnisse nach 6 Monaten:**
|
||||
|
||||
- 340 aktive Trainer-Affiliates
|
||||
- 12.000 neue Kunden über Codes
|
||||
- 65% niedrigere CAC
|
||||
|
|
@ -490,6 +541,7 @@ Die Struktur der Codes sollte sowohl menschenfreundlich als auch systemoptimiert
|
|||
- Community von 50.000 aktiven Mitgliedern
|
||||
|
||||
**Erfolgsfaktoren:**
|
||||
|
||||
- Trainer als vertrauenswürdige Autoritäten
|
||||
- Win-Win-Situation (Trainer verdienen mit)
|
||||
- Community-Events mit Code-Challenges
|
||||
|
|
@ -497,16 +549,19 @@ Die Struktur der Codes sollte sowohl menschenfreundlich als auch systemoptimiert
|
|||
### Fallstudie 2: SaaS-Tool "ProjectPro"
|
||||
|
||||
**Ausgangslage:**
|
||||
|
||||
- B2B-Projektmanagement-Tool
|
||||
- 50-500€/Monat je nach Teamgröße
|
||||
- Problem: Lange Sales-Cycles
|
||||
|
||||
**Voucher-Strategie:**
|
||||
|
||||
- Branchen-spezifische Codes (AGENCY30, STARTUP40)
|
||||
- Consultants als Affiliates
|
||||
- Gestaffelte Benefits nach Teamgröße
|
||||
|
||||
**Ergebnisse nach 12 Monaten:**
|
||||
|
||||
- 89 Partner-Consultants
|
||||
- 450 Enterprise-Kunden
|
||||
- 3.2x höherer LTV
|
||||
|
|
@ -514,6 +569,7 @@ Die Struktur der Codes sollte sowohl menschenfreundlich als auch systemoptimiert
|
|||
- 15 Branchen-Communities aufgebaut
|
||||
|
||||
**Erfolgsfaktoren:**
|
||||
|
||||
- B2B-Empfehlungen extrem wertvoll
|
||||
- Consultants bringen ganze Teams
|
||||
- Industry-Events mit exklusiven Codes
|
||||
|
|
@ -521,16 +577,19 @@ Die Struktur der Codes sollte sowohl menschenfreundlich als auch systemoptimiert
|
|||
### Fallstudie 3: Gaming-Platform "PlayHub"
|
||||
|
||||
**Ausgangslage:**
|
||||
|
||||
- Gaming-Subscription-Service
|
||||
- 9,99€/Monat
|
||||
- Zielgruppe: 16-30 Jahre
|
||||
|
||||
**Voucher-Strategie:**
|
||||
|
||||
- Streamer-Codes mit Gamification
|
||||
- Leaderboard für Code-Nutzungen
|
||||
- Monatliche Preise für Top-Affiliates
|
||||
|
||||
**Ergebnisse nach 3 Monaten:**
|
||||
|
||||
- 1.200 Streamer aktiviert
|
||||
- 180.000 Code-Redemptions
|
||||
- Viral Coefficient: 2.3
|
||||
|
|
@ -538,6 +597,7 @@ Die Struktur der Codes sollte sowohl menschenfreundlich als auch systemoptimiert
|
|||
- Sehr aktive Discord-Community
|
||||
|
||||
**Erfolgsfaktoren:**
|
||||
|
||||
- Perfekter Fit zur Gaming-Kultur
|
||||
- Competitive Element durch Leaderboard
|
||||
- Integration in Streaming-Content
|
||||
|
|
@ -550,6 +610,7 @@ Die Struktur der Codes sollte sowohl menschenfreundlich als auch systemoptimiert
|
|||
Codes wie "XYZPROMO2024Q4SPECIAL50" sind unmöglich zu merken.
|
||||
|
||||
**Lösung:**
|
||||
|
||||
- Kurz und prägnant (max. 10 Zeichen)
|
||||
- Merkbare Wörter statt Zeichensalat
|
||||
- Konsistente Struktur
|
||||
|
|
@ -560,6 +621,7 @@ Codes wie "XYZPROMO2024Q4SPECIAL50" sind unmöglich zu merken.
|
|||
Nutzer warten auf bessere Codes, wenn Rabatte stark variieren.
|
||||
|
||||
**Lösung:**
|
||||
|
||||
- Klare Tier-Struktur
|
||||
- Vorhersehbare Rabatt-Level
|
||||
- Seltene Special-Events
|
||||
|
|
@ -570,6 +632,7 @@ Nutzer warten auf bessere Codes, wenn Rabatte stark variieren.
|
|||
Code-Sharing in Gutschein-Foren kannibalisiert Vollpreis-Verkäufe.
|
||||
|
||||
**Lösung:**
|
||||
|
||||
- Personalisierte Codes mit Limits
|
||||
- Geo-Restrictions
|
||||
- Time-based Validity
|
||||
|
|
@ -580,6 +643,7 @@ Code-Sharing in Gutschein-Foren kannibalisiert Vollpreis-Verkäufe.
|
|||
Affiliates fühlen sich alleingelassen und werden inaktiv.
|
||||
|
||||
**Lösung:**
|
||||
|
||||
- Regelmäßige Check-ins
|
||||
- Performance-Feedback
|
||||
- Success-Resources
|
||||
|
|
@ -591,6 +655,7 @@ Affiliates fühlen sich alleingelassen und werden inaktiv.
|
|||
Code-Eingabe auf Mobile ist frustrierend.
|
||||
|
||||
**Lösung:**
|
||||
|
||||
- Auto-Complete
|
||||
- Copy-Paste-Optimierung
|
||||
- QR-Code-Alternative
|
||||
|
|
@ -608,6 +673,7 @@ Code-Eingabe auf Mobile ist frustrierend.
|
|||
4. **Keine PII**: Keine personenbezogenen Daten des Endnutzers nötig
|
||||
|
||||
**Best Practices:**
|
||||
|
||||
- Klare Datenschutzerklärung
|
||||
- Opt-in für Marketing-Kommunikation
|
||||
- Sichere Speicherung von Affiliate-Daten
|
||||
|
|
@ -616,12 +682,14 @@ Code-Eingabe auf Mobile ist frustrierend.
|
|||
### Steuerliche Aspekte
|
||||
|
||||
**Für Affiliates:**
|
||||
|
||||
- Provisionen sind Einkommen
|
||||
- Meldepflicht ab bestimmten Grenzen
|
||||
- Rechnungsstellung erforderlich
|
||||
- Umsatzsteuer-Thematik
|
||||
|
||||
**Für Unternehmen:**
|
||||
|
||||
- Rabatte als Marketing-Ausgaben
|
||||
- Provisions-Buchhaltung
|
||||
- Internationale Steuer-Compliance
|
||||
|
|
@ -630,6 +698,7 @@ Code-Eingabe auf Mobile ist frustrierend.
|
|||
### Verbraucherschutz
|
||||
|
||||
**Wichtige Regeln:**
|
||||
|
||||
- Keine irreführende Werbung
|
||||
- Klare Bedingungen
|
||||
- Tatsächliche Verfügbarkeit
|
||||
|
|
@ -642,16 +711,19 @@ Code-Eingabe auf Mobile ist frustrierend.
|
|||
**Kulturelle Anpassung:**
|
||||
|
||||
**USA:**
|
||||
|
||||
- Aggressive Discounts (50-70%)
|
||||
- FREEDOM, AMERICA, SUPER
|
||||
- Black Friday/Cyber Monday Focus
|
||||
|
||||
**Deutschland:**
|
||||
|
||||
- Moderate Rabatte (20-30%)
|
||||
- SPAR, VORTEIL, EXKLUSIV
|
||||
- Qualität vor Quantität
|
||||
|
||||
**Japan:**
|
||||
|
||||
- Gruppen-Codes populär
|
||||
- Saisonale Codes (Sakura, NewYear)
|
||||
- Höflichkeit in Kommunikation
|
||||
|
|
@ -666,24 +738,28 @@ Code-Eingabe auf Mobile ist frustrierend.
|
|||
### Wachstums-Strategien
|
||||
|
||||
**Phase 1: Launch (0-1.000 Nutzer)**
|
||||
|
||||
- 10-20 Hand-selektierte Affiliates
|
||||
- Hohe Rabatte (40-50%)
|
||||
- Persönliche Betreuung
|
||||
- Quick Iterations
|
||||
|
||||
**Phase 2: Growth (1.000-10.000 Nutzer)**
|
||||
|
||||
- 100+ Affiliates
|
||||
- Tiered System einführen
|
||||
- Automation Tools
|
||||
- Community Building
|
||||
|
||||
**Phase 3: Scale (10.000-100.000 Nutzer)**
|
||||
|
||||
- 1.000+ Affiliates
|
||||
- Self-Service Platform
|
||||
- API-Integration
|
||||
- International Expansion
|
||||
|
||||
**Phase 4: Maturity (100.000+ Nutzer)**
|
||||
|
||||
- Enterprise Partnerships
|
||||
- White-Label Solutions
|
||||
- Advanced Analytics
|
||||
|
|
@ -694,21 +770,25 @@ Code-Eingabe auf Mobile ist frustrierend.
|
|||
### Technologische Innovationen
|
||||
|
||||
**AI-Powered Personalization:**
|
||||
|
||||
- Individuelle Codes basierend auf Nutzer-Verhalten
|
||||
- Optimale Rabatthöhe per Machine Learning
|
||||
- Predictive Code-Generation
|
||||
|
||||
**Blockchain-Integration:**
|
||||
|
||||
- NFT-Codes als handelbare Assets
|
||||
- Transparente Attribution-Chain
|
||||
- Dezentralisierte Affiliate-Netzwerke
|
||||
|
||||
**Voice-Commerce:**
|
||||
|
||||
- Alexa/Siri Code-Eingabe
|
||||
- Audio-Codes in Podcasts
|
||||
- Voice-activated Rewards
|
||||
|
||||
**AR/VR-Integration:**
|
||||
|
||||
- Codes in virtuellen Welten
|
||||
- AR-Scavenger-Hunts
|
||||
- VR-Shopping mit Codes
|
||||
|
|
@ -716,16 +796,19 @@ Code-Eingabe auf Mobile ist frustrierend.
|
|||
### Markt-Trends
|
||||
|
||||
**Subscription-Economy:**
|
||||
|
||||
- Codes für Subscription-Services
|
||||
- Tier-Upgrades via Codes
|
||||
- Retention-Codes
|
||||
|
||||
**Creator-Economy:**
|
||||
|
||||
- Jeder Creator wird Affiliate
|
||||
- Micro-Influencer-Revolution
|
||||
- Community-Commerce
|
||||
|
||||
**Sustainability-Focus:**
|
||||
|
||||
- Eco-Codes für nachhaltige Choices
|
||||
- Carbon-Offset-Codes
|
||||
- Social-Impact-Codes
|
||||
|
|
@ -735,12 +818,14 @@ Code-Eingabe auf Mobile ist frustrierend.
|
|||
### Monat 1: Foundation
|
||||
|
||||
**Woche 1-2: System-Design**
|
||||
|
||||
- Code-Format festlegen
|
||||
- Datenbank-Schema
|
||||
- API-Spezifikation
|
||||
- Security-Konzept
|
||||
|
||||
**Woche 3-4: MVP-Entwicklung**
|
||||
|
||||
- Basic Code-Generation
|
||||
- Validation-Engine
|
||||
- Simple Dashboard
|
||||
|
|
@ -749,12 +834,14 @@ Code-Eingabe auf Mobile ist frustrierend.
|
|||
### Monat 2: Launch
|
||||
|
||||
**Woche 5-6: Testing & Refinement**
|
||||
|
||||
- Internal Testing
|
||||
- Security Audit
|
||||
- Performance Testing
|
||||
- UX-Optimierung
|
||||
|
||||
**Woche 7-8: Soft Launch**
|
||||
|
||||
- 10 Beta-Affiliates
|
||||
- Feedback sammeln
|
||||
- Iterationen
|
||||
|
|
@ -763,12 +850,14 @@ Code-Eingabe auf Mobile ist frustrierend.
|
|||
### Monat 3: Growth
|
||||
|
||||
**Woche 9-10: Public Launch**
|
||||
|
||||
- Marketing-Kampagne
|
||||
- Affiliate-Rekrutierung
|
||||
- Press-Coverage
|
||||
- Community-Events
|
||||
|
||||
**Woche 11-12: Optimization**
|
||||
|
||||
- A/B-Testing
|
||||
- Performance-Analyse
|
||||
- Feature-Erweiterungen
|
||||
|
|
@ -820,16 +909,19 @@ Code-Eingabe auf Mobile ist frustrierend.
|
|||
### Langfristige Vision
|
||||
|
||||
**Jahr 1: Foundation**
|
||||
|
||||
- 1.000 aktive Affiliates
|
||||
- 100.000 Code-Redemptions
|
||||
- Profitabilität erreichen
|
||||
|
||||
**Jahr 2: Expansion**
|
||||
|
||||
- 10.000 aktive Affiliates
|
||||
- 1 Million Code-Redemptions
|
||||
- International Launch
|
||||
|
||||
**Jahr 3: Leadership**
|
||||
|
||||
- 50.000 aktive Affiliates
|
||||
- 10 Millionen Code-Redemptions
|
||||
- Marktführer in Europa
|
||||
|
|
@ -848,4 +940,4 @@ Der Schlüssel zum Erfolg liegt nicht in der Technologie selbst, sondern in der
|
|||
|
||||
Für uload bedeutet dies konkret: Start simple, think big. Beginnt mit einem einfachen Code-System, aber designed es von Anfang an für Skalierung. Baut eine Kultur um die Codes herum. Macht sie zu einem Teil der Marken-DNA. Und vor allem: Hört auf eure Community. Sie wird euch zeigen, wie die Codes optimal genutzt werden können.
|
||||
|
||||
Die Zukunft gehört den Plattformen, die es schaffen, Technologie menschlich zu machen. Voucher-Codes sind genau das: Eine technische Lösung, die menschliche Bedürfnisse nach Zugehörigkeit, Anerkennung und Vorteil bedient. Für uload ist dies die Chance, nicht nur ein Tool zu bauen, sondern ein Ökosystem zu schaffen, in dem jeder gewinnt.
|
||||
Die Zukunft gehört den Plattformen, die es schaffen, Technologie menschlich zu machen. Voucher-Codes sind genau das: Eine technische Lösung, die menschliche Bedürfnisse nach Zugehörigkeit, Anerkennung und Vorteil bedient. Für uload ist dies die Chance, nicht nur ein Tool zu bauen, sondern ein Ökosystem zu schaffen, in dem jeder gewinnt.
|
||||
|
|
|
|||
|
|
@ -190,7 +190,7 @@ Dieses Dokument zeigt den aktuellen Status der Toast-Integration in uLoad.
|
|||
notify.promise(saveOperation(), {
|
||||
loading: 'Wird gespeichert...',
|
||||
success: 'Erfolgreich gespeichert!',
|
||||
error: (err) => `Fehler: ${err.message}`
|
||||
error: (err) => `Fehler: ${err.message}`,
|
||||
});
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ export const notify = {
|
|||
error: (message: string, details?: string) => {
|
||||
if (details) {
|
||||
toast.error(message, {
|
||||
description: details
|
||||
description: details,
|
||||
});
|
||||
} else {
|
||||
toast.error(message);
|
||||
|
|
@ -121,10 +121,10 @@ export const notify = {
|
|||
toast(message, {
|
||||
action: {
|
||||
label: actionLabel,
|
||||
onClick: onAction
|
||||
}
|
||||
onClick: onAction,
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
|
|
@ -150,7 +150,7 @@ notify.error('Username bereits vergeben');
|
|||
notify.promise(createLink(data), {
|
||||
loading: 'Link wird erstellt...',
|
||||
success: 'Link erfolgreich erstellt!',
|
||||
error: (err) => `Fehler: ${err.message}`
|
||||
error: (err) => `Fehler: ${err.message}`,
|
||||
});
|
||||
|
||||
// Link kopieren
|
||||
|
|
@ -186,14 +186,14 @@ notify.success('Passwort-Reset-Link wurde gesendet');
|
|||
notify.promise(updateProfile(data), {
|
||||
loading: 'Profil wird aktualisiert...',
|
||||
success: 'Profil erfolgreich gespeichert',
|
||||
error: 'Fehler beim Speichern des Profils'
|
||||
error: 'Fehler beim Speichern des Profils',
|
||||
});
|
||||
|
||||
// Avatar hochladen
|
||||
notify.promise(uploadAvatar(file), {
|
||||
loading: 'Bild wird hochgeladen...',
|
||||
success: 'Profilbild aktualisiert',
|
||||
error: 'Fehler beim Upload'
|
||||
error: 'Fehler beim Upload',
|
||||
});
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -3,11 +3,12 @@
|
|||
## Post 1: Die Psychologie kurzer URLs (Carousel)
|
||||
|
||||
### Slide 1 - Cover
|
||||
|
||||
**Die Psychologie kurzer URLs**
|
||||
Warum wir eher auf kurze Links klicken
|
||||
|
||||
|
||||
### Slide 2 - Das Problem
|
||||
|
||||
**42% weniger Klicks bei langen URLs**
|
||||
|
||||
Studien zeigen:
|
||||
|
|
@ -15,9 +16,10 @@ Studien zeigen:
|
|||
• Unleserliche Parameter = Spam-Verdacht
|
||||
• Mobile User scrollen nicht horizontal
|
||||
|
||||
*Quelle: Marketing Sherpa Study 2023*
|
||||
_Quelle: Marketing Sherpa Study 2023_
|
||||
|
||||
### Slide 3 - Die Wissenschaft
|
||||
|
||||
**Cognitive Load Theory**
|
||||
|
||||
Unser Gehirn:
|
||||
|
|
@ -29,6 +31,7 @@ Lang: website.com/category/2024/01/article-title-here-very-long?utm_source=socia
|
|||
Kurz: website.com/artikel
|
||||
|
||||
### Slide 4 - Vertrauensfaktoren
|
||||
|
||||
**Was macht Links vertrauenswürdig?**
|
||||
|
||||
1. **Erkennbare Domain** (60% Wichtigkeit)
|
||||
|
|
@ -37,6 +40,7 @@ Kurz: website.com/artikel
|
|||
4. **HTTPS-Verschlüsselung** (5%)
|
||||
|
||||
### Slide 5 - Best Practices
|
||||
|
||||
**So optimierst du deine Links:**
|
||||
|
||||
✅ Nutze sprechende URLs (/sommer-sale)
|
||||
|
|
@ -46,6 +50,7 @@ Kurz: website.com/artikel
|
|||
✅ Tracke die Performance
|
||||
|
||||
### Slide 6 - Call to Action
|
||||
|
||||
**Key Takeaway:**
|
||||
Kurze URLs = Mehr Vertrauen = Mehr Klicks
|
||||
|
||||
|
|
@ -58,24 +63,30 @@ Speicher diesen Post für später! 📌
|
|||
## Post 2: Der perfekte Instagram Bio-Link (Reel Script)
|
||||
|
||||
### Hook (0-3 Sek)
|
||||
|
||||
[Text Overlay: "Nur 3% nutzen ihren Bio-Link richtig!"]
|
||||
"Stop! Dein Instagram Bio-Link könnte viel mehr leisten!"
|
||||
|
||||
### Problem (3-8 Sek)
|
||||
|
||||
[Zeige schlechtes Beispiel]
|
||||
"Die meisten verlinken nur ihre Homepage. Fehler! Warum? Weil Instagram-User spezifische Inhalte suchen!"
|
||||
|
||||
### Lösung Teil 1 (8-15 Sek)
|
||||
|
||||
[Screen Recording]
|
||||
"Trick 1: Erstelle eine Link-Sammlung! Alle wichtigen Links an einem Ort. Shop, Blog, YouTube, alles!"
|
||||
|
||||
### Lösung Teil 2 (15-22 Sek)
|
||||
|
||||
"Trick 2: Update den Link bei jedem Post! Neuer Post über Smoothies? Link zum Rezept! Story über Workout? Link zum Plan!"
|
||||
|
||||
### Lösung Teil 3 (22-28 Sek)
|
||||
|
||||
"Trick 3: Nutze Analytics! Check welche Links geklickt werden. Mehr davon = mehr Engagement!"
|
||||
|
||||
### Call to Action (28-30 Sek)
|
||||
|
||||
[Text Overlay: "Speichern & ausprobieren!"]
|
||||
"Follow für mehr Instagram-Hacks! Welchen Trick probierst du zuerst?"
|
||||
|
||||
|
|
@ -84,6 +95,7 @@ Speicher diesen Post für später! 📌
|
|||
## Post 3: QR-Codes richtig einsetzen (Infografik)
|
||||
|
||||
### Haupttitel
|
||||
|
||||
**QR-Codes: Do's and Don'ts für 2024**
|
||||
|
||||
### Do's ✅
|
||||
|
|
@ -121,6 +133,7 @@ QR-Code braucht klaren Kontrast
|
|||
Immer auch kurze URL dazu angeben
|
||||
|
||||
### Fun Facts
|
||||
|
||||
• QR steht für "Quick Response"
|
||||
• Erfunden 1994 in Japan für Toyota
|
||||
• Kann bis zu 4.296 Zeichen speichern
|
||||
|
|
@ -139,6 +152,7 @@ Kleine Unternehmen haben oft begrenzte Marketing-Ressourcen. Umso wichtiger ist
|
|||
## 1. Der Hub-Ansatz
|
||||
|
||||
Statt überall verschiedene Links zu streuen, erstellen Sie einen zentralen Hub. Dies kann eine einfache Landing Page sein mit:
|
||||
|
||||
- Kontaktinformationen
|
||||
- Wichtigste Produkte/Services
|
||||
- Social Media Profile
|
||||
|
|
@ -149,6 +163,7 @@ Statt überall verschiedene Links zu streuen, erstellen Sie einen zentralen Hub.
|
|||
## 2. Campaign-spezifische Links
|
||||
|
||||
Für jede Marketing-Kampagne ein eigener Kurz-Link:
|
||||
|
||||
- Black Friday: ihrefirma.de/blackfriday
|
||||
- Newsletter: ihrefirma.de/news-mai
|
||||
- Instagram: ihrefirma.de/insta
|
||||
|
|
@ -158,6 +173,7 @@ Für jede Marketing-Kampagne ein eigener Kurz-Link:
|
|||
## 3. QR-Codes im Ladengeschäft
|
||||
|
||||
Physisch zu Digital:
|
||||
|
||||
- Auf Visitenkarten → digitale Kontaktkarte
|
||||
- An der Kasse → Google Bewertungen
|
||||
- Auf Produkten → Anleitungsvideos
|
||||
|
|
@ -166,6 +182,7 @@ Physisch zu Digital:
|
|||
## 4. Team-Koordination
|
||||
|
||||
Wenn mehrere Personen Links teilen:
|
||||
|
||||
- Einheitliche Naming-Convention
|
||||
- Zentrale Link-Verwaltung
|
||||
- Regelmäßige Aufräum-Sessions
|
||||
|
|
@ -174,6 +191,7 @@ Wenn mehrere Personen Links teilen:
|
|||
## 5. Analyse & Optimierung
|
||||
|
||||
Monatlich prüfen:
|
||||
|
||||
- Welche Links werden geklickt?
|
||||
- Zu welchen Uhrzeiten?
|
||||
- Von welchen Geräten?
|
||||
|
|
@ -186,13 +204,14 @@ Monatlich prüfen:
|
|||
□ Team schulen
|
||||
□ Monatliche Reviews planen
|
||||
|
||||
*Tools wie youload können bei der Umsetzung helfen, aber die Strategie ist tool-unabhängig anwendbar.*
|
||||
_Tools wie youload können bei der Umsetzung helfen, aber die Strategie ist tool-unabhängig anwendbar._
|
||||
|
||||
---
|
||||
|
||||
## Post 5: UTM-Parameter einfach erklärt (Story-Serie)
|
||||
|
||||
### Story 1
|
||||
|
||||
**UTM-Parameter in 60 Sekunden erklärt**
|
||||
[Grafik: URL mit markierten Parametern]
|
||||
|
||||
|
|
@ -205,6 +224,7 @@ Sie helfen dir zu verstehen:
|
|||
Swipe für Examples →
|
||||
|
||||
### Story 2
|
||||
|
||||
**Die 5 wichtigsten Parameter**
|
||||
|
||||
1. utm_source = Woher? (instagram, newsletter)
|
||||
|
|
@ -214,6 +234,7 @@ Swipe für Examples →
|
|||
5. utm_term = Welches Keyword? (bei Ads)
|
||||
|
||||
### Story 3
|
||||
|
||||
**Real-Life Beispiel**
|
||||
|
||||
Normaler Link:
|
||||
|
|
@ -225,6 +246,7 @@ meinshop.de/produkt?utm_source=instagram&utm_medium=social&utm_campaign=black-fr
|
|||
Jetzt weißt du: Dieser Kunde kam von Instagram während der Black Friday Kampagne!
|
||||
|
||||
### Story 4
|
||||
|
||||
**Pro-Tipps**
|
||||
|
||||
✅ Kleinschreibung verwenden
|
||||
|
|
@ -234,6 +256,7 @@ Jetzt weißt du: Dieser Kunde kam von Instagram während der Black Friday Kampag
|
|||
✅ In Analytics prüfen
|
||||
|
||||
### Story 5
|
||||
|
||||
**Quick Quiz!**
|
||||
|
||||
Was trackt "utm_medium"?
|
||||
|
|
@ -249,20 +272,25 @@ Antwort: B! Medium zeigt dir ob es Social, Email, Paid oder Organic Traffic war!
|
|||
## Post 6: Mobile-First Link-Strategie (Video-Script)
|
||||
|
||||
### Szene 1 (0-5 Sek)
|
||||
|
||||
[Zeige Smartphone-Statistik]
|
||||
"80% deiner Kunden sind mobil. Aber sind deine Links dafür ready?"
|
||||
|
||||
### Szene 2 (5-10 Sek)
|
||||
|
||||
[Screen-Recording: Schlechtes Beispiel]
|
||||
"Problem 1: Desktop-Links auf Mobile. Horizontal scrollen? Nobody got time for that!"
|
||||
|
||||
### Szene 3 (10-15 Sek)
|
||||
|
||||
[Zeige Daumen-Reichweite]
|
||||
"Problem 2: Links außerhalb der Thumb-Zone. Der Daumen erreicht nur 30% des Screens!"
|
||||
|
||||
### Szene 4 (15-25 Sek)
|
||||
|
||||
[Lösungen zeigen]
|
||||
"Die Fixes:
|
||||
|
||||
- Kurze URLs (max 30 Zeichen sichtbar)
|
||||
- Buttons statt Text-Links
|
||||
- Unten platzieren (Daumen-Zone)
|
||||
|
|
@ -270,6 +298,7 @@ Antwort: B! Medium zeigt dir ob es Social, Email, Paid oder Organic Traffic war!
|
|||
- Test auf verschiedenen Geräten!"
|
||||
|
||||
### Szene 5 (25-30 Sek)
|
||||
|
||||
"Mobile-First ist kein Trend, es ist die Realität. Optimiere jetzt oder verliere Kunden. Check meine Bio für den kompletten Mobile-Guide!"
|
||||
|
||||
---
|
||||
|
|
@ -277,10 +306,12 @@ Antwort: B! Medium zeigt dir ob es Social, Email, Paid oder Organic Traffic war!
|
|||
## Post 7: Hashtag-Strategie Deep Dive (Carousel)
|
||||
|
||||
### Slide 1
|
||||
|
||||
**Die 30-10-10 Hashtag-Formel**
|
||||
So findest du die perfekten Hashtags
|
||||
|
||||
### Slide 2
|
||||
|
||||
**Die Formel erklärt**
|
||||
• 30% Große Hashtags (500k+ Posts)
|
||||
• 10% Mittlere (100k-500k Posts)
|
||||
|
|
@ -290,7 +321,9 @@ So findest du die perfekten Hashtags
|
|||
Warum? Balance zwischen Reichweite und Sichtbarkeit!
|
||||
|
||||
### Slide 3
|
||||
|
||||
**Research-Methode**
|
||||
|
||||
1. Starte mit einem Haupt-Hashtag
|
||||
2. Check "Ähnliche Hashtags"
|
||||
3. Analysiere Top-Posts
|
||||
|
|
@ -299,6 +332,7 @@ Warum? Balance zwischen Reichweite und Sichtbarkeit!
|
|||
6. Erstelle deine Liste
|
||||
|
||||
### Slide 4
|
||||
|
||||
**Rotation System**
|
||||
Montag: Set A (30 Hashtags)
|
||||
Mittwoch: Set B (30 andere)
|
||||
|
|
@ -307,6 +341,7 @@ Freitag: Set C (30 neue)
|
|||
Instagram mag keine Wiederholung!
|
||||
|
||||
### Slide 5
|
||||
|
||||
**Tracking Performance**
|
||||
Welche Hashtags bringen Traffic?
|
||||
→ Instagram Insights
|
||||
|
|
@ -316,6 +351,7 @@ Welche Hashtags bringen Traffic?
|
|||
Optimiere basierend auf Daten!
|
||||
|
||||
### Slide 6
|
||||
|
||||
**Tools & Tricks**
|
||||
• Later's Hashtag Suggestions
|
||||
• Hashtagify.me
|
||||
|
|
@ -344,6 +380,7 @@ LinkedIn | Website
|
|||
|
||||
**Zeile 5: Call-to-Action**
|
||||
Rotierend je nach Quartal:
|
||||
|
||||
- Q1: "Jahresplanung? → Kostenloses Beratungsgespräch"
|
||||
- Q2: "Neu: Unser Sommer-Katalog →"
|
||||
- Q3: "Folgen Sie uns auf LinkedIn →"
|
||||
|
|
@ -351,6 +388,7 @@ Rotierend je nach Quartal:
|
|||
|
||||
**Der Link-Trick:**
|
||||
Nutze Kurz-URLs mit Tracking:
|
||||
|
||||
- firma.de/sig-jan (Januar Signatur)
|
||||
- firma.de/sig-meetings (Meeting-Anfragen)
|
||||
- firma.de/sig-katalog (Katalog-Download)
|
||||
|
|
@ -413,13 +451,15 @@ Dort findest man alle Formate an einem Ort.
|
|||
|
||||
[8-15 Sek - Was kannst du testen?]
|
||||
"Teste diese Dinge:
|
||||
|
||||
- Button-Farben
|
||||
- Link-Texte ('Mehr erfahren' vs 'Jetzt entdecken')
|
||||
- Link-Texte ('Mehr erfahren' vs 'Jetzt entdecken')
|
||||
- Platzierung (oben vs unten)
|
||||
- Mit oder ohne Emoji"
|
||||
|
||||
[15-22 Sek - Wie geht's?]
|
||||
"So geht's:
|
||||
|
||||
1. Teile deine Audience 50/50
|
||||
2. Version A an Gruppe 1
|
||||
3. Version B an Gruppe 2
|
||||
|
|
@ -442,4 +482,4 @@ Dort findest man alle Formate an einem Ort.
|
|||
**Donnerstag:** Post 4 (Link-Strategie) - LinkedIn Artikel
|
||||
**Freitag:** Post 5 (UTM-Parameter) - Story-Serie
|
||||
**Samstag:** Community Q&A basierend auf Woche 1
|
||||
**Sonntag:** Best-of Zusammenfassung der Woche
|
||||
**Sonntag:** Best-of Zusammenfassung der Woche
|
||||
|
|
|
|||
|
|
@ -1,15 +1,16 @@
|
|||
# Sprecher-Script: Die Psychologie kurzer URLs
|
||||
|
||||
## Aussprache-Hinweise
|
||||
|
||||
- URLs aussprechen als "U-R-L" oder "Links"
|
||||
- Zahlen unter 13 ausschreiben
|
||||
- Prozentzeichen als "Prozent" sprechen
|
||||
- "=" als "gleich" aussprechen
|
||||
- Betonung bei *kursiven* Wörtern
|
||||
- Betonung bei _kursiven_ Wörtern
|
||||
|
||||
---
|
||||
|
||||
Habt ihr euch schon mal gefragt, warum manche Links einfach *verdächtig* aussehen?
|
||||
Habt ihr euch schon mal gefragt, warum manche Links einfach _verdächtig_ aussehen?
|
||||
|
||||
Heute erkläre ich euch die Psychologie hinter kurzen URLs und warum unser Gehirn sie einfach lieber mag.
|
||||
|
||||
|
|
@ -17,9 +18,9 @@ Heute erkläre ich euch die Psychologie hinter kurzen URLs und warum unser Gehir
|
|||
|
||||
Krasse Zahl: Zweiundvierzig Prozent weniger Klicks bei langen URLs!
|
||||
|
||||
Stellt euch vor - fast die Hälfte eurer potenziellen Besucher klickt nicht, nur weil der Link zu lang ist.
|
||||
Stellt euch vor - fast die Hälfte eurer potenziellen Besucher klickt nicht, nur weil der Link zu lang ist.
|
||||
|
||||
Studien zeigen: URLs über hundert Zeichen erzeugen Misstrauen. Diese ganzen unleserlichen Parameter? Unser Gehirn schreit "Spam-Alarm!"
|
||||
Studien zeigen: URLs über hundert Zeichen erzeugen Misstrauen. Diese ganzen unleserlichen Parameter? Unser Gehirn schreit "Spam-Alarm!"
|
||||
|
||||
Und mobile User? Die scrollen definitiv nicht horizontal für einen Link.
|
||||
|
||||
|
|
@ -27,23 +28,23 @@ Und mobile User? Die scrollen definitiv nicht horizontal für einen Link.
|
|||
|
||||
Jetzt wird's wissenschaftlich: Cognitive Load Theory.
|
||||
|
||||
Unser Gehirn ist faul - im positiven Sinne! Es verarbeitet kurze Informationen schneller und verbindet "einfach" automatisch mit "sicher".
|
||||
Unser Gehirn ist faul - im positiven Sinne! Es verarbeitet kurze Informationen schneller und verbindet "einfach" automatisch mit "sicher".
|
||||
|
||||
Seht ihr den Unterschied?
|
||||
|
||||
Der lange Link mit all dem Kram hinten dran versus der kurze, klare Link?
|
||||
Der lange Link mit all dem Kram hinten dran versus der kurze, klare Link?
|
||||
|
||||
Unser Gehirn liebt den kurzen. Punkt.
|
||||
|
||||
---
|
||||
|
||||
Was macht Links eigentlich vertrauenswürdig?
|
||||
Was macht Links eigentlich vertrauenswürdig?
|
||||
|
||||
Nummer eins mit sechzig Prozent Wichtigkeit: Eine erkennbare Domain. Die Leute wollen wissen, wo sie landen!
|
||||
|
||||
Fünfundzwanzig Prozent: Keine kryptischen Zeichen. Diese komischen Zahlen-Buchstaben-Kombinationen? Nope!
|
||||
|
||||
Zehn Prozent: Kurz und prägnant.
|
||||
Zehn Prozent: Kurz und prägnant.
|
||||
|
||||
Und die letzten fünf Prozent? HTTPS-Verschlüsselung - das kleine Schloss-Symbol.
|
||||
|
||||
|
|
@ -51,7 +52,7 @@ Und die letzten fünf Prozent? HTTPS-Verschlüsselung - das kleine Schloss-Symbo
|
|||
|
||||
Okay, wie optimiert ihr jetzt eure Links?
|
||||
|
||||
Erstens: Nutzt sprechende URLs! Statt irgendwelche Zahlen lieber "Schrägstrich sommer-sale".
|
||||
Erstens: Nutzt sprechende URLs! Statt irgendwelche Zahlen lieber "Schrägstrich sommer-sale".
|
||||
|
||||
Zweitens: Keine Sonderzeichen! Die verwirren nur.
|
||||
|
||||
|
|
@ -68,4 +69,3 @@ Also merkt euch: Kurze URLs gleich mehr Vertrauen gleich mehr Klicks!
|
|||
Pro-Tipp: Tools wie youload helfen euch, lange URLs zu kürzen UND zu personalisieren. Win-win!
|
||||
|
||||
Speichert diesen Post für später und dropped ein Herz, wenn ihr was gelernt habt!
|
||||
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ Links kürzen, Profile erstellen, Teams managen. Die smarteste Lösung für dein
|
|||
|
||||
## Ausführliche Bio
|
||||
|
||||
**Uload - Die All-in-One Plattform für deine digitale Präsenz**
|
||||
**Uload - Die All-in-One Plattform für deine digitale Präsenz**
|
||||
|
||||
✨ **Was wir machen:**
|
||||
Wir vereinen professionelles Link-Management mit kreativer Selbstdarstellung. Egal ob Creator, Freelancer oder Team - Uload gibt dir die Tools für deinen digitalen Erfolg.
|
||||
|
|
@ -157,4 +157,4 @@ Gehostet in Deutschland, DSGVO-konform, SSL-verschlüsselt mit 99.9% Uptime.
|
|||
4. **Updates (10%)**
|
||||
- Neue Features
|
||||
- Produkt-Updates
|
||||
- Behind-the-Scenes
|
||||
- Behind-the-Scenes
|
||||
|
|
|
|||
|
|
@ -36,9 +36,11 @@
|
|||
## 2. Ausgearbeitete Content-Formate
|
||||
|
||||
### Format 1: "Link-Makeover Monday" (Wöchentliche Serie)
|
||||
|
||||
**Konzept:** Jeden Montag verwandeln wir das chaotische Link-Setup eines Followers in eine professionelle Uload-Lösung
|
||||
|
||||
**Aufbau:**
|
||||
|
||||
- Slide 1: Vorher - Screenshot des Problems
|
||||
- Slide 2-4: Transformation-Prozess mit Uload
|
||||
- Slide 5: Nachher - Fertiges Setup
|
||||
|
|
@ -47,9 +49,11 @@
|
|||
**Hook:** "Dein Link-Chaos nervt? Wir räumen auf! 🔗✨"
|
||||
|
||||
### Format 2: "60-Sekunden QR-Tutorial" (Reels/Stories)
|
||||
|
||||
**Konzept:** Schnelle, actionreiche Tutorials zur QR-Code Erstellung
|
||||
|
||||
**Struktur:**
|
||||
|
||||
- 0-5 Sek: Problem zeigen (lange URLs auf Flyern)
|
||||
- 5-15 Sek: Uload öffnen
|
||||
- 15-40 Sek: QR-Code erstellen & anpassen
|
||||
|
|
@ -59,18 +63,22 @@
|
|||
**Musik:** Trending Audio mit schnellem Beat
|
||||
|
||||
### Format 3: "Template Tuesday" (Carousel Posts)
|
||||
|
||||
**Konzept:** Wöchentliche Vorstellung von 3 Profilkarten-Templates
|
||||
|
||||
**Slides:**
|
||||
|
||||
1. Cover: "3 Link-in-Bio Templates für [Zielgruppe]"
|
||||
2-4. Je ein Template mit Use Case
|
||||
5. Vergleichstabelle der Features
|
||||
6. CTA: "Welches ist dein Favorit? Kommentiere 1, 2 oder 3!"
|
||||
2-4. Je ein Template mit Use Case
|
||||
2. Vergleichstabelle der Features
|
||||
3. CTA: "Welches ist dein Favorit? Kommentiere 1, 2 oder 3!"
|
||||
|
||||
### Format 4: "Analytics Insights" (Infografik-Serie)
|
||||
|
||||
**Konzept:** Datenvisualisierung und Tipps zur Link-Performance
|
||||
|
||||
**Beispiele:**
|
||||
|
||||
- "Die besten Zeiten für Link-Shares"
|
||||
- "Mobile vs. Desktop Click-Raten"
|
||||
- "Top 5 Referrer-Quellen"
|
||||
|
|
@ -78,18 +86,22 @@
|
|||
**Design:** Clean, minimalistisch mit Uload-Branding
|
||||
|
||||
### Format 5: "Creator Spotlight" (IGTV/Reels)
|
||||
|
||||
**Konzept:** 2-3 Minuten Interview mit erfolgreichen Uload-Nutzern
|
||||
|
||||
**Fragen:**
|
||||
|
||||
- Wie nutzt du Uload?
|
||||
- Lieblings-Feature?
|
||||
- ROI/Erfolge?
|
||||
- Tipps für andere?
|
||||
|
||||
### Format 6: "Quick Tips Thursday" (Stories-Serie)
|
||||
|
||||
**Konzept:** 5-teilige Story-Serie mit schnellen Hacks
|
||||
|
||||
**Beispiel-Serie:**
|
||||
|
||||
- Story 1: Hook "5 Uload-Tricks in 60 Sekunden"
|
||||
- Story 2-5: Je ein Trick (15 Sek)
|
||||
- Story 6: Swipe-Up/Link zu Anmeldung
|
||||
|
|
@ -97,9 +109,11 @@
|
|||
## 3. Content-Kalender
|
||||
|
||||
### Posting-Frequenz
|
||||
|
||||
**Optimal:** 5-6 Posts pro Woche + tägliche Stories
|
||||
|
||||
**Wochenrhythmus:**
|
||||
|
||||
- **Montag:** Link-Makeover Monday (Carousel)
|
||||
- **Dienstag:** Template Tuesday (Carousel)
|
||||
- **Mittwoch:** User Success/Testimonial (Reel)
|
||||
|
|
@ -111,36 +125,43 @@
|
|||
### Woche 1: Launch-Content-Plan
|
||||
|
||||
**Montag - Launch-Ankündigung**
|
||||
|
||||
- Post: Hero-Visual "Uload ist da!"
|
||||
- Stories: Countdown + Features-Preview
|
||||
- Reel: "3 Gründe warum du Uload brauchst"
|
||||
|
||||
**Dienstag - Template Tuesday**
|
||||
|
||||
- Carousel: "3 Templates für Content Creators"
|
||||
- Stories: Template-Customization Tutorial
|
||||
- Engagement: "Welches Template passt zu dir?"
|
||||
|
||||
**Mittwoch - QR-Code Magic**
|
||||
|
||||
- Reel: "Von URL zu QR in 30 Sekunden"
|
||||
- Feed-Post: QR-Use-Cases Infografik
|
||||
- Stories: User zeigen ihre QR-Codes
|
||||
|
||||
**Donnerstag - Quick Tips**
|
||||
|
||||
- Carousel: "5 Link-Management Hacks"
|
||||
- Stories-Serie: Jeden Hack live zeigen
|
||||
- IGTV: Ausführliches Tutorial
|
||||
|
||||
**Freitag - Feature Friday**
|
||||
|
||||
- Reel: "Das kann nur Uload: Team-Kollaboration"
|
||||
- Feed-Post: Team-Features erklärt
|
||||
- Stories: Q&A zu Teams
|
||||
|
||||
**Samstag - Community Day**
|
||||
|
||||
- Post: "Zeig uns dein Setup!"
|
||||
- Stories: User-Generated Content reposten
|
||||
- Engagement: Beste Setups werden gefeatured
|
||||
|
||||
**Sonntag - Soft Sell**
|
||||
|
||||
- Carousel: "Free vs. Pro - Was ist richtig für dich?"
|
||||
- Stories: Testimonials
|
||||
- Link: Special Launch-Offer
|
||||
|
|
@ -148,11 +169,13 @@
|
|||
### Story-Strategie (Täglich)
|
||||
|
||||
**Fixe Story-Elemente:**
|
||||
|
||||
- Morgens: Tipp des Tages
|
||||
- Mittags: User-Showcase oder FAQ
|
||||
- Abends: Behind-the-Scenes oder Team-Update
|
||||
|
||||
**Interaktive Elemente:**
|
||||
|
||||
- Umfragen: "Welches Feature willst du als nächstes?"
|
||||
- Quiz: "Welcher Link-Typ bist du?"
|
||||
- Q&A-Sticker: Wöchentliche Fragerunde
|
||||
|
|
@ -164,11 +187,13 @@
|
|||
#UloadLinks #MoreThanJustLinks #UloadIt
|
||||
|
||||
**Größen-Mix pro Post:**
|
||||
|
||||
- 5-7 große Hashtags (>500k Posts): #LinkInBio #ContentCreator
|
||||
- 8-10 mittlere (100k-500k): #LinkManagement #QRCode
|
||||
- 8-10 kleine (<100k): #DigitalToolsForCreators #LinkShortener
|
||||
|
||||
**Zielgruppen-spezifisch:**
|
||||
|
||||
- Creators: #CreatorTools #InfluencerTips #ContentStrategy
|
||||
- Business: #SmallBusinessTools #TeamCollaboration #B2BMarketing
|
||||
- Events: #EventMarketing #QRCodeMenu #DigitalEvents
|
||||
|
|
@ -176,6 +201,7 @@
|
|||
### KPIs & Erfolgsmessung
|
||||
|
||||
**Wöchentlich tracken:**
|
||||
|
||||
- Follower-Wachstum
|
||||
- Engagement-Rate (Ziel: >5%)
|
||||
- Story-Views
|
||||
|
|
@ -183,6 +209,7 @@
|
|||
- Saves-to-Likes Ratio
|
||||
|
||||
**Monatlich analysieren:**
|
||||
|
||||
- Top-performing Content-Typen
|
||||
- Beste Posting-Zeiten
|
||||
- Conversion Rate (Follower → User)
|
||||
|
|
@ -191,6 +218,7 @@
|
|||
### Content-Produktion
|
||||
|
||||
**Batch-Creation:**
|
||||
|
||||
- Montags: Wochenplanung & Ideation
|
||||
- Dienstags: Content-Erstellung (Grafiken, Videos)
|
||||
- Mittwochs: Copywriting & Hashtag-Research
|
||||
|
|
@ -198,7 +226,8 @@
|
|||
- Freitags: Analytics-Review & Optimierung
|
||||
|
||||
**Tools:**
|
||||
|
||||
- Design: Canva/Figma mit Uload-Templates
|
||||
- Scheduling: Later/Buffer
|
||||
- Analytics: Instagram Insights + eigene Tracking
|
||||
- Video: CapCut/InShot für Reels
|
||||
- Video: CapCut/InShot für Reels
|
||||
|
|
|
|||
|
|
@ -11,91 +11,121 @@
|
|||
- Abkürzungen ausschreiben: "z.B." wird zu "zum Beispiel"
|
||||
|
||||
## Script 1: Willkommen bei youload
|
||||
|
||||
Hey! Kennst du das? Du hast tausend Links für Instagram, aber nur Platz für einen in der Bio? Nervt, oder? Ich zeig dir jetzt in dreißig Sekunden, wie youload dein Leben verändert. Swipe up!
|
||||
|
||||
## Script 2: QR-Code in zehn Sekunden
|
||||
|
||||
Stopp! Ich erstelle jetzt einen QR-Code in zehn Sekunden. Bereit? Los! youload öffnen, Link eingeben, QR-Code klicken, Farbe wählen, fertig! Boom! Acht Sekunden! Krass, oder?
|
||||
|
||||
## Script 3: Das Link-Chaos
|
||||
|
||||
Okay, real talk. Ich hatte früher einen Notizzettel mit all meinen Links. Dann Excel. Dann Google Docs. Totales Chaos! Jetzt? Alles in youload. Ein Klick, alle Links da. Leben gleich einfacher.
|
||||
|
||||
## Script 4: DSGVO minus Warum es wichtig ist
|
||||
|
||||
Leute, Datenschutz ist kein Spaß! Wisst ihr, dass viele US-Tools eure Daten illegal speichern? youload läuft auf deutschen Servern und ist made in germany. Eure Daten, eure Kontrolle. Safety first!
|
||||
|
||||
## Script 5: Template Tuesday Teaser
|
||||
|
||||
Morgen ist Template Tuesday! Ich zeige euch drei mega Profilkarten-Designs. Eins für Fitness-Coaches, eins für Fotografen, eins für Restaurants. Welches wollt ihr zuerst sehen? Votet jetzt!
|
||||
|
||||
## Script 6: Der Preis-Check
|
||||
|
||||
Okay, Klartext: Linktree kostet neun Dollar. Bitly zehn Dollar. youload? Vier Euro neunundneunzig und du kriegst MEHR Features. Mathe ist nicht meine Stärke, aber das ist ein No-Brainer!
|
||||
|
||||
## Script 7: Team-Feature Live Demo
|
||||
|
||||
Schaut mal! Mein Team kann jetzt alle Links bearbeiten. Sarah macht QR-Codes, Tom die Analytics, ich chill. Teamwork macht den Traum work! Swipe up für Team-Features!
|
||||
|
||||
## Script 8: Analytics Insights
|
||||
|
||||
Whoa! Gerade gecheckt: Mein Link wurde fünfhundert mal geklickt! Siebzig Prozent mobile, dreißig Prozent desktop. Die meisten aus Deutschland. Diese Insights! Besser als Instagram Analytics, no cap!
|
||||
|
||||
## Script 9: Link-Makeover Ankündigung
|
||||
|
||||
Attention! Nächsten Montag mache ich ein Link-Makeover! Schickt mir euer Link-Chaos per DM. Der krasseste Vorher-Nachher gewinnt einen Pro-Account für drei Monate!
|
||||
|
||||
## Script 10: Custom URL Tutorial
|
||||
|
||||
Quick Tip! Statt youload punkt de Schrägstrich xyz eins zwei drei kannst du youload punkt de Schrägstrich deinname machen! Einfach beim Erstellen Custom Code eingeben. Professionell, merkbar, sexy! Probier's aus!
|
||||
|
||||
## Script 11: Morgen-Motivation
|
||||
|
||||
Guten Morgen! Weißt du was geil ist? Aufwachen und sehen, dass deine Links über Nacht zweihundert mal geklickt wurden. Passive Reichweite während du schläfst. Das ist youload Power!
|
||||
|
||||
## Script 12: Problem-Lösung
|
||||
|
||||
"Ich hab keine Zeit für noch ein Tool!" Digga, youload SPART dir Zeit! Fünf Minuten Setup, dann läuft's. Keine zwanzig Tabs mehr offen. Alles an einem Ort. Zeit gespart gleich Geld gespart.
|
||||
|
||||
## Script 13: Feature Friday Ankündigung
|
||||
|
||||
Es ist Freitag! Heute zeig ich euch das krasseste Feature: Passwortgeschützte Links! Perfekt für exklusiven Content. VIP only! Tutorial kommt in einer Stunde!
|
||||
|
||||
## Script 14: User Success Story
|
||||
|
||||
OMG! Julia hat mir geschrieben: "Seit youload vierzig Prozent mehr Klicks!" Sie nutzt es für ihren Onlineshop. Alle Produkte, ein Link. Smart! Zeig mir deine Success Story!
|
||||
|
||||
## Script 15: Weekend Vibes
|
||||
|
||||
Wochenende! Aber deine Links arbeiten weiter. Während du chillst, trackt youload jeden Klick. Montag checkst du die Stats und weißt genau, was funktioniert hat. Work smart, not hard!
|
||||
|
||||
## Script 16: QR-Code Use Case
|
||||
|
||||
Gestern im Restaurant: QR-Code auf dem Tisch. Speisekarte digital! Mit youload in zwei Minuten gemacht. Kein Druck mehr, Updates instant. Die Zukunft, Leute!
|
||||
|
||||
## Script 17: Free vs Pro
|
||||
|
||||
"Brauch ich Pro?" Honest: Starte free! Fünfzig Links reichen für den Anfang. Wenn du wächst, upgrade. Kein Stress, kein Druck. youload wächst mit dir!
|
||||
|
||||
## Script 18: Behind the Scenes
|
||||
|
||||
Fun Fact: youload wurde gebaut, weil wir selbst genervt waren! Kein Tool hatte alles. Also: Selbst gemacht. Von Creators für Creators. Real recognize real!
|
||||
|
||||
## Script 19: Monday Motivation
|
||||
|
||||
Neue Woche, neue Links! Tipp: Räum montags deine Links auf. Lösche alte, update neue. Zehn Minuten Investment, ganze Woche organized. Let's go!
|
||||
|
||||
## Script 20: Mittwochs-Weisheit
|
||||
|
||||
Weisheit des Tages: Ein schlechter Link ist wie ein schlechter Händedruck. Erster Eindruck zählt! Mit youload machst du IMMER einen guten ersten Eindruck.
|
||||
|
||||
## Script 21: Template Showcase
|
||||
|
||||
Check das! Diese Profilkarte! Gradient-Header, Social Icons, Newsletter-Signup, Shop-Links. Alles drin! In fünf Minuten gebaut. Template Store macht's möglich!
|
||||
|
||||
## Script 22: Donnerstag Hack
|
||||
|
||||
Life Hack: Erstelle verschiedene Profilkarten für verschiedene Zielgruppen! Eine für Insta, eine für LinkedIn, eine für TikTok. Same person, different vibe!
|
||||
|
||||
## Script 23: Freitag Feature
|
||||
|
||||
Wisst ihr was? Ihr könnt Links zeitlich begrenzen! Black Friday Sale? Link läuft nur achtundvierzig Stunden. FOMO Marketing at its best! Swipe up für Tutorial!
|
||||
|
||||
## Script 24: Samstag Stories
|
||||
|
||||
Real Question: Wie viele Links habt ihr in eurer Bio verschwendet? Mit youload: Einer reicht. Unendliche Möglichkeiten dahinter. One Link to rule them all!
|
||||
|
||||
## Script 25: Sonntag Reflexion
|
||||
|
||||
Sonntagsgedanke: Deine Links sind dein digitales Zuhause. Würdest du Gäste in ein chaotisches Haus einladen? Eben. youload gleich digitale Ordnung. Selbstliebe beginnt bei organisierten Links!
|
||||
|
||||
## Script 26: Bulk Operations
|
||||
|
||||
Yo! Wusstet ihr: Ihr könnt fünfzig Links gleichzeitig bearbeiten? Select all, add tag, done! Was früher Stunden dauerte: zwei Klicks. Efficiency level tausend!
|
||||
|
||||
## Script 27: Mobile First
|
||||
|
||||
Achtzig Prozent eurer Follower sind mobile! youload ist mobile optimized. Jeder Link, jede Profilkarte, jeder QR-Code. Perfect auf jedem Screen. Responsive ist kein Feature, es ist ein Must!
|
||||
|
||||
## Script 28: Tag System
|
||||
|
||||
Organisation Tipp: Nutzt Tags! Hashtag kampagne zweitausendvierundzwanzig Hashtag blackfriday Hashtag newsletter. Später findest du alles sofort. Marie Kondo wäre stolz auf euch!
|
||||
|
||||
## Script 29: Click Limits
|
||||
|
||||
Geheimer Trick: Setzt Click-Limits für exklusive Angebote! "Nur die ersten hundert kriegen Rabatt!" Boom, urgency created! Marketing Psychology hundertundeins!
|
||||
|
||||
## Script 30: Community Love
|
||||
Danke! Seriously! Ihr macht youload zu dem was es ist. Euer Feedback, eure Ideen, eure Support. Wir bauen das für EUCH! Drop ein Herz wenn ihr Teil der youload Familie seid!
|
||||
|
||||
Danke! Seriously! Ihr macht youload zu dem was es ist. Euer Feedback, eure Ideen, eure Support. Wir bauen das für EUCH! Drop ein Herz wenn ihr Teil der youload Familie seid!
|
||||
|
|
|
|||
|
|
@ -3,11 +3,13 @@
|
|||
## 🎯 Grundprinzipien
|
||||
|
||||
### 1. **Authentizität vor Übertreibung**
|
||||
|
||||
- ❌ **Vermeiden:** Unrealistische Nutzerzahlen ("10.000+ Nutzer" wenn man gerade startet)
|
||||
- ✅ **Stattdessen:** Ehrliche Positionierung ("Beta Launch 2024", "Neu am Markt")
|
||||
- ✅ **Alternative Metriken:** Response-Zeit, Uptime, Herkunft ("Made in Germany")
|
||||
|
||||
### 2. **Konsistente Preisgestaltung**
|
||||
|
||||
- Alle Preisangaben müssen identisch sein zwischen:
|
||||
- Landing Page
|
||||
- Pricing Page
|
||||
|
|
@ -15,6 +17,7 @@
|
|||
- Immer prüfen: `/src/routes/(app)/pricing/+page.svelte` als Single Source of Truth
|
||||
|
||||
### 3. **Realistische Social Proof**
|
||||
|
||||
- ❌ **Vermeiden:** Fake Testimonials mit erfundenen Erfolgsgeschichten
|
||||
- ✅ **Alternativen für neue Startups:**
|
||||
- Beta-Tester Feedback
|
||||
|
|
@ -25,6 +28,7 @@
|
|||
## 🏗️ Struktur-Empfehlungen
|
||||
|
||||
### Hero Section
|
||||
|
||||
```
|
||||
1. Trust Badges (DSGVO, Sicherheit, Performance)
|
||||
2. Klare Value Proposition (Headline + Subheadline)
|
||||
|
|
@ -33,6 +37,7 @@
|
|||
```
|
||||
|
||||
### Content Sections (Reihenfolge)
|
||||
|
||||
```
|
||||
1. Hero Section
|
||||
2. Zielgruppen (mit Tabs für verschiedene Personas)
|
||||
|
|
@ -46,6 +51,7 @@
|
|||
## 🎨 Design Patterns
|
||||
|
||||
### Farbcodierung
|
||||
|
||||
- **Primary Action:** `bg-theme-primary`
|
||||
- **Secondary Action:** `border-theme-primary`
|
||||
- **Success:** Grün (#10b981)
|
||||
|
|
@ -53,6 +59,7 @@
|
|||
- **Premium/Special:** Purple (#9333ea)
|
||||
|
||||
### Komponenten-Struktur
|
||||
|
||||
```
|
||||
/src/lib/components/landing/
|
||||
├── HeroSection.svelte
|
||||
|
|
@ -66,18 +73,21 @@
|
|||
## ✅ Checkliste vor Go-Live
|
||||
|
||||
### Content Check
|
||||
|
||||
- [ ] Alle Preise konsistent?
|
||||
- [ ] Keine übertriebenen Metriken?
|
||||
- [ ] Rechtschreibung geprüft?
|
||||
- [ ] CTAs klar und eindeutig?
|
||||
|
||||
### Technical Check
|
||||
|
||||
- [ ] Mobile responsive?
|
||||
- [ ] Ladezeiten optimiert?
|
||||
- [ ] Forms funktionieren?
|
||||
- [ ] Analytics tracking aktiv?
|
||||
|
||||
### Legal Check
|
||||
|
||||
- [ ] DSGVO-Hinweise vorhanden?
|
||||
- [ ] Impressum verlinkt?
|
||||
- [ ] Datenschutz verlinkt?
|
||||
|
|
@ -86,17 +96,20 @@
|
|||
## 🚀 Launch-Phasen Messaging
|
||||
|
||||
### Phase 1: Beta Launch (Aktuell)
|
||||
|
||||
- "Beta-Zugang sichern"
|
||||
- "Sei einer der Ersten"
|
||||
- "Early Access Features"
|
||||
- "Exklusiver Beta-Preis"
|
||||
|
||||
### Phase 2: Public Launch
|
||||
|
||||
- "Jetzt verfügbar"
|
||||
- "X Beta-Tester vertrauen uns"
|
||||
- "Launch-Angebot"
|
||||
|
||||
### Phase 3: Growth
|
||||
|
||||
- Echte Nutzerzahlen
|
||||
- Case Studies
|
||||
- Erfolgsgeschichten
|
||||
|
|
@ -107,18 +120,21 @@
|
|||
Statt Nutzerzahlen diese Metriken verwenden:
|
||||
|
||||
### Technische Metriken
|
||||
|
||||
- Uptime (99.9%)
|
||||
- Response Time (<100ms)
|
||||
- Support Response (<2h)
|
||||
- API Verfügbarkeit
|
||||
|
||||
### Qualitative Metriken
|
||||
|
||||
- Made in Germany/EU
|
||||
- DSGVO-konform
|
||||
- SSL-verschlüsselt
|
||||
- Open Source (falls zutreffend)
|
||||
|
||||
### Zeitliche Metriken
|
||||
|
||||
- Gegründet/Launch Jahr
|
||||
- Updates pro Monat
|
||||
- Feature Releases
|
||||
|
|
@ -126,6 +142,7 @@ Statt Nutzerzahlen diese Metriken verwenden:
|
|||
## 🔄 A/B Testing Empfehlungen
|
||||
|
||||
### Test-Elemente
|
||||
|
||||
1. Headlines (Value Proposition)
|
||||
2. CTA-Buttons (Text & Farbe)
|
||||
3. Pricing-Darstellung
|
||||
|
|
@ -133,6 +150,7 @@ Statt Nutzerzahlen diese Metriken verwenden:
|
|||
5. Trust-Signal Platzierung
|
||||
|
||||
### Tracking
|
||||
|
||||
- Conversion Rate
|
||||
- Bounce Rate
|
||||
- Time on Page
|
||||
|
|
@ -142,6 +160,7 @@ Statt Nutzerzahlen diese Metriken verwenden:
|
|||
## 📝 Copy-Writing Tipps
|
||||
|
||||
### Do's
|
||||
|
||||
- ✅ Kurze, prägnante Headlines
|
||||
- ✅ Benefits vor Features
|
||||
- ✅ Aktive Sprache
|
||||
|
|
@ -149,6 +168,7 @@ Statt Nutzerzahlen diese Metriken verwenden:
|
|||
- ✅ Dringlichkeit ohne Fake-Scarcity
|
||||
|
||||
### Don'ts
|
||||
|
||||
- ❌ Technischer Jargon
|
||||
- ❌ Zu viele Superlative
|
||||
- ❌ Unklare CTAs
|
||||
|
|
@ -158,18 +178,21 @@ Statt Nutzerzahlen diese Metriken verwenden:
|
|||
## 🎯 Conversion-Optimierung
|
||||
|
||||
### Above the Fold
|
||||
|
||||
- Value Proposition in 5 Sekunden verstehbar
|
||||
- Primärer CTA sichtbar
|
||||
- Trust-Signale erkennbar
|
||||
- Keine Ablenkungen
|
||||
|
||||
### Form-Optimierung
|
||||
|
||||
- Minimale Felder
|
||||
- Inline-Validation
|
||||
- Clear Error Messages
|
||||
- Progress Indicators (bei Multi-Step)
|
||||
|
||||
### Mobile First
|
||||
|
||||
- Touch-optimierte Buttons (min. 44x44px)
|
||||
- Lesbare Schriftgrößen (min. 16px)
|
||||
- Vereinfachte Navigation
|
||||
|
|
@ -178,12 +201,14 @@ Statt Nutzerzahlen diese Metriken verwenden:
|
|||
## 🔍 Regelmäßige Reviews
|
||||
|
||||
### Monatlich prüfen
|
||||
|
||||
- Analytics-Daten
|
||||
- Conversion-Raten
|
||||
- User-Feedback
|
||||
- Konkurrenz-Analyse
|
||||
|
||||
### Quarterly Updates
|
||||
|
||||
- Content-Refresh
|
||||
- Feature-Updates
|
||||
- Testimonial-Updates
|
||||
|
|
@ -201,5 +226,5 @@ Statt Nutzerzahlen diese Metriken verwenden:
|
|||
|
||||
---
|
||||
|
||||
*Letztes Update: Januar 2025*
|
||||
*Nächstes Review: Q2 2025*
|
||||
_Letztes Update: Januar 2025_
|
||||
_Nächstes Review: Q2 2025_
|
||||
|
|
|
|||
|
|
@ -1,249 +1,268 @@
|
|||
---
|
||||
📊 Marktpositionierung & Zielgruppen
|
||||
📊 Marktpositionierung & Zielgruppen
|
||||
|
||||
Primäre Zielgruppen
|
||||
Primäre Zielgruppen
|
||||
|
||||
1. Digital Creators & Influencer
|
||||
- Benötigen zentrale Link-Hubs für Social Media (Instagram, TikTok)
|
||||
- Profitieren von anpassbaren Profilseiten und QR-Codes
|
||||
- Schätzen detaillierte Click-Analytics
|
||||
2. Kleine bis mittlere Unternehmen (KMU)
|
||||
- Team-Kollaboration für Marketing-Teams
|
||||
- Markenkonformes Link-Management
|
||||
- Kostengünstige Alternative zu Enterprise-Lösungen
|
||||
3. Freelancer & Agenturen
|
||||
- Multi-Client-Management möglich
|
||||
- White-Label-Potenzial durch Custom Domains (bald)
|
||||
- Professionelle Analytics für Kunden-Reporting
|
||||
4. Event-Veranstalter & Gastgewerbe
|
||||
- QR-Code-Generation für Speisekarten, Events
|
||||
- Zeitlich begrenzte Links für Aktionen
|
||||
- Passwortgeschützte Links für exklusive Inhalte
|
||||
1. Digital Creators & Influencer
|
||||
|
||||
---
|
||||
🎯 Kernfunktionalitäten & USPs
|
||||
|
||||
1. Umfassendes Link-Management
|
||||
- Benötigen zentrale Link-Hubs für Social Media (Instagram, TikTok)
|
||||
- Profitieren von anpassbaren Profilseiten und QR-Codes
|
||||
- Schätzen detaillierte Click-Analytics
|
||||
|
||||
- ✅ Kurz-URLs mit Custom Codes
|
||||
- ✅ Erweiterte Metadaten (Titel, Beschreibung)
|
||||
- ✅ Ablaufdatum & Click-Limits
|
||||
- ✅ Passwortschutz
|
||||
- ✅ Tag-System zur Organisation
|
||||
- ✅ Bulk-Operationen
|
||||
2. Kleine bis mittlere Unternehmen (KMU)
|
||||
|
||||
2. Innovative Profilkarten (Alleinstellungsmerkmal!)
|
||||
|
||||
- 🎨 3-Stufen-Builder: Anfänger, Fortgeschritten, Experte
|
||||
- 📦 Modulares System: Header, Content, Media, Links
|
||||
- 🎭 Template Store: Vorgefertigte Designs
|
||||
- 🔄 Drag & Drop Sortierung
|
||||
- 👁️ Live-Preview
|
||||
- Team-Kollaboration für Marketing-Teams
|
||||
- Markenkonformes Link-Management
|
||||
- Kostengünstige Alternative zu Enterprise-Lösungen
|
||||
|
||||
3. Team-Kollaboration (NEU)
|
||||
3. Freelancer & Agenturen
|
||||
|
||||
- 👥 Skalierbare Team-Größen (1-∞ Mitglieder)
|
||||
- 🔐 Granulare Berechtigungen
|
||||
- 📊 Geteilte Analytics
|
||||
- 🏢 Workspace-Management
|
||||
|
||||
4. Professionelle Analytics
|
||||
- Multi-Client-Management möglich
|
||||
- White-Label-Potenzial durch Custom Domains (bald)
|
||||
- Professionelle Analytics für Kunden-Reporting
|
||||
|
||||
- 📈 Click-Tracking in Echtzeit
|
||||
- 🌍 Browser & Device Distribution
|
||||
- 🔗 Referrer-Analyse
|
||||
- 📅 Zeitbasierte Auswertungen
|
||||
- 📊 Visuelle Dashboards
|
||||
4. Event-Veranstalter & Gastgewerbe
|
||||
|
||||
5. QR-Code-Generator
|
||||
|
||||
- 🎨 Anpassbare Farben (Schwarz, Weiß, Gold)
|
||||
- 📐 Multiple Formate (PNG, SVG, JPG)
|
||||
- 📱 Mobile-optimiert
|
||||
- 💾 Download-Funktion
|
||||
- QR-Code-Generation für Speisekarten, Events
|
||||
- Zeitlich begrenzte Links für Aktionen
|
||||
- Passwortgeschützte Links für exklusive Inhalte
|
||||
---
|
||||
|
||||
6. Internationalisierung
|
||||
🎯 Kernfunktionalitäten & USPs
|
||||
|
||||
- 🌍 Mehrsprachige Unterstützung (Paraglide.js)
|
||||
- 🔄 A/B-Testing Framework integriert
|
||||
- 🎯 Lokalisierte User Experience
|
||||
1. Umfassendes Link-Management
|
||||
|
||||
---
|
||||
💰 Preismodell & Monetarisierung
|
||||
- ✅ Kurz-URLs mit Custom Codes
|
||||
- ✅ Erweiterte Metadaten (Titel, Beschreibung)
|
||||
- ✅ Ablaufdatum & Click-Limits
|
||||
- ✅ Passwortschutz
|
||||
- ✅ Tag-System zur Organisation
|
||||
- ✅ Bulk-Operationen
|
||||
|
||||
Freemium-Modell mit 4 Stufen:
|
||||
2. Innovative Profilkarten (Alleinstellungsmerkmal!)
|
||||
|
||||
| Plan | Preis | Highlights | Zielgruppe |
|
||||
|-----------|--------------|--------------------------------------|------------------------|
|
||||
| Free | 0€ | 50 Links/Monat, 1 Team-Mitglied | Privatnutzer, Tester |
|
||||
| Pro | 4,99€/Monat | 500 Links, 3 Team-Mitglieder | Freelancer, Creators |
|
||||
| Team | 9,99€/Monat | Unbegrenzte Links, 10 Mitglieder | Kleine Teams |
|
||||
| Team Plus | 19,99€/Monat | Alles unbegrenzt, API, Custom Domain | Agenturen, Unternehmen |
|
||||
- 🎨 3-Stufen-Builder: Anfänger, Fortgeschritten, Experte
|
||||
- 📦 Modulares System: Header, Content, Media, Links
|
||||
- 🎭 Template Store: Vorgefertigte Designs
|
||||
- 🔄 Drag & Drop Sortierung
|
||||
- 👁️ Live-Preview
|
||||
|
||||
Monetarisierungs-Strategie:
|
||||
3. Team-Kollaboration (NEU)
|
||||
|
||||
- ✅ Stripe-Integration für nahtlose Zahlungen
|
||||
- ✅ Upgrade-Prompts an strategischen Punkten
|
||||
- ✅ Feature-Gating für Premium-Funktionen
|
||||
- ✅ Team-Lizenzen als Umsatztreiber
|
||||
- 👥 Skalierbare Team-Größen (1-∞ Mitglieder)
|
||||
- 🔐 Granulare Berechtigungen
|
||||
- 📊 Geteilte Analytics
|
||||
- 🏢 Workspace-Management
|
||||
|
||||
---
|
||||
🏗️ Technische Exzellenz
|
||||
4. Professionelle Analytics
|
||||
|
||||
Modern Tech Stack:
|
||||
- 📈 Click-Tracking in Echtzeit
|
||||
- 🌍 Browser & Device Distribution
|
||||
- 🔗 Referrer-Analyse
|
||||
- 📅 Zeitbasierte Auswertungen
|
||||
- 📊 Visuelle Dashboards
|
||||
|
||||
- Frontend: SvelteKit 2.0 + Svelte 5 (Cutting-Edge)
|
||||
- Backend: PocketBase (Embedded, Skalierbar)
|
||||
- Styling: Tailwind CSS 4.0 (Neueste Version)
|
||||
- Deployment: Docker + Coolify auf Hetzner VPS
|
||||
- Database: SQLite via PocketBase (Performant)
|
||||
- Payments: Stripe Integration
|
||||
5. QR-Code-Generator
|
||||
|
||||
DevOps & Performance:
|
||||
- 🎨 Anpassbare Farben (Schwarz, Weiß, Gold)
|
||||
- 📐 Multiple Formate (PNG, SVG, JPG)
|
||||
- 📱 Mobile-optimiert
|
||||
- 💾 Download-Funktion
|
||||
|
||||
- 🚀 Automatische Deployments via Git Push
|
||||
- 🔒 HTTPS-Enforced mit SSL
|
||||
- 📊 Health-Monitoring
|
||||
- 🎯 99.9% Uptime SLA
|
||||
- ⚡ Optimierte Build-Pipeline
|
||||
6. Internationalisierung
|
||||
|
||||
---
|
||||
🎨 User Experience & Design
|
||||
- 🌍 Mehrsprachige Unterstützung (Paraglide.js)
|
||||
- 🔄 A/B-Testing Framework integriert
|
||||
- 🎯 Lokalisierte User Experience
|
||||
|
||||
Stärken:
|
||||
---
|
||||
|
||||
1. Intuitive Navigation: Klare Menüstruktur, konsistente UX
|
||||
2. Dark Mode Support: Vollständige Theme-Unterstützung
|
||||
3. Responsive Design: Mobile-First Approach
|
||||
4. Visual Feedback: Toast-Nachrichten, Ladeanimationen
|
||||
5. Accessibility: ARIA-Labels, Keyboard-Navigation
|
||||
💰 Preismodell & Monetarisierung
|
||||
|
||||
Design-Philosophie:
|
||||
Freemium-Modell mit 4 Stufen:
|
||||
|
||||
- Minimalistisch aber funktional
|
||||
- Farbcodierte Statusanzeigen
|
||||
- Konsistente Icon-Verwendung (Lucide Icons)
|
||||
- Moderne Card-Based Layouts
|
||||
| Plan | Preis | Highlights | Zielgruppe |
|
||||
| --------- | ------------ | ------------------------------------ | ---------------------- |
|
||||
| Free | 0€ | 50 Links/Monat, 1 Team-Mitglied | Privatnutzer, Tester |
|
||||
| Pro | 4,99€/Monat | 500 Links, 3 Team-Mitglieder | Freelancer, Creators |
|
||||
| Team | 9,99€/Monat | Unbegrenzte Links, 10 Mitglieder | Kleine Teams |
|
||||
| Team Plus | 19,99€/Monat | Alles unbegrenzt, API, Custom Domain | Agenturen, Unternehmen |
|
||||
|
||||
---
|
||||
📈 Wachstumspotenzial & Roadmap
|
||||
Monetarisierungs-Strategie:
|
||||
|
||||
Kurzfristig (3 Monate):
|
||||
- ✅ Stripe-Integration für nahtlose Zahlungen
|
||||
- ✅ Upgrade-Prompts an strategischen Punkten
|
||||
- ✅ Feature-Gating für Premium-Funktionen
|
||||
- ✅ Team-Lizenzen als Umsatztreiber
|
||||
|
||||
- Custom Domains für Enterprise
|
||||
- Erweiterte API-Dokumentation
|
||||
- Mobile Apps (iOS/Android)
|
||||
- Webhook-Integration
|
||||
- Bulk-Import/Export
|
||||
---
|
||||
|
||||
Mittelfristig (6-12 Monate):
|
||||
🏗️ Technische Exzellenz
|
||||
|
||||
- KI-gestützte Link-Vorschläge
|
||||
- Social Media Scheduling
|
||||
- Advanced Analytics (Heatmaps, Funnels)
|
||||
- White-Label-Lösung
|
||||
- Marketplace für Templates
|
||||
Modern Tech Stack:
|
||||
|
||||
Langfristig (12+ Monate):
|
||||
- Frontend: SvelteKit 2.0 + Svelte 5 (Cutting-Edge)
|
||||
- Backend: PocketBase (Embedded, Skalierbar)
|
||||
- Styling: Tailwind CSS 4.0 (Neueste Version)
|
||||
- Deployment: Docker + Coolify auf Hetzner VPS
|
||||
- Database: SQLite via PocketBase (Performant)
|
||||
- Payments: Stripe Integration
|
||||
|
||||
- Enterprise SSO (SAML, OAuth)
|
||||
- Compliance-Features (GDPR-Tools)
|
||||
- Multi-Region Deployment
|
||||
- B2B-Partnerschaften
|
||||
- API-First Platform
|
||||
DevOps & Performance:
|
||||
|
||||
---
|
||||
🏆 Wettbewerbsvorteile
|
||||
- 🚀 Automatische Deployments via Git Push
|
||||
- 🔒 HTTPS-Enforced mit SSL
|
||||
- 📊 Health-Monitoring
|
||||
- 🎯 99.9% Uptime SLA
|
||||
- ⚡ Optimierte Build-Pipeline
|
||||
|
||||
Vs. Bitly:
|
||||
---
|
||||
|
||||
✅ Günstigerer Einstiegspreis
|
||||
✅ Profilkarten-Feature
|
||||
✅ Bessere Team-Funktionen im niedrigen Preissegment
|
||||
🎨 User Experience & Design
|
||||
|
||||
Vs. Linktree:
|
||||
Stärken:
|
||||
|
||||
✅ Vollwertiges Link-Management zusätzlich zu Profilseiten
|
||||
✅ Detailliertere Analytics
|
||||
✅ Flexibleres Pricing
|
||||
1. Intuitive Navigation: Klare Menüstruktur, konsistente UX
|
||||
2. Dark Mode Support: Vollständige Theme-Unterstützung
|
||||
3. Responsive Design: Mobile-First Approach
|
||||
4. Visual Feedback: Toast-Nachrichten, Ladeanimationen
|
||||
5. Accessibility: ARIA-Labels, Keyboard-Navigation
|
||||
|
||||
Vs. Rebrandly:
|
||||
Design-Philosophie:
|
||||
|
||||
✅ Einfachere Bedienung
|
||||
✅ Integrierte Profilkarten
|
||||
✅ Transparenteres Pricing
|
||||
- Minimalistisch aber funktional
|
||||
- Farbcodierte Statusanzeigen
|
||||
- Konsistente Icon-Verwendung (Lucide Icons)
|
||||
- Moderne Card-Based Layouts
|
||||
|
||||
---
|
||||
🎯 Marketing-Empfehlungen
|
||||
---
|
||||
|
||||
1. Content Marketing
|
||||
📈 Wachstumspotenzial & Roadmap
|
||||
|
||||
- SEO-optimierte Blogbeiträge zu Link-Management
|
||||
- Video-Tutorials für Card-Builder
|
||||
- Case Studies erfolgreicher Nutzer
|
||||
Kurzfristig (3 Monate):
|
||||
|
||||
2. Social Media Strategie
|
||||
- Custom Domains für Enterprise
|
||||
- Erweiterte API-Dokumentation
|
||||
- Mobile Apps (iOS/Android)
|
||||
- Webhook-Integration
|
||||
- Bulk-Import/Export
|
||||
|
||||
- Instagram/TikTok: Creator-fokussierte Kampagnen
|
||||
- LinkedIn: B2B-Content für Teams
|
||||
- Twitter/X: Feature-Updates, Tips & Tricks
|
||||
Mittelfristig (6-12 Monate):
|
||||
|
||||
3. Partnerschaften
|
||||
- KI-gestützte Link-Vorschläge
|
||||
- Social Media Scheduling
|
||||
- Advanced Analytics (Heatmaps, Funnels)
|
||||
- White-Label-Lösung
|
||||
- Marketplace für Templates
|
||||
|
||||
- Influencer-Kooperationen (kostenlose Pro-Accounts)
|
||||
- Agentur-Partnerprogramm
|
||||
- Integration mit populären Tools (Zapier, Make)
|
||||
Langfristig (12+ Monate):
|
||||
|
||||
4. Growth Hacking
|
||||
- Enterprise SSO (SAML, OAuth)
|
||||
- Compliance-Features (GDPR-Tools)
|
||||
- Multi-Region Deployment
|
||||
- B2B-Partnerschaften
|
||||
- API-First Platform
|
||||
|
||||
- Referral-Programm mit Belohnungen
|
||||
- Freemium-to-Pro Conversion-Optimierung
|
||||
- Limited-Time Offers für Team-Pläne
|
||||
---
|
||||
|
||||
5. Brand Positioning
|
||||
🏆 Wettbewerbsvorteile
|
||||
|
||||
- Tagline-Vorschlag: "More than links. Your digital identity."
|
||||
- Kern-Message: Professionelles Link-Management trifft kreative Selbstdarstellung
|
||||
- Differenzierung: Der einzige Link-Shortener mit integriertem Profile-Builder
|
||||
Vs. Bitly:
|
||||
|
||||
---
|
||||
💡 Kritische Erfolgsfaktoren
|
||||
✅ Günstigerer Einstiegspreis
|
||||
✅ Profilkarten-Feature
|
||||
✅ Bessere Team-Funktionen im niedrigen Preissegment
|
||||
|
||||
Stärken:
|
||||
Vs. Linktree:
|
||||
|
||||
- ✅ Technisch ausgereift und modern
|
||||
- ✅ Klares, attraktives Pricing
|
||||
- ✅ Unique Features (Cards, Templates)
|
||||
- ✅ Starke Team-Funktionalität
|
||||
- ✅ Skalierbare Architektur
|
||||
✅ Vollwertiges Link-Management zusätzlich zu Profilseiten
|
||||
✅ Detailliertere Analytics
|
||||
✅ Flexibleres Pricing
|
||||
|
||||
Verbesserungspotenziale:
|
||||
Vs. Rebrandly:
|
||||
|
||||
- ⚠️ Markenbekanntheit aufbauen
|
||||
- ⚠️ Mobile Apps fehlen noch
|
||||
- ⚠️ API-Dokumentation ausbaufähig
|
||||
- ⚠️ Enterprise-Features limitiert
|
||||
✅ Einfachere Bedienung
|
||||
✅ Integrierte Profilkarten
|
||||
✅ Transparenteres Pricing
|
||||
|
||||
---
|
||||
🎬 Fazit
|
||||
---
|
||||
|
||||
Uload ist eine beeindruckend durchdachte Plattform, die den gesättigten Markt der Link-Shortener durch
|
||||
innovative Features wie Profilkarten und starke Team-Funktionalität neu definiert. Die technische Exzellenz,
|
||||
kombiniert mit einem attraktiven Preismodell und klarer Zielgruppenansprache, positioniert Uload ideal für
|
||||
nachhaltiges Wachstum.
|
||||
🎯 Marketing-Empfehlungen
|
||||
|
||||
Kernbotschaft für Marketing:
|
||||
"Uload ist nicht nur ein Link-Shortener – es ist die All-in-One-Plattform für digitale Präsenz, die Links,
|
||||
Profile und Teams intelligent vereint."
|
||||
1. Content Marketing
|
||||
|
||||
Mit der richtigen Marketing-Strategie und kontinuierlicher Feature-Entwicklung hat Uload das Potenzial, sich
|
||||
als führende Alternative zu etablierten Playern wie Bitly und Linktree zu etablieren, besonders im
|
||||
europäischen Markt.
|
||||
- SEO-optimierte Blogbeiträge zu Link-Management
|
||||
- Video-Tutorials für Card-Builder
|
||||
- Case Studies erfolgreicher Nutzer
|
||||
|
||||
2. Social Media Strategie
|
||||
|
||||
- Instagram/TikTok: Creator-fokussierte Kampagnen
|
||||
- LinkedIn: B2B-Content für Teams
|
||||
- Twitter/X: Feature-Updates, Tips & Tricks
|
||||
|
||||
3. Partnerschaften
|
||||
|
||||
- Influencer-Kooperationen (kostenlose Pro-Accounts)
|
||||
- Agentur-Partnerprogramm
|
||||
- Integration mit populären Tools (Zapier, Make)
|
||||
|
||||
4. Growth Hacking
|
||||
|
||||
- Referral-Programm mit Belohnungen
|
||||
- Freemium-to-Pro Conversion-Optimierung
|
||||
- Limited-Time Offers für Team-Pläne
|
||||
|
||||
5. Brand Positioning
|
||||
|
||||
- Tagline-Vorschlag: "More than links. Your digital identity."
|
||||
- Kern-Message: Professionelles Link-Management trifft kreative Selbstdarstellung
|
||||
- Differenzierung: Der einzige Link-Shortener mit integriertem Profile-Builder
|
||||
|
||||
---
|
||||
|
||||
💡 Kritische Erfolgsfaktoren
|
||||
|
||||
Stärken:
|
||||
|
||||
- ✅ Technisch ausgereift und modern
|
||||
- ✅ Klares, attraktives Pricing
|
||||
- ✅ Unique Features (Cards, Templates)
|
||||
- ✅ Starke Team-Funktionalität
|
||||
- ✅ Skalierbare Architektur
|
||||
|
||||
Verbesserungspotenziale:
|
||||
|
||||
- ⚠️ Markenbekanntheit aufbauen
|
||||
- ⚠️ Mobile Apps fehlen noch
|
||||
- ⚠️ API-Dokumentation ausbaufähig
|
||||
- ⚠️ Enterprise-Features limitiert
|
||||
|
||||
---
|
||||
|
||||
🎬 Fazit
|
||||
|
||||
Uload ist eine beeindruckend durchdachte Plattform, die den gesättigten Markt der Link-Shortener durch
|
||||
innovative Features wie Profilkarten und starke Team-Funktionalität neu definiert. Die technische Exzellenz,
|
||||
kombiniert mit einem attraktiven Preismodell und klarer Zielgruppenansprache, positioniert Uload ideal für
|
||||
nachhaltiges Wachstum.
|
||||
|
||||
Kernbotschaft für Marketing:
|
||||
"Uload ist nicht nur ein Link-Shortener – es ist die All-in-One-Plattform für digitale Präsenz, die Links,
|
||||
Profile und Teams intelligent vereint."
|
||||
|
||||
Mit der richtigen Marketing-Strategie und kontinuierlicher Feature-Entwicklung hat Uload das Potenzial, sich
|
||||
als führende Alternative zu etablierten Playern wie Bitly und Linktree zu etablieren, besonders im
|
||||
europäischen Markt.
|
||||
|
||||
⏺ Update Todos
|
||||
⎿ ☒ Analyze project structure and tech stack
|
||||
☒ Examine core features and functionality
|
||||
☒ Review user interface and design
|
||||
☒ Analyze database schema and data models
|
||||
☒ Identify unique selling points
|
||||
☒ Write comprehensive marketing report
|
||||
⎿ ☒ Analyze project structure and tech stack
|
||||
☒ Examine core features and functionality
|
||||
☒ Review user interface and design
|
||||
☒ Analyze database schema and data models
|
||||
☒ Identify unique selling points
|
||||
☒ Write comprehensive marketing report
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
# Uload - Talking Head Video Scripts
|
||||
|
||||
## Video Script 1: "Der Link-Shortener, der mehr kann"
|
||||
|
||||
**Dauer: 30-45 Sekunden**
|
||||
|
||||
---
|
||||
|
|
@ -23,6 +24,7 @@
|
|||
---
|
||||
|
||||
## Video Script 2: "Warum ich von Linktree zu Uload gewechselt bin"
|
||||
|
||||
**Dauer: 40-50 Sekunden**
|
||||
|
||||
---
|
||||
|
|
@ -48,6 +50,7 @@
|
|||
---
|
||||
|
||||
## Video Script 3: "Das perfekte Tool für Creator und Teams"
|
||||
|
||||
**Dauer: 35-45 Sekunden**
|
||||
|
||||
---
|
||||
|
|
@ -73,6 +76,7 @@
|
|||
---
|
||||
|
||||
## Video Script 4: "QR-Codes, die richtig was können"
|
||||
|
||||
**Dauer: 30-40 Sekunden**
|
||||
|
||||
---
|
||||
|
|
@ -84,7 +88,7 @@
|
|||
"Uload macht aus jedem Link einen QR-Code - in Gold, mit Passwortschutz, und Ablaufdatum!"
|
||||
|
||||
**[Use Cases - 12 Sek]**
|
||||
"Event nur für VIPs? Passwortgeschützter QR-Code.
|
||||
"Event nur für VIPs? Passwortgeschützter QR-Code.
|
||||
Flash-Sale? QR-Code der nach 24 Stunden abläuft.
|
||||
Limitierte Aktion? QR-Code für nur 100 Klicks!"
|
||||
|
||||
|
|
@ -100,6 +104,7 @@ Limitierte Aktion? QR-Code für nur 100 Klicks!"
|
|||
---
|
||||
|
||||
## Video Script 5: "Von 0 auf Profi-Profil in 5 Minuten"
|
||||
|
||||
**Dauer: 35-45 Sekunden**
|
||||
|
||||
---
|
||||
|
|
@ -131,27 +136,31 @@ QR-Code generieren - AUTOMATISCH!"
|
|||
## Produktions-Tipps:
|
||||
|
||||
### Visuelle Elemente:
|
||||
|
||||
- **Screen-Recordings** der Features während du sprichst
|
||||
- **Grafische Overlays** für Preisvergleiche
|
||||
- **Animierte Zahlen** für Statistiken
|
||||
- **Split-Screens** für Vorher-Nachher
|
||||
|
||||
### Ton & Musik:
|
||||
|
||||
- **Upbeat Background Music** (leise)
|
||||
- **Sound Effects** bei Übergängen
|
||||
- **Klare, enthusiastische Stimme**
|
||||
|
||||
### Call-to-Actions:
|
||||
|
||||
- **URL einblenden**: ulo.ad
|
||||
- **QR-Code** zum Scannen
|
||||
- **"Link in Bio"** Reference
|
||||
|
||||
### Hashtag-Strategie:
|
||||
|
||||
- #LinkInBio
|
||||
- #DigitalMarketing
|
||||
- #DigitalMarketing
|
||||
- #CreatorTools
|
||||
- #TeamWork
|
||||
- #QRCode
|
||||
- #Uload
|
||||
- #LinkShortener
|
||||
- #MarketingHacks
|
||||
- #MarketingHacks
|
||||
|
|
|
|||
|
|
@ -74,4 +74,4 @@ Fertig! Eine professionelle Profilseite mit Analytics, kurzen Links und QR-Codes
|
|||
|
||||
Bei Linktree hätte ich nur Links. Bei Uload hab ich ein komplettes Digital-Marketing-Tool!
|
||||
|
||||
Bau deine in unter 5 Minuten: ulo.ad - kostenlos starten!
|
||||
Bau deine in unter 5 Minuten: ulo.ad - kostenlos starten!
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ describe('Tags Integration', () => {
|
|||
testUser = await pb.collection('users').create({
|
||||
email: `test-${Date.now()}@example.com`,
|
||||
password: 'testpassword123',
|
||||
passwordConfirm: 'testpassword123'
|
||||
passwordConfirm: 'testpassword123',
|
||||
});
|
||||
|
||||
await pb.collection('users').authWithPassword(testUser.email, 'testpassword123');
|
||||
|
|
@ -73,7 +73,7 @@ describe('Tags Integration', () => {
|
|||
slug: 'test-tag',
|
||||
user_id: testUser.id,
|
||||
color: '#3B82F6',
|
||||
is_public: false
|
||||
is_public: false,
|
||||
});
|
||||
|
||||
expect(tag.name).toBe('Test Tag');
|
||||
|
|
@ -84,14 +84,14 @@ describe('Tags Integration', () => {
|
|||
const tag = await pb.collection('tags').create({
|
||||
name: 'My Tag',
|
||||
slug: 'my-tag',
|
||||
user_id: testUser.id
|
||||
user_id: testUser.id,
|
||||
});
|
||||
|
||||
// Try to update with different user
|
||||
const otherUser = await pb.collection('users').create({
|
||||
email: `other-${Date.now()}@example.com`,
|
||||
password: 'testpassword123',
|
||||
passwordConfirm: 'testpassword123'
|
||||
passwordConfirm: 'testpassword123',
|
||||
});
|
||||
|
||||
await pb.collection('users').authWithPassword(otherUser.email, 'testpassword123');
|
||||
|
|
@ -309,7 +309,7 @@ Sentry.init({
|
|||
integrations: [Sentry.browserTracingIntegration(), Sentry.replayIntegration()],
|
||||
tracesSampleRate: 1.0,
|
||||
replaysSessionSampleRate: 0.1,
|
||||
replaysOnErrorSampleRate: 1.0
|
||||
replaysOnErrorSampleRate: 1.0,
|
||||
});
|
||||
|
||||
export const handleError = Sentry.handleErrorWithSentry();
|
||||
|
|
@ -326,7 +326,7 @@ export async function GET() {
|
|||
const checks = {
|
||||
app: 'ok',
|
||||
database: 'unknown',
|
||||
timestamp: new Date().toISOString()
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
|
||||
try {
|
||||
|
|
@ -423,7 +423,7 @@ const migrations = [
|
|||
await pb.collections.update('tags', tagsCollection);
|
||||
|
||||
console.log('✓ Fixed tags collection rules');
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
version: 2,
|
||||
|
|
@ -431,8 +431,8 @@ const migrations = [
|
|||
up: async (pb) => {
|
||||
// Add indexes for performance
|
||||
// Implementation here
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
async function runMigrations() {
|
||||
|
|
|
|||
|
|
@ -108,7 +108,7 @@ import { writable, derived } from 'svelte/store';
|
|||
export const cardStore = createCardStore({
|
||||
cards: new Map(),
|
||||
activeCard: null,
|
||||
editMode: false
|
||||
editMode: false,
|
||||
});
|
||||
```
|
||||
|
||||
|
|
@ -174,8 +174,8 @@ const CardSchema = z.object({
|
|||
constraints: z.object({
|
||||
maxModules: z.number().max(20),
|
||||
maxHTMLSize: z.number().max(100000),
|
||||
maxCSSSize: z.number().max(50000)
|
||||
})
|
||||
maxCSSSize: z.number().max(50000),
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
|
|
@ -194,7 +194,7 @@ const CardSchema = z.object({
|
|||
const cardEventBus = {
|
||||
emit: (event: string, data: any) => {},
|
||||
on: (event: string, handler: Function) => {},
|
||||
off: (event: string, handler: Function) => {}
|
||||
off: (event: string, handler: Function) => {},
|
||||
};
|
||||
```
|
||||
|
||||
|
|
@ -222,7 +222,7 @@ export const cardsStore = {
|
|||
createCard: async (config: CardConfig) => {},
|
||||
updateCard: async (id: string, updates: Partial<Card>) => {},
|
||||
deleteCard: async (id: string) => {},
|
||||
convertCard: async (id: string, targetMode: RenderMode) => {}
|
||||
convertCard: async (id: string, targetMode: RenderMode) => {},
|
||||
};
|
||||
```
|
||||
|
||||
|
|
@ -272,7 +272,7 @@ if (import.meta.env.DEV) {
|
|||
inspect: (cardId: string) => {},
|
||||
export: (cardId: string) => {},
|
||||
import: (config: any) => {},
|
||||
benchmark: () => {}
|
||||
benchmark: () => {},
|
||||
};
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -10,12 +10,14 @@ Successfully implemented cards display on user profile pages by creating a simpl
|
|||
## Changes Made
|
||||
|
||||
### 1. Server-Side Data Loading (`/src/routes/p/[username]/+page.server.ts`)
|
||||
|
||||
- Removed complex Card type import that was causing 500 errors
|
||||
- Implemented direct PocketBase query without type dependencies
|
||||
- Added safe JSON parsing for card config/metadata fields
|
||||
- Filter cards by `page="profile"` and `visibility="public"`
|
||||
|
||||
### 2. Client-Side Rendering (`/src/routes/p/[username]/+page.svelte`)
|
||||
|
||||
- Removed dependency on complex CardRenderer component
|
||||
- Created inline simplified card rendering for three modes:
|
||||
- **Beginner Mode**: Renders modules (header, content, media, links)
|
||||
|
|
@ -24,6 +26,7 @@ Successfully implemented cards display on user profile pages by creating a simpl
|
|||
- Added "Featured Cards" section above links
|
||||
|
||||
### 3. Card Management (`/src/routes/(app)/my/cards/+page.svelte`)
|
||||
|
||||
- Updated to load ALL user cards, not just profile ones
|
||||
- Existing profile toggle functionality allows users to control visibility
|
||||
- Added stats showing total cards vs cards on profile
|
||||
|
|
@ -32,12 +35,14 @@ Successfully implemented cards display on user profile pages by creating a simpl
|
|||
## Key Features
|
||||
|
||||
### Profile Page
|
||||
|
||||
- Cards appear in a responsive grid (1-3 columns)
|
||||
- Simple module-based rendering for beginner cards
|
||||
- Fallback display for advanced/expert cards
|
||||
- Only shows public cards marked for profile display
|
||||
|
||||
### Management Page
|
||||
|
||||
- Checkbox to toggle "Show on Profile"
|
||||
- Warning if card is not public but set for profile
|
||||
- Drag-and-drop reordering still functional
|
||||
|
|
@ -46,12 +51,14 @@ Successfully implemented cards display on user profile pages by creating a simpl
|
|||
## Technical Approach
|
||||
|
||||
### Why This Works
|
||||
|
||||
1. **No Type Imports**: Server-side code doesn't import complex Card types
|
||||
2. **Direct Database Access**: Uses PocketBase directly with JSON parsing
|
||||
3. **Simplified Rendering**: Inline rendering without complex components
|
||||
4. **Progressive Enhancement**: Basic display with room for improvements
|
||||
|
||||
### Trade-offs
|
||||
|
||||
- Less feature-rich display than full CardRenderer
|
||||
- Advanced/Expert cards show metadata only (not full rendering)
|
||||
- No template variable replacement
|
||||
|
|
@ -60,11 +67,13 @@ Successfully implemented cards display on user profile pages by creating a simpl
|
|||
## Future Improvements
|
||||
|
||||
### Short Term
|
||||
|
||||
1. Add client-side CardRenderer for better display
|
||||
2. Implement template variable replacement
|
||||
3. Add preview mode in card management
|
||||
|
||||
### Long Term
|
||||
|
||||
1. Refactor Card type system for SSR compatibility
|
||||
2. Create server-safe card components
|
||||
3. Implement full rendering for all card modes
|
||||
|
|
@ -80,4 +89,4 @@ Successfully implemented cards display on user profile pages by creating a simpl
|
|||
|
||||
## Conclusion
|
||||
|
||||
The simplified approach successfully displays cards on profile pages while avoiding the complex type system issues. This provides a working foundation that can be enhanced incrementally without breaking production.
|
||||
The simplified approach successfully displays cards on profile pages while avoiding the complex type system issues. This provides a working foundation that can be enhanced incrementally without breaking production.
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ Nach eingehender Analyse des aktuellen Projekts und dem erhaltenen Feedback bewe
|
|||
## Aktuelle Situation
|
||||
|
||||
### Technologie-Stack
|
||||
|
||||
- **Frontend:** SvelteKit v2.22 mit Svelte 5.0
|
||||
- **Styling:** Tailwind CSS v4.0
|
||||
- **Backend:** PocketBase (aktuell)
|
||||
|
|
@ -18,6 +19,7 @@ Nach eingehender Analyse des aktuellen Projekts und dem erhaltenen Feedback bewe
|
|||
- **Hosting:** Separate Instanzen für Dev (localhost:8090) und Prod (pb.ulo.ad)
|
||||
|
||||
### Implementierte Features
|
||||
|
||||
- URL-Shortening mit custom Short-Codes
|
||||
- User Authentication & Profile Management
|
||||
- Link-Management mit Tags
|
||||
|
|
@ -89,6 +91,7 @@ Nach eingehender Analyse des aktuellen Projekts und dem erhaltenen Feedback bewe
|
|||
## Migrations-Strategie (falls notwendig)
|
||||
|
||||
### Wann der Wechsel sinnvoll wäre:
|
||||
|
||||
- ✅ Mehr als 10.000 aktive User
|
||||
- ✅ Mehr als 1 Million Clicks/Tag
|
||||
- ✅ Komplexe Business Intelligence Anforderungen
|
||||
|
|
@ -96,6 +99,7 @@ Nach eingehender Analyse des aktuellen Projekts und dem erhaltenen Feedback bewe
|
|||
- ✅ Geografisch verteilte Deployments
|
||||
|
||||
### Empfohlener Migrations-Pfad:
|
||||
|
||||
1. **Phase 1:** Weiter mit PocketBase (JETZT)
|
||||
2. **Phase 2:** Redis-Cache für Hot-Links hinzufügen
|
||||
3. **Phase 3:** Analytics in separaten Service auslagern
|
||||
|
|
@ -104,6 +108,7 @@ Nach eingehender Analyse des aktuellen Projekts und dem erhaltenen Feedback bewe
|
|||
## Konkrete Empfehlungen
|
||||
|
||||
### Kurzfristig (0-3 Monate)
|
||||
|
||||
1. **Bei PocketBase bleiben**
|
||||
- Fokus auf Feature-Entwicklung
|
||||
- User-Feedback sammeln
|
||||
|
|
@ -115,6 +120,7 @@ Nach eingehender Analyse des aktuellen Projekts und dem erhaltenen Feedback bewe
|
|||
- Lazy-Loading für Analytics
|
||||
|
||||
### Mittelfristig (3-6 Monate)
|
||||
|
||||
1. **Hybrid-Ansatz evaluieren**
|
||||
- PocketBase für Auth & Core-Data
|
||||
- Redis für Link-Resolution Cache
|
||||
|
|
@ -126,6 +132,7 @@ Nach eingehender Analyse des aktuellen Projekts und dem erhaltenen Feedback bewe
|
|||
- Datenbasierte Entscheidungen treffen
|
||||
|
||||
### Langfristig (6+ Monate)
|
||||
|
||||
1. **Bei nachgewiesenem Bedarf**
|
||||
- Migration zu PostgreSQL planen
|
||||
- Schrittweise Migration
|
||||
|
|
@ -143,17 +150,17 @@ Die Kritik an PocketBase ist **teilweise berechtigt**, aber für die aktuelle Pr
|
|||
|
||||
## Anhang: Feature-Vergleich
|
||||
|
||||
| Feature | PocketBase | PostgreSQL + Stack | Bewertung |
|
||||
|---------|------------|-------------------|-----------|
|
||||
| Setup-Zeit | 5 Minuten | 2-3 Stunden | PocketBase ✅ |
|
||||
| Auth System | Eingebaut | Lucia/Auth.js nötig | PocketBase ✅ |
|
||||
| REST API | Automatisch | Prisma + tRPC/REST | PocketBase ✅ |
|
||||
| Realtime | WebSockets eingebaut | Separate Lösung | PocketBase ✅ |
|
||||
| File Storage | Eingebaut | S3/Cloudinary | PocketBase ✅ |
|
||||
| Admin UI | Eingebaut | Eigenbau/Forest Admin | PocketBase ✅ |
|
||||
| Complex Queries | Limitiert | Vollständig | PostgreSQL ✅ |
|
||||
| Skalierung | Vertikal | Horizontal | PostgreSQL ✅ |
|
||||
| Performance | Gut bis 100k Users | Exzellent | PostgreSQL ✅ |
|
||||
| Kosten | Niedrig | Mittel-Hoch | PocketBase ✅ |
|
||||
| Feature | PocketBase | PostgreSQL + Stack | Bewertung |
|
||||
| --------------- | -------------------- | --------------------- | ------------- |
|
||||
| Setup-Zeit | 5 Minuten | 2-3 Stunden | PocketBase ✅ |
|
||||
| Auth System | Eingebaut | Lucia/Auth.js nötig | PocketBase ✅ |
|
||||
| REST API | Automatisch | Prisma + tRPC/REST | PocketBase ✅ |
|
||||
| Realtime | WebSockets eingebaut | Separate Lösung | PocketBase ✅ |
|
||||
| File Storage | Eingebaut | S3/Cloudinary | PocketBase ✅ |
|
||||
| Admin UI | Eingebaut | Eigenbau/Forest Admin | PocketBase ✅ |
|
||||
| Complex Queries | Limitiert | Vollständig | PostgreSQL ✅ |
|
||||
| Skalierung | Vertikal | Horizontal | PostgreSQL ✅ |
|
||||
| Performance | Gut bis 100k Users | Exzellent | PostgreSQL ✅ |
|
||||
| Kosten | Niedrig | Mittel-Hoch | PocketBase ✅ |
|
||||
|
||||
**Gesamtbewertung für aktuelles Projekt:** PocketBase 7:3 PostgreSQL
|
||||
**Gesamtbewertung für aktuelles Projekt:** PocketBase 7:3 PostgreSQL
|
||||
|
|
|
|||
|
|
@ -209,7 +209,7 @@ export function getVariant(testId: string, cookies: Cookies) {
|
|||
variant = Math.random() > 0.5 ? 'A' : 'B';
|
||||
cookies.set(cookieName, variant, {
|
||||
path: '/',
|
||||
maxAge: 60 * 60 * 24 * 30 // 30 Tage
|
||||
maxAge: 60 * 60 * 24 * 30, // 30 Tage
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -219,7 +219,7 @@ export function getVariant(testId: string, cookies: Cookies) {
|
|||
// Usage in +layout.server.ts
|
||||
export async function load({ cookies }) {
|
||||
return {
|
||||
heroVariant: getVariant('homepage-hero', cookies)
|
||||
heroVariant: getVariant('homepage-hero', cookies),
|
||||
};
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -68,8 +68,8 @@ export class ABTestingService {
|
|||
variants: [
|
||||
{ id: 'control', name: 'Original', weight: 34 },
|
||||
{ id: 'value-focused', name: 'Value Proposition', weight: 33 },
|
||||
{ id: 'social-proof', name: 'Social Proof First', weight: 33 }
|
||||
]
|
||||
{ id: 'social-proof', name: 'Social Proof First', weight: 33 },
|
||||
],
|
||||
});
|
||||
|
||||
this.tests.set('homepage-cta', {
|
||||
|
|
@ -80,8 +80,8 @@ export class ABTestingService {
|
|||
{ id: 'start-free', name: 'Start Free', weight: 25 },
|
||||
{ id: 'try-now', name: 'Try Now', weight: 25 },
|
||||
{ id: 'get-started', name: 'Get Started', weight: 25 },
|
||||
{ id: 'create-link', name: 'Create Your First Link', weight: 25 }
|
||||
]
|
||||
{ id: 'create-link', name: 'Create Your First Link', weight: 25 },
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -107,7 +107,7 @@ export class ABTestingService {
|
|||
maxAge: 60 * 60 * 24 * 30,
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
sameSite: 'lax'
|
||||
sameSite: 'lax',
|
||||
});
|
||||
|
||||
return variant;
|
||||
|
|
@ -151,8 +151,8 @@ export const load: PageServerLoad = async ({ locals, cookies }) => {
|
|||
// Existing data...
|
||||
abTests: {
|
||||
hero: heroVariant,
|
||||
cta: ctaVariant
|
||||
}
|
||||
cta: ctaVariant,
|
||||
},
|
||||
};
|
||||
};
|
||||
```
|
||||
|
|
@ -173,7 +173,7 @@ export const load: PageServerLoad = async ({ locals, cookies }) => {
|
|||
const heroComponents = {
|
||||
control: HeroOriginal,
|
||||
'value-focused': HeroValue,
|
||||
'social-proof': HeroSocial
|
||||
'social-proof': HeroSocial,
|
||||
};
|
||||
|
||||
const HeroComponent = heroComponents[data.abTests?.hero?.id || 'control'];
|
||||
|
|
@ -183,7 +183,7 @@ export const load: PageServerLoad = async ({ locals, cookies }) => {
|
|||
'start-free': 'Start Free - No Credit Card',
|
||||
'try-now': 'Try Now',
|
||||
'get-started': 'Get Started Free',
|
||||
'create-link': 'Create Your First Link'
|
||||
'create-link': 'Create Your First Link',
|
||||
};
|
||||
|
||||
const ctaText = ctaTexts[data.abTests?.cta?.id || 'start-free'];
|
||||
|
|
@ -204,7 +204,7 @@ export const load: PageServerLoad = async ({ locals, cookies }) => {
|
|||
export const featureFlags = {
|
||||
newHero: import.meta.env.PUBLIC_FEATURE_NEW_HERO === 'true',
|
||||
interactiveDemo: import.meta.env.PUBLIC_FEATURE_DEMO === 'true',
|
||||
pricingCalculator: import.meta.env.PUBLIC_FEATURE_CALCULATOR === 'true'
|
||||
pricingCalculator: import.meta.env.PUBLIC_FEATURE_CALCULATOR === 'true',
|
||||
};
|
||||
|
||||
// .env.local
|
||||
|
|
@ -225,7 +225,7 @@ export const load: PageServerLoad = async ({ url, cookies }) => {
|
|||
// Override cookie for testing
|
||||
cookies.set('ab_homepage-hero', variant, {
|
||||
path: '/',
|
||||
maxAge: 60 * 60 * 24
|
||||
maxAge: 60 * 60 * 24,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -259,16 +259,16 @@ export function getTimeBasedVariant(): string {
|
|||
export const heroHeadlines = {
|
||||
control: {
|
||||
headline: 'Short Links That Work Harder',
|
||||
subheadline: 'Professional URL management with real-time analytics'
|
||||
subheadline: 'Professional URL management with real-time analytics',
|
||||
},
|
||||
benefit: {
|
||||
headline: 'Save 3 Hours Per Week on Link Management',
|
||||
subheadline: 'Automate your URL workflow with smart analytics'
|
||||
subheadline: 'Automate your URL workflow with smart analytics',
|
||||
},
|
||||
social: {
|
||||
headline: 'Join 10,000+ Marketers Using uLoad',
|
||||
subheadline: 'The trusted URL shortener for growing brands'
|
||||
}
|
||||
subheadline: 'The trusted URL shortener for growing brands',
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
|
|
@ -288,23 +288,23 @@ export const heroHeadlines = {
|
|||
'start-free': {
|
||||
text: 'Start Free - No Credit Card',
|
||||
color: 'bg-purple-600 hover:bg-purple-700',
|
||||
size: 'px-8 py-4 text-lg'
|
||||
size: 'px-8 py-4 text-lg',
|
||||
},
|
||||
'try-now': {
|
||||
text: 'Try Now →',
|
||||
color: 'bg-blue-600 hover:bg-blue-700',
|
||||
size: 'px-6 py-3 text-base'
|
||||
size: 'px-6 py-3 text-base',
|
||||
},
|
||||
'get-started': {
|
||||
text: 'Get Started Free',
|
||||
color: 'bg-gradient-to-r from-purple-600 to-blue-600',
|
||||
size: 'px-8 py-4 text-lg'
|
||||
size: 'px-8 py-4 text-lg',
|
||||
},
|
||||
'create-link': {
|
||||
text: '🔗 Create Your First Link',
|
||||
color: 'bg-black hover:bg-gray-800',
|
||||
size: 'px-6 py-4 text-lg'
|
||||
}
|
||||
size: 'px-6 py-4 text-lg',
|
||||
},
|
||||
};
|
||||
|
||||
const config = configs[variant];
|
||||
|
|
@ -363,7 +363,7 @@ export function trackABEvent(
|
|||
window.gtag('event', 'ab_test', {
|
||||
test_id: testId,
|
||||
variant_id: variantId,
|
||||
action: action
|
||||
action: action,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -375,8 +375,8 @@ export function trackABEvent(
|
|||
testId,
|
||||
variantId,
|
||||
action,
|
||||
timestamp: new Date().toISOString()
|
||||
})
|
||||
timestamp: new Date().toISOString(),
|
||||
}),
|
||||
});
|
||||
}
|
||||
```
|
||||
|
|
@ -520,7 +520,7 @@ export const load: PageServerLoad = async ({ cookies, locals }) => {
|
|||
heroVariant = Math.random() > 0.5 ? 'a' : 'b';
|
||||
cookies.set('ab_hero', heroVariant, {
|
||||
path: '/',
|
||||
maxAge: 60 * 60 * 24 * 30
|
||||
maxAge: 60 * 60 * 24 * 30,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -528,7 +528,7 @@ export const load: PageServerLoad = async ({ cookies, locals }) => {
|
|||
|
||||
return {
|
||||
// Existing data...
|
||||
heroVariant
|
||||
heroVariant,
|
||||
};
|
||||
};
|
||||
```
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ async function getLocationFromIP(ipAddress) {
|
|||
const data = await response.json();
|
||||
return {
|
||||
country: data.country_name || 'Unknown',
|
||||
city: data.city || 'Unknown'
|
||||
city: data.city || 'Unknown',
|
||||
};
|
||||
} catch (error) {
|
||||
return { country: 'Unknown', city: 'Unknown' };
|
||||
|
|
|
|||
|
|
@ -139,7 +139,7 @@ migrate(
|
|||
{ name: 'folders', oldField: 'user', newField: 'user_id' },
|
||||
{ name: 'folders', oldField: 'public', newField: 'is_public' },
|
||||
{ name: 'tags', oldField: 'user', newField: 'user_id' },
|
||||
{ name: 'tags', oldField: 'public', newField: 'is_public' }
|
||||
{ name: 'tags', oldField: 'public', newField: 'is_public' },
|
||||
];
|
||||
|
||||
collections.forEach(({ name, oldField, newField }) => {
|
||||
|
|
@ -169,7 +169,7 @@ migrate(
|
|||
{ name: 'links', fields: ['user_id', 'is_active', 'folder_id'] },
|
||||
{ name: 'analytics', fields: ['link_id', 'ip_address'] },
|
||||
{ name: 'folders', fields: ['user_id', 'is_public'] },
|
||||
{ name: 'tags', fields: ['user_id', 'is_public'] }
|
||||
{ name: 'tags', fields: ['user_id', 'is_public'] },
|
||||
];
|
||||
|
||||
collections.forEach(({ name, fields }) => {
|
||||
|
|
@ -206,7 +206,7 @@ export class DBCompatibility {
|
|||
folder: 'folder_id',
|
||||
active: 'is_active',
|
||||
public: 'is_public',
|
||||
ip: 'ip_address'
|
||||
ip: 'ip_address',
|
||||
};
|
||||
|
||||
return mapping[oldName] || oldName;
|
||||
|
|
@ -236,7 +236,7 @@ export class DBCompatibility {
|
|||
folder_id: 'folder',
|
||||
is_active: 'active',
|
||||
is_public: 'public',
|
||||
ip_address: 'ip'
|
||||
ip_address: 'ip',
|
||||
};
|
||||
|
||||
const transformed = { ...data };
|
||||
|
|
@ -346,7 +346,7 @@ migrate((db) => {
|
|||
const updates = [
|
||||
'UPDATE links SET user = user_id WHERE user_id IS NOT NULL',
|
||||
'UPDATE links SET active = is_active WHERE is_active IS NOT NULL',
|
||||
'UPDATE analytics SET link = link_id WHERE link_id IS NOT NULL'
|
||||
'UPDATE analytics SET link = link_id WHERE link_id IS NOT NULL',
|
||||
// ... weitere Updates
|
||||
];
|
||||
|
||||
|
|
@ -390,7 +390,7 @@ export async function validateMigration() {
|
|||
linksIntegrity: await checkLinksIntegrity(),
|
||||
analyticsConsistency: await checkAnalyticsConsistency(),
|
||||
relationsValid: await checkRelationsValid(),
|
||||
performanceMetrics: await checkPerformance()
|
||||
performanceMetrics: await checkPerformance(),
|
||||
};
|
||||
|
||||
return checks;
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ PocketBase ist bereits gestartet auf: http://localhost:8090
|
|||
|
||||
### 1. Admin Account erstellen
|
||||
|
||||
Öffne im Browser: http://localhost:8090/_/
|
||||
Öffne im Browser: http://localhost:8090/\_/
|
||||
|
||||
- Beim ersten Besuch wirst du aufgefordert einen Admin-Account zu erstellen
|
||||
- Verwende sichere Credentials (nur für lokale Entwicklung)
|
||||
|
|
@ -27,10 +27,12 @@ Nach dem Login als Admin:
|
|||
Erstelle ein paar Test-Einträge:
|
||||
|
||||
**Test User:**
|
||||
|
||||
- Email: test@localhost
|
||||
- Password: test123456
|
||||
|
||||
**Test Links:**
|
||||
|
||||
- Kurz-Code: test1
|
||||
- Original URL: https://example.com
|
||||
- Aktiv: Ja
|
||||
|
|
@ -54,4 +56,4 @@ Die App sollte jetzt http://localhost:8090 als Backend nutzen!
|
|||
|
||||
## 🎉 Fertig!
|
||||
|
||||
Deine lokale Entwicklungsumgebung ist jetzt vollständig von Production getrennt!
|
||||
Deine lokale Entwicklungsumgebung ist jetzt vollständig von Production getrennt!
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import { PUBLIC_APP_URL } from '$env/static/public';
|
|||
import type { RequestHandler } from './$types';
|
||||
|
||||
const stripe = new Stripe(STRIPE_SECRET_KEY, {
|
||||
apiVersion: '2024-11-20'
|
||||
apiVersion: '2024-11-20',
|
||||
});
|
||||
|
||||
export const POST: RequestHandler = async ({ request, locals }) => {
|
||||
|
|
@ -40,15 +40,15 @@ export const POST: RequestHandler = async ({ request, locals }) => {
|
|||
name: user.name || undefined,
|
||||
metadata: {
|
||||
pocketbase_id: user.id,
|
||||
username: user.username || ''
|
||||
}
|
||||
username: user.username || '',
|
||||
},
|
||||
});
|
||||
|
||||
stripeCustomerId = customer.id;
|
||||
|
||||
// Save customer ID for future use
|
||||
await locals.pb.collection('users').update(user.id, {
|
||||
stripe_customer_id: stripeCustomerId
|
||||
stripe_customer_id: stripeCustomerId,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -63,34 +63,34 @@ export const POST: RequestHandler = async ({ request, locals }) => {
|
|||
line_items: [
|
||||
{
|
||||
price: priceId,
|
||||
quantity: 1
|
||||
}
|
||||
quantity: 1,
|
||||
},
|
||||
],
|
||||
mode: 'subscription',
|
||||
allow_promotion_codes: true,
|
||||
subscription_data: {
|
||||
metadata: {
|
||||
pocketbase_user_id: user.id
|
||||
}
|
||||
pocketbase_user_id: user.id,
|
||||
},
|
||||
},
|
||||
success_url: `${PUBLIC_APP_URL}/checkout/success?session_id={CHECKOUT_SESSION_ID}`,
|
||||
cancel_url: `${PUBLIC_APP_URL}/pricing?cancelled=true`,
|
||||
locale: 'de',
|
||||
metadata: {
|
||||
user_id: user.id,
|
||||
user_email: user.email
|
||||
}
|
||||
user_email: user.email,
|
||||
},
|
||||
});
|
||||
|
||||
return json({
|
||||
sessionId: session.id,
|
||||
url: session.url
|
||||
url: session.url,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Stripe checkout error:', error);
|
||||
return json(
|
||||
{
|
||||
error: 'Fehler beim Erstellen der Checkout-Session'
|
||||
error: 'Fehler beim Erstellen der Checkout-Session',
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
|
|
@ -107,7 +107,7 @@ import { STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET } from '$env/static/private';
|
|||
import type { RequestHandler } from './$types';
|
||||
|
||||
const stripe = new Stripe(STRIPE_SECRET_KEY, {
|
||||
apiVersion: '2024-11-20'
|
||||
apiVersion: '2024-11-20',
|
||||
});
|
||||
|
||||
export const POST: RequestHandler = async ({ request, locals }) => {
|
||||
|
|
@ -149,7 +149,7 @@ export const POST: RequestHandler = async ({ request, locals }) => {
|
|||
stripe_customer_id: session.customer,
|
||||
stripe_subscription_id: subscription.id,
|
||||
current_period_end: new Date(subscription.current_period_end * 1000).toISOString(),
|
||||
subscription_interval: subscription.items.data[0].price.recurring?.interval || 'month'
|
||||
subscription_interval: subscription.items.data[0].price.recurring?.interval || 'month',
|
||||
});
|
||||
|
||||
// Reset usage counter for new subscribers
|
||||
|
|
@ -159,7 +159,7 @@ export const POST: RequestHandler = async ({ request, locals }) => {
|
|||
new Date().getFullYear(),
|
||||
new Date().getMonth() + 1,
|
||||
1
|
||||
).toISOString()
|
||||
).toISOString(),
|
||||
});
|
||||
|
||||
console.log(`User ${userId} upgraded to Pro`);
|
||||
|
|
@ -199,7 +199,7 @@ export const POST: RequestHandler = async ({ request, locals }) => {
|
|||
// Update user subscription status
|
||||
await locals.pb.collection('users').update(userId, {
|
||||
subscription_status: status,
|
||||
current_period_end: new Date(subscription.current_period_end * 1000).toISOString()
|
||||
current_period_end: new Date(subscription.current_period_end * 1000).toISOString(),
|
||||
});
|
||||
|
||||
console.log(`User ${userId} subscription status: ${status}`);
|
||||
|
|
@ -224,7 +224,7 @@ export const POST: RequestHandler = async ({ request, locals }) => {
|
|||
await locals.pb.collection('users').update(userId, {
|
||||
subscription_status: 'free',
|
||||
stripe_subscription_id: null,
|
||||
current_period_end: null
|
||||
current_period_end: null,
|
||||
});
|
||||
|
||||
console.log(`User ${userId} downgraded to Free`);
|
||||
|
|
@ -244,7 +244,7 @@ export const POST: RequestHandler = async ({ request, locals }) => {
|
|||
|
||||
// Mark as past_due
|
||||
await locals.pb.collection('users').update(userId, {
|
||||
subscription_status: 'past_due'
|
||||
subscription_status: 'past_due',
|
||||
});
|
||||
|
||||
// TODO: Send email notification to user
|
||||
|
|
@ -267,7 +267,7 @@ export const POST: RequestHandler = async ({ request, locals }) => {
|
|||
const user = await locals.pb.collection('users').getOne(userId);
|
||||
if (user.subscription_status === 'past_due') {
|
||||
await locals.pb.collection('users').update(userId, {
|
||||
subscription_status: 'pro'
|
||||
subscription_status: 'pro',
|
||||
});
|
||||
console.log(`User ${userId} reactivated after payment`);
|
||||
}
|
||||
|
|
@ -297,7 +297,7 @@ import { PUBLIC_APP_URL } from '$env/static/public';
|
|||
import type { RequestHandler } from './$types';
|
||||
|
||||
const stripe = new Stripe(STRIPE_SECRET_KEY, {
|
||||
apiVersion: '2024-11-20'
|
||||
apiVersion: '2024-11-20',
|
||||
});
|
||||
|
||||
export const POST: RequestHandler = async ({ locals }) => {
|
||||
|
|
@ -316,7 +316,7 @@ export const POST: RequestHandler = async ({ locals }) => {
|
|||
const session = await stripe.billingPortal.sessions.create({
|
||||
customer: user.stripe_customer_id,
|
||||
return_url: `${PUBLIC_APP_URL}/account`,
|
||||
locale: 'de'
|
||||
locale: 'de',
|
||||
});
|
||||
|
||||
return json({ url: session.url });
|
||||
|
|
@ -348,7 +348,7 @@ export class SubscriptionService {
|
|||
if (user.subscription_status === 'past_due') {
|
||||
return {
|
||||
allowed: false,
|
||||
reason: 'Bitte aktualisiere deine Zahlungsmethode'
|
||||
reason: 'Bitte aktualisiere deine Zahlungsmethode',
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -361,7 +361,7 @@ export class SubscriptionService {
|
|||
if (linksUsed >= 10) {
|
||||
return {
|
||||
allowed: false,
|
||||
reason: `Du hast bereits ${linksUsed} von 10 kostenlosen Links diesen Monat erstellt`
|
||||
reason: `Du hast bereits ${linksUsed} von 10 kostenlosen Links diesen Monat erstellt`,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -377,14 +377,14 @@ export class SubscriptionService {
|
|||
const currentCount = user.links_created_this_month || 0;
|
||||
|
||||
await this.pb.collection('users').update(userId, {
|
||||
links_created_this_month: currentCount + 1
|
||||
links_created_this_month: currentCount + 1,
|
||||
});
|
||||
|
||||
// Log usage
|
||||
await this.pb.collection('usage_logs').create({
|
||||
user: userId,
|
||||
action: 'link_created',
|
||||
timestamp: new Date().toISOString()
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -398,7 +398,7 @@ export class SubscriptionService {
|
|||
|
||||
await this.pb.collection('users').update(userId, {
|
||||
links_created_this_month: 0,
|
||||
monthly_reset_date: nextReset.toISOString()
|
||||
monthly_reset_date: nextReset.toISOString(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -416,7 +416,7 @@ export class SubscriptionService {
|
|||
used: 0,
|
||||
limit: 0,
|
||||
unlimited: true,
|
||||
daysUntilReset: 0
|
||||
daysUntilReset: 0,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -431,7 +431,7 @@ export class SubscriptionService {
|
|||
used: updatedUser.links_created_this_month || 0,
|
||||
limit: 10,
|
||||
unlimited: false,
|
||||
daysUntilReset
|
||||
daysUntilReset,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -463,7 +463,7 @@ export class SubscriptionService {
|
|||
const response = await fetch('/api/stripe/checkout', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ interval })
|
||||
body: JSON.stringify({ interval }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
|
|
@ -556,11 +556,11 @@ export const handle: Handle = async ({ event, resolve }) => {
|
|||
return new Response(
|
||||
JSON.stringify({
|
||||
error: reason,
|
||||
requiresUpgrade: true
|
||||
requiresUpgrade: true,
|
||||
}),
|
||||
{
|
||||
status: 403,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
@ -587,7 +587,7 @@ export const handle: Handle = async ({ event, resolve }) => {
|
|||
|
||||
try {
|
||||
const response = await fetch('/api/stripe/portal', {
|
||||
method: 'POST'
|
||||
method: 'POST',
|
||||
});
|
||||
|
||||
const { url } = await response.json();
|
||||
|
|
@ -665,7 +665,7 @@ export async function createTestSubscription(pb: PocketBase, userId: string) {
|
|||
subscription_status: 'pro',
|
||||
stripe_customer_id: 'cus_test_' + Date.now(),
|
||||
stripe_subscription_id: 'sub_test_' + Date.now(),
|
||||
current_period_end: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString()
|
||||
current_period_end: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -674,12 +674,12 @@ export async function simulateWebhook(eventType: string, data: any) {
|
|||
method: 'POST',
|
||||
headers: {
|
||||
'stripe-signature': 'test_signature',
|
||||
'Content-Type': 'application/json'
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
type: eventType,
|
||||
data: { object: data }
|
||||
})
|
||||
data: { object: data },
|
||||
}),
|
||||
});
|
||||
|
||||
return response;
|
||||
|
|
@ -705,41 +705,41 @@ async function addStripeFields() {
|
|||
name: 'subscription_status',
|
||||
type: 'select',
|
||||
options: {
|
||||
values: ['free', 'pro', 'cancelled', 'past_due']
|
||||
values: ['free', 'pro', 'cancelled', 'past_due'],
|
||||
},
|
||||
required: true
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'stripe_customer_id',
|
||||
type: 'text',
|
||||
required: false
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'stripe_subscription_id',
|
||||
type: 'text',
|
||||
required: false
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'current_period_end',
|
||||
type: 'date',
|
||||
required: false
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'links_created_this_month',
|
||||
type: 'number',
|
||||
min: 0,
|
||||
required: true
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'monthly_reset_date',
|
||||
type: 'date',
|
||||
required: false
|
||||
}
|
||||
required: false,
|
||||
},
|
||||
];
|
||||
|
||||
// Update collection
|
||||
await pb.collections.update('users', {
|
||||
schema: updatedSchema
|
||||
schema: updatedSchema,
|
||||
});
|
||||
|
||||
console.log('✅ Migration completed');
|
||||
|
|
|
|||
|
|
@ -42,9 +42,7 @@ Prozess:
|
|||
(pocketbase_id, username)
|
||||
4. Erstellt Checkout
|
||||
Session mit:
|
||||
|
||||
|
||||
- Lokalisation
|
||||
- Lokalisation
|
||||
|
||||
(DE/EN/IT/FR/ES) - Zahlungsmethoden
|
||||
(Karte + SEPA) - Promotion Codes
|
||||
|
|
|
|||
|
|
@ -295,7 +295,7 @@ trackLinkClick({
|
|||
shortCode: 'abc123',
|
||||
username: 'john',
|
||||
hasPassword: false,
|
||||
isExpiring: true
|
||||
isExpiring: true,
|
||||
});
|
||||
|
||||
// Track link creation
|
||||
|
|
@ -304,7 +304,7 @@ trackLinkCreated({
|
|||
hasPassword: true,
|
||||
hasExpiry: false,
|
||||
hasClickLimit: true,
|
||||
isCustomCode: false
|
||||
isCustomCode: false,
|
||||
});
|
||||
|
||||
// Track authentication
|
||||
|
|
@ -316,7 +316,7 @@ trackAuth('logout');
|
|||
trackError({
|
||||
type: 'validation',
|
||||
message: 'Invalid URL format',
|
||||
code: 'INVALID_URL'
|
||||
code: 'INVALID_URL',
|
||||
});
|
||||
```
|
||||
|
||||
|
|
@ -351,7 +351,7 @@ Example:
|
|||
// In analytics.ts
|
||||
export const EVENTS = {
|
||||
// ... existing events
|
||||
NEW_EVENT_NAME: 'new-event-name'
|
||||
NEW_EVENT_NAME: 'new-event-name',
|
||||
};
|
||||
|
||||
// In your component
|
||||
|
|
@ -359,6 +359,6 @@ import { trackEvent, EVENTS } from '$lib/analytics';
|
|||
|
||||
trackEvent(EVENTS.NEW_EVENT_NAME, {
|
||||
property1: 'value1',
|
||||
property2: 'value2'
|
||||
property2: 'value2',
|
||||
});
|
||||
```
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue