feat: add Open Graph and X Card meta tags with server-side crawler proxy
This commit is contained in:
@@ -79,7 +79,8 @@ docs/
|
|||||||
├── DIRECTUS-SETUP.md # Directus Backend Setup
|
├── DIRECTUS-SETUP.md # Directus Backend Setup
|
||||||
├── DIRECTUS-SCHEMA.md # Collection-Strukturen & Permissions
|
├── DIRECTUS-SCHEMA.md # Collection-Strukturen & Permissions
|
||||||
├── MONETIZATION.md # Monetarisierung & Anti-Abuse
|
├── 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/
|
css/
|
||||||
├── fonts.css # @font-face Definitionen (Inter, Space Grotesk)
|
├── fonts.css # @font-face Definitionen (Inter, Space Grotesk)
|
||||||
@@ -88,7 +89,8 @@ css/
|
|||||||
└── components.css # UI-Komponenten (Buttons, Cards, etc.)
|
└── components.css # UI-Komponenten (Buttons, Cards, etc.)
|
||||||
|
|
||||||
assets/
|
assets/
|
||||||
└── fonts/ # Self-hosted Fonts (Inter, Space Grotesk)
|
├── fonts/ # Self-hosted Fonts (Inter, Space Grotesk)
|
||||||
|
└── press/ # Press Kit (Logos, OG-Image, Brand Guidelines)
|
||||||
|
|
||||||
tests/
|
tests/
|
||||||
├── index.html # Test-Runner UI (im Browser öffnen)
|
├── index.html # Test-Runner UI (im Browser öffnen)
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ dgray.io ermöglicht es Nutzern, Kleinanzeigen zu schalten und Waren/Dienstleist
|
|||||||
|
|
||||||
### Services
|
### Services
|
||||||
- **Directus** Backend: `api.dgray.io` (Docker)
|
- **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)
|
- **BTCPay Server**: `pay.xmr.rocks` (Monero-Zahlungen, Trocador-Plugin)
|
||||||
- **TweetNaCl**: Self-hosted in `js/vendor/` (E2E-Verschlüsselung)
|
- **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] PoW Captcha (server-seitig via pow.dgray.io, HMAC-signiert)
|
||||||
- [x] TweetNaCl self-hosted (kein CDN)
|
- [x] TweetNaCl self-hosted (kein CDN)
|
||||||
- [x] In-App Benachrichtigungen (Notifications-Service, Glocke mit Badge)
|
- [x] In-App Benachrichtigungen (Notifications-Service, Glocke mit Badge)
|
||||||
|
- [x] Open Graph & X Card Meta-Tags (dynamisch pro Listing)
|
||||||
- [ ] Push-Benachrichtigungen (Web Push API)
|
- [ ] Push-Benachrichtigungen (Web Push API)
|
||||||
|
|
||||||
### Phase 4: Payments
|
### Phase 4: Payments
|
||||||
|
|||||||
112
docs/pow-server/og-proxy.php
Normal file
112
docs/pow-server/og-proxy.php
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* OG Meta Tag Proxy for Social Media Crawlers
|
||||||
|
*
|
||||||
|
* Setup: Nginx/Apache rewrite rule on dgray.io:
|
||||||
|
* If User-Agent matches crawler → proxy to this script
|
||||||
|
* Else → serve static index.html
|
||||||
|
*
|
||||||
|
* Example Nginx config:
|
||||||
|
* if ($http_user_agent ~* "Twitterbot|facebookexternalhit|TelegramBot|Discordbot|Slackbot|LinkedInBot|WhatsApp") {
|
||||||
|
* proxy_pass https://pow.dgray.io/og-proxy.php;
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
|
||||||
|
require __DIR__ . '/config.php';
|
||||||
|
|
||||||
|
$requestUri = $_SERVER['REQUEST_URI'] ?? '/';
|
||||||
|
|
||||||
|
// Extract listing ID from hash URL or query param
|
||||||
|
// Crawlers may receive: /listing/UUID or ?listing=UUID
|
||||||
|
$listingId = null;
|
||||||
|
|
||||||
|
if (preg_match('#/listing/([a-f0-9-]{36})#i', $requestUri, $matches)) {
|
||||||
|
$listingId = $matches[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$listingId && isset($_GET['listing'])) {
|
||||||
|
$listingId = $_GET['listing'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$siteUrl = 'https://dgray.io';
|
||||||
|
$defaultTitle = 'dgray.io – Anonymous Classifieds with Monero';
|
||||||
|
$defaultDesc = 'Buy and sell anonymously with Monero. No KYC, no email, E2E encrypted chat.';
|
||||||
|
$defaultImage = $siteUrl . '/assets/press/og-image.png';
|
||||||
|
|
||||||
|
$title = $defaultTitle;
|
||||||
|
$description = $defaultDesc;
|
||||||
|
$image = $defaultImage;
|
||||||
|
$url = $siteUrl;
|
||||||
|
$type = 'website';
|
||||||
|
|
||||||
|
if ($listingId) {
|
||||||
|
$apiUrl = DIRECTUS_URL . '/items/listings/' . urlencode($listingId)
|
||||||
|
. '?fields=id,title,description,price,currency,images.directus_files_id.id';
|
||||||
|
|
||||||
|
$ch = curl_init($apiUrl);
|
||||||
|
curl_setopt_array($ch, [
|
||||||
|
CURLOPT_HTTPHEADER => [
|
||||||
|
'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');
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title><?= $title ?></title>
|
||||||
|
<meta name="description" content="<?= $description ?>">
|
||||||
|
|
||||||
|
<!-- Open Graph -->
|
||||||
|
<meta property="og:type" content="<?= $type ?>">
|
||||||
|
<meta property="og:site_name" content="dgray.io">
|
||||||
|
<meta property="og:title" content="<?= $title ?>">
|
||||||
|
<meta property="og:description" content="<?= $description ?>">
|
||||||
|
<meta property="og:url" content="<?= $url ?>">
|
||||||
|
<meta property="og:image" content="<?= $image ?>">
|
||||||
|
<meta property="og:image:width" content="1200">
|
||||||
|
<meta property="og:image:height" content="630">
|
||||||
|
|
||||||
|
<!-- X (Twitter) Card -->
|
||||||
|
<meta name="twitter:card" content="summary_large_image">
|
||||||
|
<meta name="twitter:title" content="<?= $title ?>">
|
||||||
|
<meta name="twitter:description" content="<?= $description ?>">
|
||||||
|
<meta name="twitter:image" content="<?= $image ?>">
|
||||||
|
|
||||||
|
<!-- Redirect real users to the actual page -->
|
||||||
|
<meta http-equiv="refresh" content="0;url=<?= $url ?>">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<p>Redirecting to <a href="<?= $url ?>"><?= $title ?></a></p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
23
index.html
23
index.html
@@ -3,10 +3,29 @@
|
|||||||
<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="dgray.io - deals in gray">
|
<meta name="description" content="Anonymer Marktplatz mit Monero-Bezahlung. Keine persönlichen Daten, E2E-verschlüsselter Chat.">
|
||||||
<meta name="theme-color" content="#555555">
|
<meta name="theme-color" content="#555555">
|
||||||
|
|
||||||
<title>dgray.io</title>
|
<title>dgray.io – Anonymous Classifieds with Monero</title>
|
||||||
|
|
||||||
|
<!-- Open Graph -->
|
||||||
|
<meta property="og:type" content="website">
|
||||||
|
<meta property="og:site_name" content="dgray.io">
|
||||||
|
<meta property="og:title" content="dgray.io – Anonymous Classifieds with Monero">
|
||||||
|
<meta property="og:description" content="Buy and sell anonymously with Monero. No KYC, no email, E2E encrypted chat.">
|
||||||
|
<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:width" content="1200">
|
||||||
|
<meta property="og:image:height" content="630">
|
||||||
|
<meta property="og:locale" content="de_DE">
|
||||||
|
<meta property="og:locale:alternate" content="en_US">
|
||||||
|
<meta property="og:locale:alternate" content="fr_FR">
|
||||||
|
|
||||||
|
<!-- X (Twitter) Card -->
|
||||||
|
<meta name="twitter:card" content="summary_large_image">
|
||||||
|
<meta name="twitter:title" content="dgray.io – Anonymous Classifieds with Monero">
|
||||||
|
<meta name="twitter:description" content="Buy and sell anonymously with Monero. No KYC, no email, E2E encrypted chat.">
|
||||||
|
<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">
|
||||||
<link rel="icon" type="image/svg+xml" href="assets/icon-light.svg" media="(prefers-color-scheme: light)">
|
<link rel="icon" type="image/svg+xml" href="assets/icon-light.svg" media="(prefers-color-scheme: light)">
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ class PageListing extends HTMLElement {
|
|||||||
disconnectedCallback() {
|
disconnectedCallback() {
|
||||||
if (this.unsubscribe) this.unsubscribe()
|
if (this.unsubscribe) this.unsubscribe()
|
||||||
window.removeEventListener('currency-changed', this.handleCurrencyChange)
|
window.removeEventListener('currency-changed', this.handleCurrencyChange)
|
||||||
|
this.resetMetaTags()
|
||||||
}
|
}
|
||||||
|
|
||||||
handleCurrencyChange() {
|
handleCurrencyChange() {
|
||||||
@@ -67,6 +68,58 @@ class PageListing extends HTMLElement {
|
|||||||
this.loading = false
|
this.loading = false
|
||||||
this.render()
|
this.render()
|
||||||
this.setupEventListeners()
|
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() {
|
async checkOwnership() {
|
||||||
|
|||||||
Reference in New Issue
Block a user