feat: add i18n meta tags (title, description) in all 7 languages with dynamic updates on locale change
This commit is contained in:
254
docs/LAUNCH-PLAN.md
Normal file
254
docs/LAUNCH-PLAN.md
Normal file
@@ -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
|
||||||
|
<?php
|
||||||
|
require __DIR__ . '/config.php';
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
header('Access-Control-Allow-Origin: https://dgray.io');
|
||||||
|
header('Access-Control-Allow-Methods: POST, OPTIONS');
|
||||||
|
header('Access-Control-Allow-Headers: Content-Type');
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { exit; }
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
http_response_code(405);
|
||||||
|
echo json_encode(['error' => '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)
|
||||||
12
index.html
12
index.html
@@ -3,16 +3,16 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<meta name="description" content="Anonymer Marktplatz mit Monero-Bezahlung. Keine persönlichen Daten, E2E-verschlüsselter Chat.">
|
<meta name="description" content="Kaufen und verkaufen ohne Konto, ohne E-Mail. Bezahlung mit Monero. Ende-zu-Ende verschlüsselter Chat.">
|
||||||
<meta name="theme-color" content="#555555">
|
<meta name="theme-color" content="#555555">
|
||||||
|
|
||||||
<title>dgray.io – Anonymous Classifieds with Monero</title>
|
<title>dgray.io – Anonyme Kleinanzeigen</title>
|
||||||
|
|
||||||
<!-- Open Graph -->
|
<!-- Open Graph -->
|
||||||
<meta property="og:type" content="website">
|
<meta property="og:type" content="website">
|
||||||
<meta property="og:site_name" content="dgray.io">
|
<meta property="og:site_name" content="dgray.io">
|
||||||
<meta property="og:title" content="dgray.io – Anonymous Classifieds with Monero">
|
<meta property="og:title" content="dgray.io – Anonyme Kleinanzeigen">
|
||||||
<meta property="og:description" content="Buy and sell anonymously with Monero. No KYC, no email, E2E encrypted chat.">
|
<meta property="og:description" content="Kaufen und verkaufen ohne Konto, ohne E-Mail. Bezahlung mit Monero. Ende-zu-Ende verschlüsselter Chat.">
|
||||||
<meta property="og:url" content="https://dgray.io">
|
<meta property="og:url" content="https://dgray.io">
|
||||||
<meta property="og:image" content="https://dgray.io/assets/press/og-image.png">
|
<meta property="og:image" content="https://dgray.io/assets/press/og-image.png">
|
||||||
<meta property="og:image:width" content="1200">
|
<meta property="og:image:width" content="1200">
|
||||||
@@ -23,8 +23,8 @@
|
|||||||
|
|
||||||
<!-- X (Twitter) Card -->
|
<!-- X (Twitter) Card -->
|
||||||
<meta name="twitter:card" content="summary_large_image">
|
<meta name="twitter:card" content="summary_large_image">
|
||||||
<meta name="twitter:title" content="dgray.io – Anonymous Classifieds with Monero">
|
<meta name="twitter:title" content="dgray.io – Anonyme Kleinanzeigen">
|
||||||
<meta name="twitter:description" content="Buy and sell anonymously with Monero. No KYC, no email, E2E encrypted chat.">
|
<meta name="twitter:description" content="Kaufen und verkaufen ohne Konto, ohne E-Mail. Bezahlung mit Monero. Ende-zu-Ende verschlüsselter Chat.">
|
||||||
<meta name="twitter:image" content="https://dgray.io/assets/press/og-image.png">
|
<meta name="twitter:image" content="https://dgray.io/assets/press/og-image.png">
|
||||||
|
|
||||||
<link rel="manifest" href="manifest.json">
|
<link rel="manifest" href="manifest.json">
|
||||||
|
|||||||
18
js/i18n.js
18
js/i18n.js
@@ -183,6 +183,24 @@ class I18n {
|
|||||||
const key = el.getAttribute('data-i18n-aria')
|
const key = el.getAttribute('data-i18n-aria')
|
||||||
el.setAttribute('aria-label', this.t(key))
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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": {
|
"header": {
|
||||||
"searchPlaceholder": "Was suchst du?",
|
"searchPlaceholder": "Was suchst du?",
|
||||||
"createListing": "Anzeige erstellen",
|
"createListing": "Anzeige erstellen",
|
||||||
|
|||||||
@@ -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": {
|
"header": {
|
||||||
"searchPlaceholder": "What are you looking for?",
|
"searchPlaceholder": "What are you looking for?",
|
||||||
"createListing": "Create Listing",
|
"createListing": "Create Listing",
|
||||||
|
|||||||
@@ -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": {
|
"header": {
|
||||||
"searchPlaceholder": "¿Qué estás buscando?",
|
"searchPlaceholder": "¿Qué estás buscando?",
|
||||||
"createListing": "Crear anuncio",
|
"createListing": "Crear anuncio",
|
||||||
|
|||||||
@@ -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": {
|
"header": {
|
||||||
"searchPlaceholder": "Que cherchez-vous ?",
|
"searchPlaceholder": "Que cherchez-vous ?",
|
||||||
"createListing": "Créer une annonce",
|
"createListing": "Créer une annonce",
|
||||||
|
|||||||
@@ -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": {
|
"header": {
|
||||||
"searchPlaceholder": "Cosa stai cercando?",
|
"searchPlaceholder": "Cosa stai cercando?",
|
||||||
"createListing": "Crea annuncio",
|
"createListing": "Crea annuncio",
|
||||||
|
|||||||
@@ -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": {
|
"header": {
|
||||||
"searchPlaceholder": "O que você está procurando?",
|
"searchPlaceholder": "O que você está procurando?",
|
||||||
"createListing": "Criar Anúncio",
|
"createListing": "Criar Anúncio",
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
{
|
{
|
||||||
|
"meta": {
|
||||||
|
"title": "dgray.io – Анонимные объявления",
|
||||||
|
"description": "Покупайте и продавайте без аккаунта, без email. Оплата Monero. Сквозное шифрование чата."
|
||||||
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"searchPlaceholder": "Что вы ищете?",
|
"searchPlaceholder": "Что вы ищете?",
|
||||||
"createListing": "Создать объявление",
|
"createListing": "Создать объявление",
|
||||||
|
|||||||
Reference in New Issue
Block a user