From de5ac8022b62048b6208b43b028df0aa0ab250fb Mon Sep 17 00:00:00 2001 From: Alexander Schmidt Date: Mon, 9 Feb 2026 16:45:43 +0100 Subject: [PATCH] feat: add i18n meta tags (title, description) in all 7 languages with dynamic updates on locale change --- docs/LAUNCH-PLAN.md | 254 ++++++++++++++++++++++++++++++++++++++++++++ index.html | 12 +-- js/i18n.js | 18 ++++ locales/de.json | 4 + locales/en.json | 4 + locales/es.json | 4 + locales/fr.json | 4 + locales/it.json | 4 + locales/pt.json | 4 + locales/ru.json | 4 + 10 files changed, 306 insertions(+), 6 deletions(-) create mode 100644 docs/LAUNCH-PLAN.md diff --git a/docs/LAUNCH-PLAN.md b/docs/LAUNCH-PLAN.md new file mode 100644 index 0000000..eb5594c --- /dev/null +++ b/docs/LAUNCH-PLAN.md @@ -0,0 +1,254 @@ +# Launch Plan – dgray.io + +## Release-Phasen + +| Phase | Preis pro Anzeige | Dauer | Zugang | +|-------|-------------------|-------|--------| +| **Closed Alpha** | 0,01 € / 0,01 USD / 0,01 CHF | 4–6 Wochen | Nur mit Invite-Code | +| **Open Beta** | 0,10 € / 0,10 USD / 0,10 CHF | 3 Monate | Öffentlich, Beta-Banner | +| **Launch** | 1 € / 1 USD / 1 CHF | Dauerhaft | Öffentlich | + +**Pricing-Philosophie:** Die Zahl **1** als magische Zahl — einfach, einprägsam, international gleich. +`1 listing = 1 month = 1` + +--- + +## Phase 1: Closed Alpha + +### Ziel +- Feedback von Crypto/Privacy-Enthusiasten +- Payment-Flow testen (1 Cent = echte Transaktion, minimale Kosten) +- Bugs finden, UX validieren + +### Invite-Code System + +#### Directus: Collection `invite_codes` + +| Feld | Typ | Hinweise | +|------|-----|----------| +| `id` | UUID (auto) | Primary Key | +| `code` | String, unique | z.B. `ALPHA-XMR-2026` | +| `max_uses` | Integer | Max. Einlösungen (0 = unbegrenzt) | +| `used_count` | Integer, default 0 | Aktuelle Einlösungen | +| `expires_at` | DateTime, nullable | Optional: Ablaufdatum | +| `status` | String, default `active` | `active` / `disabled` | +| `date_created` | Timestamp (auto) | | + +#### Directus: Permissions für `invite_codes` + +| Rolle | Read | Create | Update | +|-------|------|--------|--------| +| Public | Nein | Nein | Nein | +| Admin | Ja | Ja | Ja | + +Die Validierung passiert **serverseitig** im PoW-Server (PHP), nicht im Frontend. + +#### PHP: Invite-Code Validierung + +Neuer Endpoint: `POST /invite/validate` + +```php +// pow.dgray.io/invite/validate.php + 'Method not allowed']); + exit; +} + +$input = json_decode(file_get_contents('php://input'), true); +$code = trim($input['code'] ?? ''); + +if (!$code) { + http_response_code(400); + echo json_encode(['valid' => false, 'error' => 'Missing invite code']); + exit; +} + +// Query Directus for invite code +$url = DIRECTUS_URL . '/items/invite_codes?filter[code][_eq]=' . urlencode($code) + . '&filter[status][_eq]=active&limit=1'; + +$context = stream_context_create([ + 'http' => [ + 'header' => "Authorization: Bearer " . DIRECTUS_TOKEN . "\r\n", + 'ignore_errors' => true, + ], +]); + +$response = file_get_contents($url, false, $context); +$data = json_decode($response, true); +$invite = $data['data'][0] ?? null; + +if (!$invite) { + echo json_encode(['valid' => false, 'error' => 'Invalid or expired invite code']); + exit; +} + +// Check max uses +if ($invite['max_uses'] > 0 && $invite['used_count'] >= $invite['max_uses']) { + echo json_encode(['valid' => false, 'error' => 'Invite code fully redeemed']); + exit; +} + +// Check expiry +if ($invite['expires_at'] && strtotime($invite['expires_at']) < time()) { + echo json_encode(['valid' => false, 'error' => 'Invite code expired']); + exit; +} + +// Increment used_count +$updateUrl = DIRECTUS_URL . '/items/invite_codes/' . $invite['id']; +$updateContext = stream_context_create([ + 'http' => [ + 'method' => 'PATCH', + 'header' => "Content-Type: application/json\r\nAuthorization: Bearer " . DIRECTUS_TOKEN . "\r\n", + 'content' => json_encode(['used_count' => $invite['used_count'] + 1]), + 'ignore_errors' => true, + ], +]); +file_get_contents($updateUrl, false, $updateContext); + +echo json_encode(['valid' => true]); +``` + +#### Frontend: Invite-Code bei Registrierung + +In `js/components/auth-modal.js` — im Registrierungs-Flow ein Textfeld hinzufügen: + +``` +[Invite Code: _________ ] ← nur in Alpha-Phase sichtbar +[UUID generieren] +``` + +Vor `createAccount()` den Code serverseitig validieren: + +```js +const res = await fetch('https://pow.dgray.io/invite/validate', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ code: inviteCode }) +}) +const { valid, error } = await res.json() +if (!valid) { /* Fehlermeldung anzeigen */ return } +``` + +#### Feature-Flag + +In `config.php` ein Flag, um Alpha-Modus zu steuern: + +```php +define('REQUIRE_INVITE_CODE', true); // false für Open Beta / Launch +``` + +Im Invoice-Endpoint kann optional der Invite-Code mitgesendet werden, +um Alpha-User zu tracken. + +### Pricing-Änderung für Alpha + +In `docs/pow-server/config.php`: + +```php +// Alpha: 1 Cent +define('LISTING_FEE', ['EUR' => 0.01, 'USD' => 0.01, 'CHF' => 0.01, 'GBP' => 0.01, 'JPY' => 2]); +``` + +### Verteilung der Invite-Codes + +- Manuell in Directus erstellen (Admin-Panel) +- Verteilen über: + - r/Monero, r/privacy + - Monero Matrix/IRC Channels + - Persönliche Kontakte +- **10–20 Codes** mit je 5–10 max_uses → 50–200 Alpha-Tester + +--- + +## Phase 2: Open Beta + +### Änderungen + +1. **Pricing**: `config.php` → `LISTING_FEE` auf 0,10 setzen +2. **Invite-Code**: `REQUIRE_INVITE_CODE` → `false` +3. **Beta-Banner**: Hinweis auf der Seite ("Beta — Feedback willkommen") +4. **Feedback-Kanal**: Link zu Matrix-Raum oder einfaches Kontaktformular + +### Marketing + +- **Show HN** Post auf Hacker News +- r/Monero, r/privacy, r/degoogle — organische Posts +- Privacy-Newsletter (PrivacyGuides, Techlore) +- Monero-Community (Matrix, IRC) + +### Metriken tracken + +- Anzahl Registrierungen +- Anzahl veröffentlichter Anzeigen +- Payment Completion Rate +- Chat-Nutzung (nur Anzahl, nicht Inhalt) + +--- + +## Phase 3: Launch + +### Änderungen + +1. **Pricing**: `config.php` → `LISTING_FEE` auf 1 setzen (bereits Standard) +2. **Beta-Banner** entfernen +3. **Invite-Code-System** kann aktiv bleiben (für spätere Promo-Codes nutzbar) + +### Kommunikation + +**"1 listing = 1 month = 1"** — so simpel wie möglich. + +--- + +## Meta-Tags (pro Sprache) + +Die statischen Meta-Tags in `index.html` sind deutsch (Fallback). +Der OG-Proxy (`pow.dgray.io/og-proxy.php`) liefert Listing-spezifische Tags. + +### Umgesetzte Texte + +**Title:** `dgray.io – [Sprache]` + +| Sprache | Title | Description | +|---------|-------|-------------| +| **de** | dgray.io – Anonyme Kleinanzeigen | Kaufen und verkaufen ohne Konto, ohne E-Mail. Bezahlung mit Monero. Ende-zu-Ende verschlüsselter Chat. | +| **en** | dgray.io – Private Classifieds | Buy and sell without an account, without email. Pay with Monero. End-to-end encrypted chat. | +| **fr** | dgray.io – Petites annonces anonymes | Achetez et vendez sans compte, sans e-mail. Paiement en Monero. Chat chiffré de bout en bout. | +| **it** | dgray.io – Annunci anonimi | Compra e vendi senza account, senza email. Pagamento in Monero. Chat crittografata end-to-end. | +| **es** | dgray.io – Clasificados anónimos | Compra y vende sin cuenta, sin email. Pago con Monero. Chat cifrado de extremo a extremo. | +| **pt** | dgray.io – Classificados anônimos | Compre e venda sem conta, sem email. Pagamento com Monero. Chat criptografado ponta a ponta. | +| **ru** | dgray.io – Анонимные объявления | Покупайте и продавайте без аккаунта, без email. Оплата Monero. Сквозное шифрование чата. | + +### Umsetzung + +Die `index.html` enthält die Standard-Meta-Tags (de). +Für sprachspezifische OG-Tags bei Social-Media-Shares: +Der OG-Proxy kann um einen `?lang=` Parameter erweitert werden. + +Die `i18n.js` `updateDOM()` Methode aktualisiert `document.title`, `og:title/description` +und `twitter:title/description` dynamisch bei jedem Sprachwechsel (i18n-Keys `meta.title`, `meta.description`). + +--- + +## Checkliste vor Alpha-Start + +- [ ] Directus: Collection `invite_codes` anlegen (Schema siehe oben) +- [ ] PHP: `invite/validate.php` deployen auf `pow.dgray.io` +- [ ] PHP: `config.php` → `LISTING_FEE` auf 0.01 setzen +- [ ] PHP: `config.php` → `REQUIRE_INVITE_CODE = true` +- [ ] Frontend: Invite-Code-Feld in `auth-modal.js` einbauen +- [x] Frontend: Meta-Description i18n-Keys in alle 7 Sprachen +- [ ] 10–20 Invite-Codes in Directus erstellen +- [ ] Codes verteilen (r/Monero, Matrix, persönlich) +- [ ] Feedback-Kanal einrichten (Matrix-Raum) diff --git a/index.html b/index.html index d923fa6..f281943 100644 --- a/index.html +++ b/index.html @@ -3,16 +3,16 @@ - + - dgray.io – Anonymous Classifieds with Monero + dgray.io – Anonyme Kleinanzeigen - - + + @@ -23,8 +23,8 @@ - - + + diff --git a/js/i18n.js b/js/i18n.js index 4800ed4..aa8e719 100644 --- a/js/i18n.js +++ b/js/i18n.js @@ -183,6 +183,24 @@ class I18n { const key = el.getAttribute('data-i18n-aria') el.setAttribute('aria-label', this.t(key)) }) + + const metaTitle = this.t('meta.title') + if (metaTitle !== 'meta.title') { + document.title = metaTitle + const ogTitle = document.querySelector('meta[property="og:title"]') + if (ogTitle) ogTitle.setAttribute('content', metaTitle) + const twTitle = document.querySelector('meta[name="twitter:title"]') + if (twTitle) twTitle.setAttribute('content', metaTitle) + } + const metaDesc = this.t('meta.description') + if (metaDesc !== 'meta.description') { + const desc = document.querySelector('meta[name="description"]') + if (desc) desc.setAttribute('content', metaDesc) + const ogDesc = document.querySelector('meta[property="og:description"]') + if (ogDesc) ogDesc.setAttribute('content', metaDesc) + const twDesc = document.querySelector('meta[name="twitter:description"]') + if (twDesc) twDesc.setAttribute('content', metaDesc) + } } /** diff --git a/locales/de.json b/locales/de.json index 61960e7..7096216 100644 --- a/locales/de.json +++ b/locales/de.json @@ -1,4 +1,8 @@ { + "meta": { + "title": "dgray.io – Anonyme Kleinanzeigen", + "description": "Kaufen und verkaufen ohne Konto, ohne E-Mail. Bezahlung mit Monero. Ende-zu-Ende verschlüsselter Chat." + }, "header": { "searchPlaceholder": "Was suchst du?", "createListing": "Anzeige erstellen", diff --git a/locales/en.json b/locales/en.json index 43d1d64..ee5c7a5 100644 --- a/locales/en.json +++ b/locales/en.json @@ -1,4 +1,8 @@ { + "meta": { + "title": "dgray.io – Private Classifieds", + "description": "Buy and sell without an account, without email. Pay with Monero. End-to-end encrypted chat." + }, "header": { "searchPlaceholder": "What are you looking for?", "createListing": "Create Listing", diff --git a/locales/es.json b/locales/es.json index eb6e946..97c621e 100644 --- a/locales/es.json +++ b/locales/es.json @@ -1,4 +1,8 @@ { + "meta": { + "title": "dgray.io – Clasificados anónimos", + "description": "Compra y vende sin cuenta, sin email. Pago con Monero. Chat cifrado de extremo a extremo." + }, "header": { "searchPlaceholder": "¿Qué estás buscando?", "createListing": "Crear anuncio", diff --git a/locales/fr.json b/locales/fr.json index d4fabf2..c05cf8d 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -1,4 +1,8 @@ { + "meta": { + "title": "dgray.io – Petites annonces anonymes", + "description": "Achetez et vendez sans compte, sans e-mail. Paiement en Monero. Chat chiffré de bout en bout." + }, "header": { "searchPlaceholder": "Que cherchez-vous ?", "createListing": "Créer une annonce", diff --git a/locales/it.json b/locales/it.json index 71c2201..28173d1 100644 --- a/locales/it.json +++ b/locales/it.json @@ -1,4 +1,8 @@ { + "meta": { + "title": "dgray.io – Annunci anonimi", + "description": "Compra e vendi senza account, senza email. Pagamento in Monero. Chat crittografata end-to-end." + }, "header": { "searchPlaceholder": "Cosa stai cercando?", "createListing": "Crea annuncio", diff --git a/locales/pt.json b/locales/pt.json index 93e1eab..e9567f7 100644 --- a/locales/pt.json +++ b/locales/pt.json @@ -1,4 +1,8 @@ { + "meta": { + "title": "dgray.io – Classificados anônimos", + "description": "Compre e venda sem conta, sem email. Pagamento com Monero. Chat criptografado ponta a ponta." + }, "header": { "searchPlaceholder": "O que você está procurando?", "createListing": "Criar Anúncio", diff --git a/locales/ru.json b/locales/ru.json index 9e12039..6d4b3a8 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -1,4 +1,8 @@ { + "meta": { + "title": "dgray.io – Анонимные объявления", + "description": "Покупайте и продавайте без аккаунта, без email. Оплата Monero. Сквозное шифрование чата." + }, "header": { "searchPlaceholder": "Что вы ищете?", "createListing": "Создать объявление",