jekcms'in yorum sistemi spam'i gerçek bir spam_score sütunuyla puanlar ve gönderimleri reCAPTCHA geçti/geçmedi kontrolüyle süzer — bazı yazılarda anlatılan recaptcha_score, spam_fingerprints ya da otomatik özet-cron altyapısı değil. Gerçek şema ve işlem hattı burada.
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 INT DEFAULT 0,
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 100 arasinda bir tam sayi olarak saklar — ayri bir reCAPTCHA sutunu yoktur, cunku reCAPTCHA agirlikli puana katilmak yerine gecer/kalir seklinde bir kapi olarak kullanilir (asagida daha fazlasi var). 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 Doğrulaması
reCAPTCHA v3 gorunmez çalışır — onay kutusu yok, resim bulmacasi yok. Google 0 (muhtemelen bot) ile 1 (muhtemelen insan) arasinda bir puan dondurur, JekCMS'in verify_recaptcha() fonksiyonu bunu ham puani saklamak yerine sabit bir esikte gecer/kalir kararina cevirir. 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 verify_recaptcha(string $token): bool {
$secret = get_setting('recaptcha_secret_key');
if (empty($secret) || empty($token)) return false;
$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'] || $result['action'] !== 'comment') return false;
return (float) $result['score'] >= RECAPTCHA_MIN_SCORE;
}
Basarisiz bir reCAPTCHA kontrolu agirlikli bir ortalamayi sessizce etkilemez — boru hattinin yorumu moderasyona yonlendirmek ya da dogrudan reddetmek için kullandigi girdilerden biridir. Ham puani saklamak yerine gecer/kalir'i sectik cunku bu, "bu puana ne kadar guvenelim" karar mekanizmasini her sitenin esik yapilandirmasina yaymak yerine tek bir yerde tutar.
Boru Hatti Puanlarini Birlestirme
Format, icerik ve hiz sinirlama asamalari her biri 0 ile 1 arasinda bir puan dondurur. Bunlari, yoruma kaydedilen 0-100 tam sayilik spam_score'a birlestiririz; reCAPTCHA sonucunu ise agirlikli bir girdi olarak degil, ayri bir kapi olarak ele aliriz:
function calculateSpamScore(array $stages): int {
$weights = [
'format' => 0.30,
'content' => 0.40,
'rateLimit' => 0.30,
];
$total = ($stages['format'] * $weights['format'])
+ ($stages['content'] * $weights['content'])
+ ($stages['rateLimit'] * $weights['rateLimit']);
return (int) round($total * 100);
}
// Karar esikleri
$spamScore = calculateSpamScore($stages);
$recaptchaPassed = verify_recaptcha($recaptchaToken);
if (!$recaptchaPassed || $spamScore >= 70) {
$status = 'spam'; // Otomatik red, moderasyon kuyrupunda gosterilmez
} elseif ($spamScore >= 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. Bekleyen yeni yorumlar sitenin admin e-postasina bildirim olarak gider — onemli olan tam teslim mekanigi degil, gruplamanin onemli olmasidir: yorum başina bir e-posta gondermek, gercek trafigi olan bir sitede bildirimlerin tamamen susturulmasinin en hizli yoludur.
Yanit bildirimleri moderasyon bildirimlerinden ayri bir konudur: birisi bir yorumcuya yanit verdiginde, orijinal yorumcu bildirim alabilir — ama yalnizca yorumunu birakirken bunu kabul ettiyse. Bu tercihi opt-in tutmak, yorum formunun istenmeyen postalara bir e-posta adresi sizdiran baska bir yol haline gelmesini onler.
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 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.