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-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)
|
||||
|
||||
@@ -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
|
||||
|
||||
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>
|
||||
<meta charset="UTF-8">
|
||||
<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">
|
||||
|
||||
<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="icon" type="image/svg+xml" href="assets/icon-light.svg" media="(prefers-color-scheme: light)">
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user