Görüntülenme Sayacı Ekleyen WordPress Eklentisi Nasıl Yapılır
WordPress ile biraz vakit geçirmiş her geliştirici şunu anlar: Bir yazının kaç kez görüntülendiğini bilmek, içerik stratejisi açısından altın değerinde. Google Analytics var, evet, ama admin panelinden çıkmadan doğrudan yazının yanında o sayıyı görmek başka bir şey. Bugün bu ihtiyacı karşılayan sıfırdan bir WordPress eklentisi yazacağız. Ama sadece “çalışan” değil, doğru yazılmış, güvenli ve genişletilebilir bir eklenti.
Bu yazı biraz uzun olacak. Çünkü amacımız yalnızca kod yapıştırmak değil, bir WordPress eklentisinin nasıl düşünüldüğünü, nasıl tasarlandığını anlamak. Hazırsan başlayalım.
Eklentinin Mimarisi: Ne İnşa Ediyoruz?
Yapacağımız şey şu: Her post görüntülendiğinde bir sayaç artacak. Bu sayaç post meta olarak saklanacak. Admin panelinde bir sütun ekleyeceğiz, böylece hangi yazının kaç görüntülenme aldığını listeden görebileceğiz. Son olarak shortcode ile ya da template fonksiyonu ile bu sayıyı istediğimiz yerde göstereceğiz.
Temel bileşenler:
- post_meta tabanlı sayaç (hızlı ve basit)
- wp_footer hook’u ile sayaç artırma (bot engellemek için JavaScript ile)
- Admin sütunu entegrasyonu
- Shortcode desteği
- REST API endpoint (bonus, modern kullanım için)
- Cache uyumu (W3TC, WP Rocket gibi eklentilerle sorun çıkmaması için)
Dosya Yapısı
WordPress eklenti geliştirmenin ilk adımı dosya yapısını kurmak. Karmaşık projeler için PSR-4 ve Composer kullanılır ama bugünkü örnek için temiz ama sade bir yapı yeterli:
wp-content/plugins/post-view-counter/
├── post-view-counter.php # Ana dosya
├── includes/
│ ├── class-counter.php # Sayaç mantığı
│ ├── class-admin.php # Admin panel entegrasyonu
│ └── class-rest-api.php # REST API
├── assets/
│ └── js/
│ └── counter.js # Frontend JavaScript
└── languages/ # i18n hazırlığı
Ana Eklenti Dosyası
Her WordPress eklentisinin bir ana dosyası vardır. Plugin header bu dosyada olur. Şu bilgileri WordPress bu header’dan okur:
<?php
/**
* Plugin Name: Post View Counter
* Plugin URI: https://orneksite.com/post-view-counter
* Description: Her gönderi için görüntülenme sayacı ekler.
* Version: 1.0.0
* Author: Ahmet Yilmaz
* Author URI: https://orneksite.com
* License: GPL-2.0+
* Text Domain: post-view-counter
* Domain Path: /languages
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Direkt erişimi engelle
}
define( 'PVC_VERSION', '1.0.0' );
define( 'PVC_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
define( 'PVC_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
require_once PVC_PLUGIN_DIR . 'includes/class-counter.php';
require_once PVC_PLUGIN_DIR . 'includes/class-admin.php';
require_once PVC_PLUGIN_DIR . 'includes/class-rest-api.php';
function pvc_init() {
$counter = new PVC_Counter();
$admin = new PVC_Admin( $counter );
$rest_api = new PVC_REST_API( $counter );
$counter->register_hooks();
$admin->register_hooks();
$rest_api->register_hooks();
}
add_action( 'plugins_loaded', 'pvc_init' );
ABSPATH kontrolü kritik. Birisi doğrudan PHP dosyasına erişmeye çalışırsa, WordPress ortamı olmadan bir şey çalışmasın istiyoruz. Bu basit kontrol, pek çok açığı kapatır.
Sayaç Sınıfı: Çekirdeği Yazıyoruz
class-counter.php dosyası, işin en önemli parçası. Sayaç okuma, artırma ve sıfırlama burada yaşıyor:
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class PVC_Counter {
const META_KEY = '_pvc_view_count';
public function register_hooks() {
add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
add_action( 'wp_ajax_pvc_track_view', array( $this, 'track_view' ) );
add_action( 'wp_ajax_nopriv_pvc_track_view', array( $this, 'track_view' ) );
add_shortcode( 'post_views', array( $this, 'shortcode_handler' ) );
}
public function enqueue_scripts() {
if ( ! is_singular() ) {
return;
}
wp_enqueue_script(
'pvc-counter',
PVC_PLUGIN_URL . 'assets/js/counter.js',
array( 'jquery' ),
PVC_VERSION,
true
);
wp_localize_script( 'pvc-counter', 'pvcData', array(
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
'postId' => get_the_ID(),
'nonce' => wp_create_nonce( 'pvc_track_view_' . get_the_ID() ),
) );
}
public function track_view() {
$post_id = isset( $_POST['post_id'] ) ? absint( $_POST['post_id'] ) : 0;
if ( ! $post_id ) {
wp_send_json_error( 'Gecersiz post ID.' );
}
$nonce = isset( $_POST['nonce'] ) ? sanitize_text_field( $_POST['nonce'] ) : '';
if ( ! wp_verify_nonce( $nonce, 'pvc_track_view_' . $post_id ) ) {
wp_send_json_error( 'Guvenlik dogrulamasi basarisiz.' );
}
// Admin kullanicilarin viewlarini sayma
if ( current_user_can( 'manage_options' ) ) {
wp_send_json_success( array( 'skipped' => true ) );
}
$this->increment_view_count( $post_id );
$count = $this->get_view_count( $post_id );
wp_send_json_success( array( 'count' => $count ) );
}
public function increment_view_count( $post_id ) {
$current = $this->get_view_count( $post_id );
update_post_meta( $post_id, self::META_KEY, $current + 1 );
}
public function get_view_count( $post_id ) {
$count = get_post_meta( $post_id, self::META_KEY, true );
return $count ? absint( $count ) : 0;
}
public function reset_view_count( $post_id ) {
update_post_meta( $post_id, self::META_KEY, 0 );
}
public function shortcode_handler( $atts ) {
$atts = shortcode_atts( array(
'post_id' => get_the_ID(),
'label' => 'Görüntülenme: ',
'icon' => true,
), $atts, 'post_views' );
$post_id = absint( $atts['post_id'] );
$count = $this->get_view_count( $post_id );
$icon = filter_var( $atts['icon'], FILTER_VALIDATE_BOOLEAN ) ? '👁 ' : '';
return sprintf(
'<span class="pvc-count">%s%s%s</span>',
esc_html( $icon ),
esc_html( $atts['label'] ),
number_format_i18n( $count )
);
}
}
Burada dikkat etmemiz gereken birkaç nokta var. absint() ile post ID’yi her zaman pozitif tam sayıya çeviriyoruz. wp_verify_nonce() ile CSRF saldırılarına karşı koruma sağlıyoruz. Admin kullanıcılar sayaça dahil edilmiyor; yoksa kendi yazılarını sürekli yenileyen geliştirici sayıları şişirir.
Frontend JavaScript: Cache Sorununun Çözümü
Bu noktada bir gerçeklikle yüzleşelim. Çoğu production WordPress sitesinde page caching var. Eğer sayacı doğrudan PHP ile artırırsak, cache’den gelen sayfada sayaç asla artmaz. Çözüm: Sayacı JavaScript ile, yani sayfa yüklendikten sonra AJAX ile artırmak.
// assets/js/counter.js
(function ($) {
'use strict';
$(document).ready(function () {
if (typeof pvcData === 'undefined') {
return;
}
var sessionKey = 'pvc_viewed_' + pvcData.postId;
// Aynı oturumda birden fazla saymayı engelle
if (sessionStorage.getItem(sessionKey)) {
return;
}
$.ajax({
url: pvcData.ajaxUrl,
type: 'POST',
data: {
action: 'pvc_track_view',
post_id: pvcData.postId,
nonce: pvcData.nonce
},
success: function (response) {
if (response.success && !response.data.skipped) {
sessionStorage.setItem(sessionKey, '1');
// Sayfada sayacı gösteren bir element varsa güncelle
var $counter = $('.pvc-count');
if ($counter.length && response.data.count) {
// Label kısmını koruyarak sadece sayıyı güncelle
var currentText = $counter.text();
var parts = currentText.split(':');
if (parts.length > 1) {
$counter.text(parts[0] + ': ' + response.data.count);
}
}
}
}
});
});
}(jQuery));
sessionStorage kullanımına dikkat et. localStorage kullansaydık, kullanıcı tarayıcısını kapatıp açsa da sayılmaz olurdu. Session mantığı daha adil: Aynı oturumda aynı yazıyı defalarca yenilemek sayılmıyor, ama yeni sekmede ya da yeni oturumda sayılıyor.
Admin Sınıfı: Post Listesine Sütun Ekleme
Admin tarafında iki şey yapacağız: Post listesine “Görüntülenme” sütunu ekleyeceğiz ve bu sütuna göre sıralamayı etkinleştireceğiz.
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class PVC_Admin {
private $counter;
public function __construct( PVC_Counter $counter ) {
$this->counter = $counter;
}
public function register_hooks() {
add_filter( 'manage_posts_columns', array( $this, 'add_view_column' ) );
add_action( 'manage_posts_custom_column', array( $this, 'render_view_column' ), 10, 2 );
add_filter( 'manage_edit-post_sortable_columns', array( $this, 'make_column_sortable' ) );
add_action( 'pre_get_posts', array( $this, 'sort_by_view_count' ) );
add_action( 'add_meta_boxes', array( $this, 'add_meta_box' ) );
add_action( 'admin_init', array( $this, 'handle_reset_action' ) );
}
public function add_view_column( $columns ) {
$columns['pvc_views'] = '👁 Görüntülenme';
return $columns;
}
public function render_view_column( $column, $post_id ) {
if ( 'pvc_views' !== $column ) {
return;
}
$count = $this->counter->get_view_count( $post_id );
$reset_url = wp_nonce_url(
admin_url( 'admin.php?action=pvc_reset&post_id=' . $post_id ),
'pvc_reset_' . $post_id
);
printf(
'<strong>%s</strong> <br><small><a href="%s" onclick="return confirm('Sayacı sıfırlamak istediğinizden emin misiniz?')">Sıfırla</a></small>',
number_format_i18n( $count ),
esc_url( $reset_url )
);
}
public function make_column_sortable( $columns ) {
$columns['pvc_views'] = 'pvc_views';
return $columns;
}
public function sort_by_view_count( $query ) {
if ( ! is_admin() || ! $query->is_main_query() ) {
return;
}
if ( 'pvc_views' === $query->get( 'orderby' ) ) {
$query->set( 'meta_key', PVC_Counter::META_KEY );
$query->set( 'orderby', 'meta_value_num' );
}
}
public function add_meta_box() {
add_meta_box(
'pvc_stats',
'Görüntülenme İstatistikleri',
array( $this, 'render_meta_box' ),
'post',
'side',
'low'
);
}
public function render_meta_box( $post ) {
$count = $this->counter->get_view_count( $post->ID );
$reset_url = wp_nonce_url(
admin_url( 'admin.php?action=pvc_reset&post_id=' . $post->ID ),
'pvc_reset_' . $post->ID
);
echo '<p><strong>Toplam görüntülenme:</strong> ' . number_format_i18n( $count ) . '</p>';
echo '<p><a href="' . esc_url( $reset_url ) . '" class="button">Sayacı Sıfırla</a></p>';
}
public function handle_reset_action() {
if ( ! isset( $_GET['action'] ) || 'pvc_reset' !== $_GET['action'] ) {
return;
}
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( 'Yetersiz yetki.' );
}
$post_id = absint( $_GET['post_id'] );
if ( ! wp_verify_nonce( $_GET['_wpnonce'], 'pvc_reset_' . $post_id ) ) {
wp_die( 'Güvenlik doğrulaması başarısız.' );
}
$this->counter->reset_view_count( $post_id );
wp_redirect( admin_url( 'edit.php?pvc_reset=1' ) );
exit;
}
}
REST API Endpoint: Modern Entegrasyon
Headless WordPress kullananlar, mobil uygulama geliştirenler ya da Gutenberg blokları yazanlar için REST API endpoint eklemek artık standart bir pratik. Çok da karmaşık değil:
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class PVC_REST_API {
private $counter;
public function __construct( PVC_Counter $counter ) {
$this->counter = $counter;
}
public function register_hooks() {
add_action( 'rest_api_init', array( $this, 'register_routes' ) );
}
public function register_routes() {
register_rest_route( 'pvc/v1', '/views/(?P<id>d+)', array(
'methods' => 'GET',
'callback' => array( $this, 'get_view_count' ),
'permission_callback' => '__return_true',
'args' => array(
'id' => array(
'validate_callback' => function( $param ) {
return is_numeric( $param );
},
'sanitize_callback' => 'absint',
),
),
) );
register_rest_route( 'pvc/v1', '/views/(?P<id>d+)/increment', array(
'methods' => 'POST',
'callback' => array( $this, 'increment_view_count' ),
'permission_callback' => '__return_true',
'args' => array(
'id' => array(
'validate_callback' => function( $param ) {
return is_numeric( $param );
},
'sanitize_callback' => 'absint',
),
),
) );
}
public function get_view_count( WP_REST_Request $request ) {
$post_id = $request->get_param( 'id' );
$post = get_post( $post_id );
if ( ! $post ) {
return new WP_Error( 'not_found', 'Gönderi bulunamadı.', array( 'status' => 404 ) );
}
return rest_ensure_response( array(
'post_id' => $post_id,
'count' => $this->counter->get_view_count( $post_id ),
) );
}
public function increment_view_count( WP_REST_Request $request ) {
$post_id = $request->get_param( 'id' );
$post = get_post( $post_id );
if ( ! $post ) {
return new WP_Error( 'not_found', 'Gönderi bulunamadı.', array( 'status' => 404 ) );
}
$this->counter->increment_view_count( $post_id );
return rest_ensure_response( array(
'post_id' => $post_id,
'count' => $this->counter->get_view_count( $post_id ),
) );
}
}
REST API endpointlerini test etmek için curl kullanabilirsin:
# Görüntülenme sayısını al
curl -X GET "https://siteniz.com/wp-json/pvc/v1/views/123"
# Sayacı artır (mobil uygulama veya headless CMS senaryosu)
curl -X POST "https://siteniz.com/wp-json/pvc/v1/views/123/increment"
-H "Content-Type: application/json"
# Beklenen yanıt formatı:
# {"post_id":123,"count":457}
Şablonda Kullanım
Tema dosyalarında sayacı göstermek için iki yol var. Birincisi shortcode, ikincisi doğrudan fonksiyon çağrısı:
<?php
// single.php veya content.php içinde
// Yöntem 1: Shortcode (Gutenberg editöründe de çalışır)
// [post_views label="Okunma: " icon="true"]
// Yöntem 2: Template tag
// Eklentiye bağımlılığı kontrol ederek kullan:
if ( class_exists( 'PVC_Counter' ) ) {
$counter = new PVC_Counter();
$count = $counter->get_view_count( get_the_ID() );
echo '<span class="post-views">';
echo '<svg>...</svg> '; // İkon eklenebilir
printf(
esc_html__( '%s görüntülenme', 'post-view-counter' ),
number_format_i18n( $count )
);
echo '</span>';
}
// Yöntem 3: En popüler yazıları çek (WP_Query ile)
$popular_posts = new WP_Query( array(
'post_type' => 'post',
'posts_per_page' => 5,
'meta_key' => '_pvc_view_count',
'orderby' => 'meta_value_num',
'order' => 'DESC',
) );
Performans ve Güvenlik Notları
Production ortamında bu eklenti çalışmadan önce birkaç şeyi netleştirmek gerekiyor.
Yüksek trafikli sitelerde race condition sorunu: Çok eş zamanlı istek geldiğinde get_post_meta + update_post_meta ikilisi doğrusal çalışmaz. Bu durumda doğrudan SQL kullanmak daha sağlıklı:
public function increment_view_count_safe( $post_id ) {
global $wpdb;
// Atomic increment - race condition'a karşı güvenli
$wpdb->query(
$wpdb->prepare(
"INSERT INTO {$wpdb->postmeta} (post_id, meta_key, meta_value)
VALUES (%d, %s, 1)
ON DUPLICATE KEY UPDATE meta_value = meta_value + 1",
$post_id,
self::META_KEY
)
);
}
Bu sorgu MySQL’in ON DUPLICATE KEY UPDATE özelliğini kullanır. Eş zamanlı 100 istek gelse bile sayaç tutarlı kalır.
Bot trafiğini eleme: JavaScript üzerinden sayma yapmak zaten büyük bir bot filtresi. Ama daha da iyileştirmek istersen user-agent kontrolü ekleyebilirsin. Bunu PHP tarafında değil, Nginx seviyesinde yapman daha akıllıca:
# /etc/nginx/sites-available/siteniz.conf
# Bot user-agent'lardan gelen isteklerde AJAX endpoint'ini engelle
location = /wp-admin/admin-ajax.php {
# Basit bot kontrolü
if ($http_user_agent ~* "(bot|crawl|slurp|spider|mediapartners)") {
return 403;
}
fastcgi_pass unix:/run/php/php8.1-fpm.sock;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
Sonuç
Sıfırdan bir WordPress eklentisi yazmak göründüğü kadar zor değil, ama küçük kararlar büyük farklar yaratıyor. update_post_meta yerine doğrudan SQL kullanmak, nonce doğrulaması, admin görüntülemelerini saymaması, session tabanlı tekrar sayım engeli… Bunların her biri gerçek production sorunlarından doğmuş çözümler.
Bu eklentiyi kendin geliştirip kullanıyorsan ekleyebileceğin birkaç fikir daha:
- Günlük/haftalık görüntülenme takibi için ayrı bir custom table
- WooCommerce ürünleri için de aynı sayacı genişletme
- Görüntülenme eşiğine göre “Popüler” rozeti otomatik ekleme
- Redis Object Cache ile sayaç yazımını toplu yapma (yüksek trafik için)
Kodu olduğu gibi almak yerine ihtiyacına göre kırp, genişlet. WordPress ekosistemi buna izin veriyor; ondan faydalanmak senin elinde.
