diff --git a/AGENTS.md b/AGENTS.md index e5b597a..9948217 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -79,7 +79,8 @@ docs/ ├── DIRECTUS-SETUP.md # Directus Backend Setup ├── DIRECTUS-SCHEMA.md # Collection-Strukturen & Permissions ├── MONETIZATION.md # Monetarisierung & Anti-Abuse -└── pow-server/ # PHP PoW-Captcha Server (pow.dgray.io) +├── pow-server/ # PHP PoW-Captcha Server (pow.dgray.io) +└── og-proxy.php # Open Graph Meta-Tag Proxy (pow.dgray.io) css/ ├── fonts.css # @font-face Definitionen (Inter, Space Grotesk) @@ -88,7 +89,8 @@ css/ └── components.css # UI-Komponenten (Buttons, Cards, etc.) assets/ -└── fonts/ # Self-hosted Fonts (Inter, Space Grotesk) +├── fonts/ # Self-hosted Fonts (Inter, Space Grotesk) +└── press/ # Press Kit (Logos, OG-Image, Brand Guidelines) tests/ ├── index.html # Test-Runner UI (im Browser öffnen) diff --git a/README.md b/README.md index 2019f97..04bfdec 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,7 @@ dgray.io ermöglicht es Nutzern, Kleinanzeigen zu schalten und Waren/Dienstleist ### Services - **Directus** Backend: `api.dgray.io` (Docker) -- **PoW Captcha + Payment Proxy**: `pow.dgray.io` (PHP, HMAC-signierte Challenges, BTCPay Proxy + Webhook) +- **PoW Captcha + Payment Proxy**: `pow.dgray.io` (PHP, HMAC-signierte Challenges, BTCPay Proxy + Webhook, OG Meta Proxy) - **BTCPay Server**: `pay.xmr.rocks` (Monero-Zahlungen, Trocador-Plugin) - **TweetNaCl**: Self-hosted in `js/vendor/` (E2E-Verschlüsselung) @@ -219,6 +219,7 @@ dgray/ - [x] PoW Captcha (server-seitig via pow.dgray.io, HMAC-signiert) - [x] TweetNaCl self-hosted (kein CDN) - [x] In-App Benachrichtigungen (Notifications-Service, Glocke mit Badge) +- [x] Open Graph & X Card Meta-Tags (dynamisch pro Listing) - [ ] Push-Benachrichtigungen (Web Push API) ### Phase 4: Payments diff --git a/docs/pow-server/og-proxy.php b/docs/pow-server/og-proxy.php new file mode 100644 index 0000000..b0bbc27 --- /dev/null +++ b/docs/pow-server/og-proxy.php @@ -0,0 +1,112 @@ + [ + 'Authorization: Bearer ' . DIRECTUS_TOKEN, + ], + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => 5, + ]); + $response = curl_exec($ch); + curl_close($ch); + + $data = json_decode($response, true); + $listing = $data['data'] ?? null; + + if ($listing) { + $title = htmlspecialchars($listing['title'] ?? '') . ' – dgray.io'; + $description = htmlspecialchars(mb_substr($listing['description'] ?? '', 0, 160)); + $url = $siteUrl . '/#/listing/' . $listing['id']; + $type = 'product'; + + $imageId = $listing['images'][0]['directus_files_id']['id'] + ?? $listing['images'][0]['directus_files_id'] + ?? null; + + if ($imageId) { + $image = DIRECTUS_URL . '/assets/' . $imageId . '?width=1200&height=630&fit=cover'; + } + + if (!empty($listing['price']) && !empty($listing['currency'])) { + $price = number_format((float)$listing['price'], 2); + $description .= " | {$price} {$listing['currency']}"; + } + } +} + +header('Content-Type: text/html; charset=utf-8'); +?> + + + + + <?= $title ?> + + + + + + + + + + + + + + + + + + + + + + +

Redirecting to

+ + diff --git a/index.html b/index.html index b453736..9c8c273 100644 --- a/index.html +++ b/index.html @@ -3,10 +3,29 @@ - + - dgray.io + dgray.io – Anonymous Classifieds with Monero + + + + + + + + + + + + + + + + + + + diff --git a/js/components/pages/page-listing.js b/js/components/pages/page-listing.js index 8b56302..6157f9d 100644 --- a/js/components/pages/page-listing.js +++ b/js/components/pages/page-listing.js @@ -32,6 +32,7 @@ class PageListing extends HTMLElement { disconnectedCallback() { if (this.unsubscribe) this.unsubscribe() window.removeEventListener('currency-changed', this.handleCurrencyChange) + this.resetMetaTags() } handleCurrencyChange() { @@ -67,6 +68,58 @@ class PageListing extends HTMLElement { this.loading = false this.render() this.setupEventListeners() + this.updateMetaTags() + } + + updateMetaTags() { + if (!this.listing) return + + const title = `${this.listing.title} – dgray.io` + const description = (this.listing.description || '').substring(0, 160) + const imageId = this.listing.images?.[0]?.directus_files_id?.id || this.listing.images?.[0]?.directus_files_id + const imageUrl = imageId ? directus.getFileUrl(imageId, { width: 1200, height: 630, fit: 'cover' }) : 'https://dgray.io/assets/press/og-image.png' + const url = `https://dgray.io/#/listing/${this.listing.id}` + + document.title = title + this._setMeta('description', description) + this._setMeta('og:title', title, true) + this._setMeta('og:description', description, true) + this._setMeta('og:image', imageUrl, true) + this._setMeta('og:url', url, true) + this._setMeta('og:type', 'product', true) + this._setMeta('twitter:title', title) + this._setMeta('twitter:description', description) + this._setMeta('twitter:image', imageUrl) + } + + resetMetaTags() { + const defaultTitle = 'dgray.io – Anonymous Classifieds with Monero' + const defaultDesc = 'Buy and sell anonymously with Monero. No KYC, no email, E2E encrypted chat.' + const defaultImage = 'https://dgray.io/assets/press/og-image.png' + + document.title = defaultTitle + this._setMeta('description', defaultDesc) + this._setMeta('og:title', defaultTitle, true) + this._setMeta('og:description', defaultDesc, true) + this._setMeta('og:image', defaultImage, true) + this._setMeta('og:url', 'https://dgray.io', true) + this._setMeta('og:type', 'website', true) + this._setMeta('twitter:title', defaultTitle) + this._setMeta('twitter:description', defaultDesc) + this._setMeta('twitter:image', defaultImage) + } + + _setMeta(name, content, isProperty = false) { + const attr = isProperty ? 'property' : 'name' + let el = document.querySelector(`meta[${attr}="${name}"]`) + if (el) { + el.setAttribute('content', content) + } else { + el = document.createElement('meta') + el.setAttribute(attr, name) + el.setAttribute('content', content) + document.head.appendChild(el) + } } async checkOwnership() {