Yorum veritabanı şeması, iç içe yanıtlar, moderasyon kuyruğu, spam tespiti, reCAPTCHA v3 entegrasyonu ve e-posta bildirimleri.
Neden Cogu CMS Yorum Sistemi Basarisiz Olur
Her CMS bir sekilde yorum sistemi sunar. Cogunda bu sonradan eklenmis bir özellik gibidir — tek bir veritabanı tablosu, bir metin alani ve gonder butonu. Ilk hafta sorunsuz çalışır. Sonra spamlar baslar. Bir ay icinde kripto para ve zayiflama haplari hakkinda 4.000 bekleyen yorumunuz olur, gerçek yorumcular yanitlari hic gosterilmedigi için peslerini birakmistir ve siz de yorumlari tamamen kapatirsiniz cunku moderasyon, yaziyi yazmaktan daha uzun surer.
Biz bu donguyu uc kez yasadiktan sonra oturup JekCMS ile birlikte gelen yorum sistemini insa ettik. Temel farkindalik suydu: yorumlar tek bir özellik degil — birlikte çalışmasi gereken dort ayri sistemdir: depolama ve konulama, spam önleme, moderasyon is akisi ve bildirim dağıtımi. Bunlardan herhangi birini yanlis yapin ve tum sistem coker.
Bu kilavuz tum mimariyi kapsar. Gerçek veritabanı semasini, spam tespit hattini, reCAPTCHA entegrasyonunu ve moderasyon arayuzunu gosterecegim. Buradaki her sey onlarca JekCMS sitesinde productionda çalışıyor.
Konulu Yorumlar Icin Veritabani Semasi
Yorum sistemi tasarimindaki en yaygin hata, konulama destegi olmayan duz bir tablo kullanmaktir. Yanit baglaminin tamamen kayboldupu kronolojik bir liste elde edersiniz. Ikinci en yaygin hata ise sinursiz icleme derinligine sahip tam bitisiklik listeleri uygulamaktir — bu da rekursif sorgu kabuslarini ve dort seviyeden sonra okunamaz hale gelen UI girintisini yaratir.
JekCMS iki seviyeli bir konulama modeli kullanir. Ust duzey yorumlar yaziyi yanitlar. Yanitlar her zaman bir ust duzey yorumu hedefler, baska bir yaniti degil. Bu, arayuzu temiz ve sorgulari basit tutar.
CREATE TABLE comments (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
post_id INT UNSIGNED NOT NULL,
parent_id INT UNSIGNED DEFAULT NULL,
author_name VARCHAR(100) NOT NULL,
author_email VARCHAR(255) NOT NULL,
author_ip VARCHAR(45) NOT NULL,
author_user_agent TEXT,
content TEXT NOT NULL,
status ENUM('pending', 'approved', 'spam', 'trash') DEFAULT 'pending',
spam_score DECIMAL(3,2) DEFAULT 0.00,
recaptcha_score DECIMAL(3,2) DEFAULT NULL,
is_admin_reply TINYINT(1) DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_post_status (post_id, status),
INDEX idx_parent (parent_id),
INDEX idx_email (author_email),
INDEX idx_created (created_at),
FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE,
FOREIGN KEY (parent_id) REFERENCES comments(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
Bu semada birkac onemli nokta var. parent_id nullable — NULL degeri ust duzey yorum anlamina gelir. spam_score sutunu sezgisel analizimizin sonucunu 0 ile 1 arasinda bir deger olarak saklar. recaptcha_score Google'nin puanini ayri olarak saklar, boylece esik degerlerimizi bağımsız olarak ayarlayabiliriz. Ve author_ip VARCHAR(45) kullanir cunku IPv6 adresleri cogu kisinin beklediginden uzundur.
Konulu Yorumlari Verimli Cekme
Iki seviyeli konulamayla sadece iki sorguya — veya uygulama duzeyi gruplama ile tek bir sorguya — ihtiyacimiz var:
// Tek sorgu yaklasimi - PHP'de grupla
$comments = $db->fetchAll(
"SELECT c.*,
(SELECT COUNT(*) FROM comments r
WHERE r.parent_id = c.id AND r.status = 'approved') as reply_count
FROM comments c
WHERE c.post_id = ? AND c.status = 'approved'
ORDER BY c.parent_id IS NULL DESC, c.parent_id, c.created_at ASC",
[$postId]
);
// Konulara grupla
$threads = [];
$replies = [];
foreach ($comments as $comment) {
if ($comment['parent_id'] === null) {
$threads[$comment['id']] = $comment;
$threads[$comment['id']]['replies'] = [];
} else {
$replies[] = $comment;
}
}
foreach ($replies as $reply) {
if (isset($threads[$reply['parent_id']])) {
$threads[$reply['parent_id']]['replies'][] = $reply;
}
}
Bu, yorum sayisindan bağımsız olarak tek bir veritabanı round-trip'i ile çalışır. ORDER BY ifadesi ust duzey yorumlari once koyar (parent_id IS NULL DESC ust duzey için 1 degerini verir), sonra yanitlari ebeveynlerine ve oluşturulma zamanina gore siralar.
Spam Tespit Hatti
Spam tespiti tek bir kontrol degildir — giderek daha pahali hale gelen işlemlerin bir boru hatti olarak çalışır. Once ucuz kontrolleri yapariz ve mumkun oldugunda erken cikariz. Boru hattinin dort asamasi var: format analizi, içerik sezgileri, hiz sinirlamasi ve reCAPTCHA doğrulamasi.
Asama 1: Format Analizi
Icerigi bile incelemeden yapisal gostergeleri kontrol ederiz. Bunlar hesaplama acisindan neredeyse ucretsizdir ve spamlarin yaklasik %40'ini aninda yakalar:
function analyzeFormat(string $name, string $email, string $content): float {
$score = 0.0;
// Isimde URL var mi
if (preg_match('#https?://#i', $name)) {
$score += 0.4;
}
// Isim TAMAMEN BUYUK HARF mi
if ($name === mb_strtoupper($name) && mb_strlen($name) > 3) {
$score += 0.15;
}
// Tek kullanimlik e-posta domaini
$disposableDomains = ['mailinator.com', 'guerrillamail.com', 'tempmail.com',
'throwaway.email', '10minutemail.com', 'trashmail.com'];
$emailDomain = substr($email, strpos($email, '@') + 1);
if (in_array(strtolower($emailDomain), $disposableDomains)) {
$score += 0.3;
}
// İçerik asiri kisa (10 karakterden az) veya asiri uzun (5000 den fazla)
$len = mb_strlen($content);
if ($len < 10) $score += 0.2;
if ($len > 5000) $score += 0.15;
return min($score, 1.0);
}
Asama 2: İçerik Sezgileri
Bu asamada yorumcunun gerçekte ne yazdigini inceleriz. Spamda görünen ancak gerçek yorumlarda nadiren bulunan kalıpları arariz:
function analyzeContent(string $content): float {
$score = 0.0;
$lower = mb_strtolower($content);
// Link yogunlugu - bir yorumda 2 den fazla URL supheli
$urlCount = preg_match_all('#https?://S+#i', $content);
if ($urlCount > 2) $score += 0.3;
if ($urlCount > 5) $score += 0.3;
// Ilac ve kumar anahtar kelimeleri
$spamKeywords = ['viagra', 'cialis', 'casino', 'poker', 'bitcoin trading',
'weight loss pills', 'buy cheap', 'free money', 'click here',
'work from home', 'earn $', 'limited offer',
'bedava', 'hemen tikla', 'kazanc firsati'];
foreach ($spamKeywords as $keyword) {
if (strpos($lower, $keyword) !== false) {
$score += 0.4;
break;
}
}
// Yorumcularin asla kullanmamasi gereken HTML etiketleri
if (preg_match('/<(script|iframe|object|embed|form)/i', $content)) {
$score += 0.5;
}
return min($score, 1.0);
}
Anahtar kelime listesi bilerek kisa tutulmustur. Uzun anahtar kelime listeleri yanlis pozitifler uretir ve surekli bakim gerektirir. Herhangi bir asamayi mukemmel yapmaya çalışmak yerine dort asamanin birlesimime guveniyoruz.
Asama 3: Hiz Sinirlamasi
Gerçek yorumcular kisa bir surede nadiren iki veya ucten fazla yorum yapar. Spam botlar duzinelerce yazar. IP ve e-posta başına gonderim oranlarini takip ederiz:
function checkRateLimit(string $ip, string $email): float {
global $db;
// Son bir saatte bu IP den gelen yorumlar
$recentByIp = $db->fetch(
"SELECT COUNT(*) as cnt FROM comments
WHERE author_ip = ? AND created_at > DATE_SUB(NOW(), INTERVAL 1 HOUR)",
[$ip]
)['cnt'];
// Son bir gunde bu e-postadan gelen yorumlar
$recentByEmail = $db->fetch(
"SELECT COUNT(*) as cnt FROM comments
WHERE author_email = ? AND created_at > DATE_SUB(NOW(), INTERVAL 1 DAY)",
[$email]
)['cnt'];
$score = 0.0;
if ($recentByIp > 5) $score += 0.3;
if ($recentByIp > 10) $score += 0.4;
if ($recentByEmail > 10) $score += 0.3;
return min($score, 1.0);
}
Asama 4: reCAPTCHA v3 Entegrasyonu
reCAPTCHA v3 gorunmez çalışır — onay kutusu yok, resim bulmacasi yok. 0 (muhtemelen bot) ile 1 (muhtemelen insan) arasinda bir puan dondurur. Entegrasyonun iki parcasi var: frontend token uretimi ve backend doğrulamasi.
Frontend — reCAPTCHA script'ini yukleyin ve form gonderildiginde token uretin:
<script src="https://www.google.com/recaptcha/api.js?render=SITE_KEY"></script>
<script>
document.getElementById('comment-form').addEventListener('submit', function(e) {
e.preventDefault();
grecaptcha.ready(function() {
grecaptcha.execute('SITE_KEY', {action: 'comment'}).then(function(token) {
document.getElementById('recaptcha-token').value = token;
e.target.submit();
});
});
});
</script>
Backend — token'i Google'nin API'si ile doğrulayin:
function verifyRecaptcha(string $token): float {
$secret = get_setting('recaptcha_secret_key');
if (empty($secret) || empty($token)) return 0.5;
$response = file_get_contents(
'https://www.google.com/recaptcha/api/siteverify?' .
http_build_query(['secret' => $secret, 'response' => $token])
);
$result = json_decode($response, true);
if (!$result['success']) return 0.0;
if ($result['action'] !== 'comment') return 0.1;
return (float) $result['score'];
}
reCAPTCHA'dan gelen puan, spam puanimiza eklenmeden once ters cevirilir — 0.9'luk bir reCAPTCHA puani (cok insansi) spam puanina neredeyse hicbir sey katmaz, 0.1 (cok bot benzeri) ise onemli olcude ekler.
Boru Hatti Puanlarini Birlestirme
Her asama 0 ile 1 arasinda bir puan dondurur. Bunlari ağırlıkli ortalama ile birlestiririz:
function calculateSpamScore(array $stages, float $recaptchaScore): float {
$weights = [
'format' => 0.20,
'content' => 0.25,
'rateLimit' => 0.20,
'recaptcha' => 0.35,
];
$invertedRecaptcha = 1.0 - $recaptchaScore;
$total = ($stages['format'] * $weights['format'])
+ ($stages['content'] * $weights['content'])
+ ($stages['rateLimit'] * $weights['rateLimit'])
+ ($invertedRecaptcha * $weights['recaptcha']);
return round($total, 2);
}
// Karar esikleri
if ($spamScore >= 0.70) {
$status = 'spam'; // Otomatik red, moderasyon kuyrupunda gosterilmez
} elseif ($spamScore >= 0.40) {
$status = 'pending'; // Manuel inceleme gerektirir
} else {
$status = 'approved'; // Otomatik onay
}
Bu esik degerleri admin panelinden yapılandırilabilir. Yuksek trafige sahip siteler moderasyon is yukunu azaltmak için otomatik onay esigini düşürabilir. Hassas nislerdeki siteler ise insan kontrolu olmadan hicbir seyin yayina girmemesi için esigi yukseltebilir.
Moderasyon Kuyrugunun Oluşturulmasi
Moderasyon arayuzu site yoneticilerinin yorumlarla ilgili zamanlarinin cogunu gecirdigi yerdir, bu yuzden hizli ve klavye dostu olmasi gerekir. Moderasyon sayfamiz bekleyen yorumlari tam baglamlariyla gosterir — ait olduklari yazi, yanitladiklari ust yorum (konuluysa), spam puan dokumu ve tek tikla onayla/spam/cop eylemleri.
// admin/comments.php - Bekleyen yorumlari baglamla birlikte cek
$pending = $db->fetchAll(
"SELECT c.*, p.title as post_title, p.slug as post_slug,
parent.content as parent_content, parent.author_name as parent_author
FROM comments c
JOIN posts p ON p.id = c.post_id
LEFT JOIN comments parent ON parent.id = c.parent_id
WHERE c.status = 'pending'
ORDER BY c.created_at DESC
LIMIT 50"
);
Arayuz ayrica klavye kısayollari icerir: onayla için a, spam için s, cop için d, yorumlar arasinda gezinmek için j/k. Bu doğrudan Gmail'in klavye navigasyonundan esinlenmistir ve 50 yorumluk bir kuyrugu işlemek on dakika yerine iki dakikanin altinda surer.
E-posta Bildirimleri
Yorum bildirimleri dogru yapmasi zor seylerdir. Cok fazla e-posta ve yonetici bunlari kapatir. Cok az ve gerçek yorumlar gunlerce kuyrukta bekler. JekCMS yapilandiirlabilir esiklerle bir ozet yaklasimi kullanir.
Yorum başına bir e-posta gondermek yerine bildirimleri toplu hale getiririz. Sistem her 15 dakikada bir cron ile bildirilmemis yorumlari kontrol eder. Bekleyen yorumlar varsa hepsini iceren tek bir ozet e-postasi gonderir. Yanit bildirimleri için — birisi bir yorumcuya yanit verdiginde — bireysel e-postalar gondeririz ancak yalnizca orijinal yorumcu kabul ettiyse. Yorum formunda bir onay kutusu vardir: "Yanitlardan beni haberdar et." Bu tercihi bir notify_replies sutununda saklar ve gondermeden once kontrol ederiz.
Akismet Alternatifi: Yerel Spam Veritabani
Akismet spam tespiti için endustri standardidir ancak her yorum için harici bir API cagrisi gerektirir ve olcekte para tutar. Kendi kendine yeten kalmak isteyen siteler için JekCMS yerel bir spam parmak izi veritabanı tutar.
Bir yorum spam olarak işaretlendiginde (otomatik veya moderator tarafindan) parmak izlerini cikarip saklariz. E-posta parmak izi, IP parmak izi (süresi dolan — IP'ler degisir) ve içerik benzerlik hash'i. Spam tespit boru hattinda gelen yorumlari bu parmak izlerine karsi kontrol ederiz. Bes kez spam olarak işaretlenmis bir e-posta otomatik olarak spam puanina 0.5 ekler. Eslesen bir içerik hash'i 0.6 ekler. Bu yerel veritabanı zamanla daha etkili hale gelir — ne kadar cok spam işaretlerseniz, benzer spamlari yakalamada o kadar iyi olur.
Yorum Schema Markup
Google, Comment schema isarclemeyi destekler ve bu sayfalarinizin arama sonuçlarinda nasıl gorunduugunu iyileştirebilir. JekCMS bunu otomatik olarak JSON-LD seklinde uretir. Her yorum için @type: Comment, yazar bilgisi ve oluşturulma tarihi dahil edilir. Ust yapi olarak DiscussionForumPosting tipi kullanilir.
Yapilandirma ve Ayarlar
Tum yorum ayarlari Admin > Ayarlar > Yorumlar üzerinden yonetilir. Mevcut secenekler arasinda yorumlari genel olarak etkinlestirme veya devre disi birakma, yapilandiirlabilir gun sayisindan sonra yorumlari otomatik kapatma, otomatik onay ve otomatik spam için moderasyon esiklerini belirleme, reCAPTCHA'yi acip kapatma ve API anahtarlarini girme, bildirim ozet sikligini yapılandırma ve konulu yanitlari etkinlestirme veya devre disi birakma yer alir.
Bu ayarlar site_settings tablosunda comments grubu altinda saklanir ve nadiren degistikleri için agresif bicimde önbellepe alinir. Ayarlar sayfasi ayrica son 30 gun icinde kac yorumun otomatik onaylandigini, moderasyona gonderildigini ve otomatik reddedildigini gosteren bir spam istatistikleri paneli icerir.
Sonuc
Bir yorum sistemi aldatici derecede karmasiktir. Metin alani ve gonder butonu kolay kisimlaridir. Zor olan her sey gonderimden sonra olur — doğrulama, puanlama, yönlendirme, bildirim gonderme ve yorumlari gerçek tartimayi tesvik ederken spami gorunmez tutan sekilde gosterme. Boru hatti yaklasimi çalışır cunku hicbir asamanin mukemmel olmasi gerekmez. Her asama yapabildigini yakalar ve birlesmis puan herhangi bir bireysel kontrolden cok daha dogrudur. JekCMS üzerinde insaa ediyorsaniz yorumlar kutudan cikar cikmaz hazirdir. Sifirdan insa ediyorsaniz sema ve boru hatti ile baslayin — bu iki temel diger her seyi kolaylastirir.