Okuma Süresi Hesaplayan WordPress Eklentisi Yapımı

WordPress ile çalışan herkes bir noktada şunu fark eder: ziyaretçiler uzun yazıları okuyup okumadıklarını bilmek ister. “Ortalama okuma süresi” göstergesi hem kullanıcı deneyimini iyileştirir hem de bounce rate’i düşürmede dolaylı katkı sağlar. Medium, Substack gibi platformlarda standart hale gelen bu özelliği kendi WordPress sitenize eklemek için mevcut eklentilere bağımlı kalmak zorunda değilsiniz. Kendi eklentinizi sıfırdan yazarak hem tam kontrol sahibi olursunuz hem de PHP/WordPress hook sistemini derinlemesine öğrenirsiniz.

Bu yazıda sıfırdan, production-ready bir WordPress eklentisi geliştireceğiz. Basit bir “şunu kopyala yapıştır” yazısı değil; settings API kullanımı, shortcode entegrasyonu, Gutenberg blok desteği ve performans optimizasyonu içeren gerçek bir eklenti geliştirme süreci anlatacağım.

Eklentinin Mimarisini Planlamak

Kod yazmadan önce ne yapacağımızı netleştirmek gerekiyor. İyi bir sysadmin gibi düşünün: önce gereksinimler, sonra tasarım, sonra implementasyon.

Eklentimiz şunları yapacak:

  • Yazıdaki kelime sayısını hesaplayacak
  • Ortalama okuma hızına göre süreyi belirleyecek (varsayılan: dakikada 200 kelime)
  • Yazı başlığının altına veya içeriğin üstüne otomatik ekleyecek
  • Shortcode ile manuel yerleştirmeye izin verecek
  • WordPress admin panelinden yapılandırılabilir olacak
  • Resimleri ve medyayı hesaba katacak (bir resim = 12 saniyelik ek süre)

Klasik WordPress eklenti yapısını kullanacağız, OOP yaklaşımıyla:

wp-content/plugins/reading-time-calculator/
├── reading-time-calculator.php   # Ana dosya
├── includes/
│   ├── class-rtc-core.php        # Çekirdek mantık
│   ├── class-rtc-settings.php    # Admin ayarları
│   └── class-rtc-shortcode.php   # Shortcode işlemleri
├── assets/
│   ├── css/
│   │   └── rtc-style.css
│   └── js/
│       └── rtc-admin.js
└── readme.txt

Ana Eklenti Dosyası

WordPress her eklentiyi /wp-content/plugins/ altında kendi dizininde arar. Ana dosyada mutlaka plugin header yorumu olmalı:

<?php
/**
 * Plugin Name: Okuma Süresi Hesaplayıcı
 * Plugin URI: https://example.com/reading-time-calculator
 * Description: WordPress yazılarınız için otomatik okuma süresi hesaplar.
 * Version: 1.0.0
 * Author: Sysadmin Blog
 * Author URI: https://example.com
 * License: GPL v2 or later
 * Text Domain: reading-time-calculator
 * Domain Path: /languages
 */

// Direkt erişimi engelle
if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

// Sabitler
define( 'RTC_VERSION', '1.0.0' );
define( 'RTC_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
define( 'RTC_PLUGIN_URL', plugin_dir_url( __FILE__ ) );

// Sınıfları yükle
require_once RTC_PLUGIN_DIR . 'includes/class-rtc-core.php';
require_once RTC_PLUGIN_DIR . 'includes/class-rtc-settings.php';
require_once RTC_PLUGIN_DIR . 'includes/class-rtc-shortcode.php';

// Eklentiyi başlat
function rtc_init() {
    $core = new RTC_Core();
    $core->init();

    $settings = new RTC_Settings();
    $settings->init();

    $shortcode = new RTC_Shortcode();
    $shortcode->init();
}
add_action( 'plugins_loaded', 'rtc_init' );

// Aktivasyon hook
register_activation_hook( __FILE__, 'rtc_activate' );
function rtc_activate() {
    // Varsayılan seçenekleri kaydet
    $defaults = array(
        'words_per_minute'    => 200,
        'image_reading_time'  => 12,
        'display_position'    => 'before_content',
        'show_on_post_types'  => array( 'post' ),
        'prefix_text'         => 'Tahmini okuma süresi:',
        'suffix_text'         => 'dakika',
        'show_icon'           => 'yes',
    );

    if ( ! get_option( 'rtc_settings' ) ) {
        add_option( 'rtc_settings', $defaults );
    }
}

// Deaktivasyon hook (opsiyonel temizlik)
register_deactivation_hook( __FILE__, 'rtc_deactivate' );
function rtc_deactivate() {
    // Gerekirse cache temizliği burada yapılır
}

ABSPATH kontrolü kritik bir güvenlik önlemi. WordPress yüklenmeden direkt PHP dosyasına erişilmesini engeller. Bu alışkanlığı hiç bırakmayın.

Çekirdek Hesaplama Mantığı

class-rtc-core.php dosyası eklentinin beyni. Kelime sayma, süre hesaplama ve içeriğe ekleme işlemlerini buraya yazacağız:

<?php
if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

class RTC_Core {

    private $settings;

    public function __construct() {
        $this->settings = get_option( 'rtc_settings', array() );
    }

    public function init() {
        add_filter( 'the_content', array( $this, 'maybe_add_reading_time' ) );
        add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_styles' ) );
    }

    /**
     * İçeriğe okuma süresini ekle
     */
    public function maybe_add_reading_time( $content ) {
        // Sadece tekil yazı görünümünde çalış
        if ( ! is_singular() ) {
            return $content;
        }

        $post_type = get_post_type();
        $allowed_types = isset( $this->settings['show_on_post_types'] )
            ? $this->settings['show_on_post_types']
            : array( 'post' );

        if ( ! in_array( $post_type, $allowed_types ) ) {
            return $content;
        }

        $reading_time_html = $this->get_reading_time_html( $content );
        $position = isset( $this->settings['display_position'] )
            ? $this->settings['display_position']
            : 'before_content';

        if ( 'before_content' === $position ) {
            return $reading_time_html . $content;
        } elseif ( 'after_content' === $position ) {
            return $content . $reading_time_html;
        }

        return $content;
    }

    /**
     * Okuma süresini hesapla
     */
    public function calculate_reading_time( $content ) {
        $wpm = isset( $this->settings['words_per_minute'] )
            ? intval( $this->settings['words_per_minute'] )
            : 200;

        $image_time = isset( $this->settings['image_reading_time'] )
            ? intval( $this->settings['image_reading_time'] )
            : 12;

        // HTML'i temizle, sadece metni al
        $clean_content = wp_strip_all_tags( $content );
        $word_count    = str_word_count( $clean_content );

        // Resim sayısını bul
        preg_match_all( '/<img[^>]+>/i', $content, $images );
        $image_count = count( $images[0] );

        // Toplam saniye
        $reading_seconds  = ( $word_count / $wpm ) * 60;
        $image_seconds    = $image_count * $image_time;
        $total_seconds    = $reading_seconds + $image_seconds;

        // Dakikaya çevir, en az 1 dakika göster
        $total_minutes = max( 1, ceil( $total_seconds / 60 ) );

        return array(
            'minutes'     => $total_minutes,
            'word_count'  => $word_count,
            'image_count' => $image_count,
        );
    }

    /**
     * HTML çıktısını oluştur
     */
    public function get_reading_time_html( $content ) {
        $time_data   = $this->calculate_reading_time( $content );
        $prefix      = isset( $this->settings['prefix_text'] )
            ? esc_html( $this->settings['prefix_text'] )
            : 'Tahmini okuma süresi:';
        $suffix      = isset( $this->settings['suffix_text'] )
            ? esc_html( $this->settings['suffix_text'] )
            : 'dakika';
        $show_icon   = isset( $this->settings['show_icon'] )
            ? $this->settings['show_icon']
            : 'yes';

        $icon = '';
        if ( 'yes' === $show_icon ) {
            $icon = '<span class="rtc-icon" aria-hidden="true">&#9200;</span> ';
        }

        $html  = '<div class="rtc-reading-time" aria-label="' . $prefix . ' ' . $time_data['minutes'] . ' ' . $suffix . '">';
        $html .= $icon;
        $html .= '<span class="rtc-prefix">' . $prefix . '</span> ';
        $html .= '<span class="rtc-time">' . $time_data['minutes'] . '</span> ';
        $html .= '<span class="rtc-suffix">' . $suffix . '</span>';
        $html .= '</div>';

        return $html;
    }

    public function enqueue_styles() {
        wp_enqueue_style(
            'rtc-style',
            RTC_PLUGIN_URL . 'assets/css/rtc-style.css',
            array(),
            RTC_VERSION
        );
    }
}

str_word_count() Türkçe karakterlerle bazen tutarsız davranır. Production ortamında mb_str_word_count() gibi özel bir fonksiyon yazmanızı öneririm, aşağıda buna değineceğim.

Admin Ayarlar Paneli

WordPress Settings API, güvenli ve tutarlı bir admin panel oluşturmanın standart yolu. Kendi HTML formu yazmak yerine bu API’yi kullanmak hem güvenlik açısından doğru hem de WordPress tema/eklenti standartlarına uygun:

<?php
if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

class RTC_Settings {

    public function init() {
        add_action( 'admin_menu', array( $this, 'add_settings_page' ) );
        add_action( 'admin_init', array( $this, 'register_settings' ) );
        add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_scripts' ) );
    }

    public function add_settings_page() {
        add_options_page(
            'Okuma Süresi Ayarları',
            'Okuma Süresi',
            'manage_options',
            'reading-time-calculator',
            array( $this, 'render_settings_page' )
        );
    }

    public function register_settings() {
        register_setting(
            'rtc_settings_group',
            'rtc_settings',
            array( $this, 'sanitize_settings' )
        );

        add_settings_section(
            'rtc_general_section',
            'Genel Ayarlar',
            array( $this, 'general_section_callback' ),
            'reading-time-calculator'
        );

        add_settings_field(
            'words_per_minute',
            'Dakikada Kelime Sayısı',
            array( $this, 'wpm_field_callback' ),
            'reading-time-calculator',
            'rtc_general_section'
        );

        add_settings_field(
            'display_position',
            'Gösterim Konumu',
            array( $this, 'position_field_callback' ),
            'reading-time-calculator',
            'rtc_general_section'
        );

        add_settings_field(
            'prefix_text',
            'Ön Metin',
            array( $this, 'prefix_field_callback' ),
            'reading-time-calculator',
            'rtc_general_section'
        );
    }

    public function sanitize_settings( $input ) {
        $sanitized = array();

        $sanitized['words_per_minute'] = isset( $input['words_per_minute'] )
            ? absint( $input['words_per_minute'] )
            : 200;

        // WPM için makul sınırlar
        if ( $sanitized['words_per_minute'] < 100 ) {
            $sanitized['words_per_minute'] = 100;
        }
        if ( $sanitized['words_per_minute'] > 500 ) {
            $sanitized['words_per_minute'] = 500;
        }

        $sanitized['display_position'] = isset( $input['display_position'] )
            && in_array( $input['display_position'], array( 'before_content', 'after_content' ) )
            ? $input['display_position']
            : 'before_content';

        $sanitized['prefix_text'] = isset( $input['prefix_text'] )
            ? sanitize_text_field( $input['prefix_text'] )
            : 'Tahmini okuma süresi:';

        $sanitized['suffix_text'] = isset( $input['suffix_text'] )
            ? sanitize_text_field( $input['suffix_text'] )
            : 'dakika';

        $sanitized['show_icon'] = isset( $input['show_icon'] ) ? 'yes' : 'no';

        $sanitized['image_reading_time'] = isset( $input['image_reading_time'] )
            ? absint( $input['image_reading_time'] )
            : 12;

        return $sanitized;
    }

    public function render_settings_page() {
        if ( ! current_user_can( 'manage_options' ) ) {
            return;
        }
        ?>
        <div class="wrap">
            <h1><?php echo esc_html( get_admin_page_title() ); ?></h1>
            <form action="options.php" method="post">
                <?php
                settings_fields( 'rtc_settings_group' );
                do_settings_sections( 'reading-time-calculator' );
                submit_button( 'Ayarları Kaydet' );
                ?>
            </form>
        </div>
        <?php
    }

    public function general_section_callback() {
        echo '<p>Okuma süresi hesaplayıcısının genel davranışını buradan ayarlayabilirsiniz.</p>';
    }

    public function wpm_field_callback() {
        $settings = get_option( 'rtc_settings' );
        $value    = isset( $settings['words_per_minute'] ) ? $settings['words_per_minute'] : 200;
        echo '<input type="number" name="rtc_settings[words_per_minute]" value="' . esc_attr( $value ) . '" min="100" max="500" />';
        echo '<p class="description">Ortalama Türk okuyucu için 200 kelime/dakika önerilir.</p>';
    }

    public function position_field_callback() {
        $settings = get_option( 'rtc_settings' );
        $position = isset( $settings['display_position'] ) ? $settings['display_position'] : 'before_content';
        echo '<select name="rtc_settings[display_position]">';
        echo '<option value="before_content"' . selected( $position, 'before_content', false ) . '>İçerik Öncesi</option>';
        echo '<option value="after_content"' . selected( $position, 'after_content', false ) . '>İçerik Sonrası</option>';
        echo '</select>';
    }

    public function prefix_field_callback() {
        $settings = get_option( 'rtc_settings' );
        $value    = isset( $settings['prefix_text'] ) ? $settings['prefix_text'] : 'Tahmini okuma süresi:';
        echo '<input type="text" name="rtc_settings[prefix_text]" value="' . esc_attr( $value ) . '" class="regular-text" />';
    }

    public function enqueue_admin_scripts( $hook ) {
        if ( 'settings_page_reading-time-calculator' !== $hook ) {
            return;
        }
        wp_enqueue_script(
            'rtc-admin',
            RTC_PLUGIN_URL . 'assets/js/rtc-admin.js',
            array( 'jquery' ),
            RTC_VERSION,
            true
        );
    }
}

Shortcode Entegrasyonu

Otomatik yerleştirme her zaman yeterli değil. Bazı temalarda the_content filtresi çalışmaz ya da editörde spesifik bir yere koymak istersiniz. Shortcode bu durumu çözer:

<?php
if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

class RTC_Shortcode {

    public function init() {
        add_shortcode( 'okuma_suresi', array( $this, 'render_shortcode' ) );
        // Gutenberg için de register et
        add_action( 'init', array( $this, 'register_block' ) );
    }

    /**
     * [okuma_suresi] shortcode
     * Kullanım: [okuma_suresi wpm="200" show_icon="yes"]
     */
    public function render_shortcode( $atts ) {
        $atts = shortcode_atts(
            array(
                'wpm'       => 200,
                'show_icon' => 'yes',
                'prefix'    => 'Tahmini okuma süresi:',
                'suffix'    => 'dakika',
            ),
            $atts,
            'okuma_suresi'
        );

        global $post;
        if ( ! $post ) {
            return '';
        }

        $core    = new RTC_Core();
        $content = get_the_content( null, false, $post );
        $data    = $core->calculate_reading_time( $content );

        $icon = '';
        if ( 'yes' === $atts['show_icon'] ) {
            $icon = '<span class="rtc-icon" aria-hidden="true">&#9200;</span> ';
        }

        $html  = '<div class="rtc-reading-time rtc-shortcode">';
        $html .= $icon;
        $html .= '<span class="rtc-prefix">' . esc_html( $atts['prefix'] ) . '</span> ';
        $html .= '<span class="rtc-time">' . intval( $data['minutes'] ) . '</span> ';
        $html .= '<span class="rtc-suffix">' . esc_html( $atts['suffix'] ) . '</span>';
        $html .= '</div>';

        return $html;
    }

    /**
     * Gutenberg blok kaydı (REST API üzerinden çalışır)
     */
    public function register_block() {
        if ( ! function_exists( 'register_block_type' ) ) {
            return;
        }

        register_block_type( 'rtc/reading-time', array(
            'render_callback' => array( $this, 'render_gutenberg_block' ),
            'attributes'      => array(
                'showIcon' => array(
                    'type'    => 'boolean',
                    'default' => true,
                ),
            ),
        ) );
    }

    public function render_gutenberg_block( $attributes ) {
        $atts = array(
            'show_icon' => ! empty( $attributes['showIcon'] ) ? 'yes' : 'no',
        );
        return $this->render_shortcode( $atts );
    }
}

Türkçe Karakter Sorunu için Özel Kelime Sayma

PHP’nin str_word_count() fonksiyonu ASCII tabanlıdır ve Türkçe karakterleri (ğ, ş, ı, ü, ö, ç) kelime sınırı olarak algılayabilir. Gerçek bir Türkçe blog için bunu düzeltmek şart:

<?php
/**
 * Türkçe karakter destekli kelime sayma
 * Bu fonksiyonu RTC_Core sınıfına ekleyin
 */
private function count_words_turkish( $text ) {
    // HTML taglarını temizle
    $text = wp_strip_all_tags( $text );

    // Decode HTML entities
    $text = html_entity_decode( $text, ENT_QUOTES, 'UTF-8' );

    // Türkçe karakterler dahil Unicode harf grubu ile eşleştir
    // p{L} = herhangi bir dildeki harf karakteri
    // p{N} = sayı karakteri
    preg_match_all( '/[p{L}p{N}]+(?:[''-][p{L}p{N}]+)*/u', $text, $matches );

    return count( $matches[0] );
}

Bu regex pattern’i /u modifier’ı ile UTF-8 modunda çalışır. p{L} ifadesi Unicode Letter kategorisindeki tüm karakterleri kapsar; yani Türkçe, Arapça, Rusça ne olursa olsun doğru sayım yaparsınız.

CSS Stilleri

Eklentimizin frontend görünümü şık ve tema bağımsız olmalı:

/* assets/css/rtc-style.css */

.rtc-reading-time {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    padding: 6px 12px;
    margin-bottom: 16px;
    background-color: #f0f4f8;
    border-left: 3px solid #3b82f6;
    border-radius: 4px;
    font-size: 0.875rem;
    color: #475569;
    font-family: inherit;
    line-height: 1.5;
}

.rtc-icon {
    font-size: 1rem;
    flex-shrink: 0;
}

.rtc-time {
    font-weight: 700;
    color: #1e40af;
}

.rtc-prefix,
.rtc-suffix {
    font-weight: 400;
}

/* Koyu tema desteği */
@media (prefers-color-scheme: dark) {
    .rtc-reading-time {
        background-color: #1e293b;
        border-left-color: #60a5fa;
        color: #94a3b8;
    }

    .rtc-time {
        color: #93c5fd;
    }
}

/* Mobil uyum */
@media screen and (max-width: 480px) {
    .rtc-reading-time {
        font-size: 0.8rem;
        padding: 5px 10px;
    }
}

Gerçek Dünya Senaryoları ve Edge Case’ler

Bir eklentiyi production’a almadan önce birkaç durumu test etmek gerekir:

Çok kısa yazılar: 50 kelimelik bir yazı için “0 dakika” göstermemek için max(1, ...) kullandık. Ama kullanıcıya “1 dakikadan az” göstermek daha iyi bir UX olabilir. Bunu suffix_text ayarından özelleştirilebilir yapabilirsiniz.

WooCommerce ürün sayfaları: show_on_post_types ayarına product eklenirse ürün açıklamalarında da çalışır. Ancak WooCommerce the_content filtrelerini kendisi yönettiğinden çakışma olabilir. Bu durumda woocommerce_single_product_summary hook’unu ayrıca ele almak gerekir.

Çoklu dil desteği (WPML/Polylang): Eklentimiz text-domain tanımlıyor. Çeviri dosyaları için languages/ dizininde .pot dosyası oluşturmak gerekir. poedit veya wp i18n make-pot komutu bunu otomatik yapar.

Önbellek sorunu: WP Rocket, LiteSpeed Cache gibi eklentiler sayfaları statik HTML olarak önbelleğe alır. Okuma süresi değişken olmadığından bu genellikle sorun yaratmaz. Ancak dinamik içerik (kullanıcıya özel mesajlar) varsa dikkat edin.

REST API üzerinden yazı çeken headless siteler: the_content filtresi çalışmayabilir. Bu durumda REST API response’una özel alan eklemek için register_rest_field() kullanmanız gerekir. Bu başlı başına ayrı bir konu.

Eklentiyi Test Etmek

WordPress geliştirmede test süreci kritik. En azından şunları kontrol edin:

  • Farklı post type’larında görünüm (post, page, custom post type)
  • Kısa içerik (50 kelime altı)
  • Uzun içerik (10.000+ kelime)
  • Resim ağırlıklı içerik (galeri yazıları)
  • Shortcode’un doğru çalışması
  • Admin ayarlarının kaydedilmesi ve uygulanması
  • XSS açıklarına karşı sanitize kontrolü

Basit bir WP-CLI komutu ile test ortamı kurabilirsiniz:

# WP-CLI ile hızlı test içeriği oluştur
wp post create --post_title="Test Yazısı" 
  --post_content="$(wp eval 'echo str_repeat("Lorem ipsum dolor sit amet ", 300);')" 
  --post_status=publish 
  --post_type=post

# Eklenti aktif mi kontrol et
wp plugin list --status=active | grep reading-time

# Seçenek değerini doğrula
wp option get rtc_settings --format=json

Sonuç

Sıfırdan bir WordPress eklentisi yazmak göründüğü kadar karmaşık değil. Önemli olan birkaç temel prensibi içselleştirmek: her kullanıcı girdisini sanitize et, her çıktıyı escape et, WordPress hook sistemini doğru kullan, sabitler yerine ayarları veritabanında tut.

Bu eklenti yaklaşık 300-400 satır PHP kodu ile gerçek bir üretim ortamında kullanılabilecek düzeyde. Üzerine şunları ekleyerek geliştirebilirsiniz:

  • Kelime bulutu veya okunabilirlik skoru (Flesch-Kincaid Türkçe uyarlaması)
  • İlerleme çubuğu (JavaScript ile scroll-based)
  • Gutenberg sidebar paneli ile editörde canlı önizleme
  • Google Analytics custom event entegrasyonu

Kod yazarken her zaman şunu sorun kendinize: “Bu kodu altı ay sonra başka biri okursa anlayabilir mi?” Sınıf tabanlı yapı, anlamlı fonksiyon isimleri ve yorumlar bunu sağlar. WordPress dünyasında kötü yazılmış eklentilerin sistemi nasıl çökerttiğini hepimiz bizzat yaşadık. Temiz kod yazmak sadece estetik bir tercih değil, operasyonel bir zorunluluk.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir