WordPress’te Özel Hata Sayfaları: 403 ve 503 Hataları için functions.php Kullanımı

Bir WordPress sitesini yönetirken er ya da geç şu gerçekle yüzleşirsin: varsayılan hata sayfaları hem çirkin hem de kullanıcı deneyimi açısından berbat. Ziyaretçin 403 veya 503 hatası aldığında karşısına çıkan o jenerik beyaz ekran, sitenin profesyonelliğine ciddi zarar verir. Üstelik bu hata sayfaları SEO açısından da kritik; arama motorları bu sayfaları tarar ve nasıl davrandıklarına göre sitenin indekslemesini etkiler. Bu yazıda WordPress’te özel 403 ve 503 hata sayfaları oluşturmayı, bunları functions.php üzerinden yönetmeyi ve gerçek dünya senaryolarında nasıl kullanabileceğini adım adım ele alacağız.

403 ve 503 Hatalarını Anlamak

Önce temel bilgileri netleştirelim. Bu iki hata birbirine karıştırılıyor ama tamamen farklı anlamlar taşıyor.

403 Forbidden: Sunucu isteği anladı ama yerine getirmeyi reddetti. Yani kaynak var, ama erişim izni yok. Örneğin üyelik gerektiren bir sayfaya giriş yapmadan erişmeye çalışmak, dosya izinlerinin yanlış ayarlanmış olması ya da bir IP’nin kara listeye alınmış olması 403’e yol açar.

503 Service Unavailable: Sunucu şu an hizmet veremiyor. Bakım modu, aşırı yük, kaynak tükenmesi veya bir eklentinin siteyi çökertmesi bu hatanın en yaygın nedenleri. 503’ün SEO açısından çok önemli bir özelliği var: arama motorlarına “ben geçici olarak erişilemez durumdayım, tekrar gel” mesajı gönderiyor. Yani doğru kullanılırsa SEO’yu korur.

Neden functions.php Kullanıyoruz?

Birçok sysadmin “neden eklenti kullanmıyorsun?” diye sorar. Haklı bir soru. Ama şunu düşün: hata sayfaları için ayrı bir eklenti yüklemek, o eklentinin her sayfa yüklemesinde devreye girmesi demek. functions.php ile yaptığın işlemler doğrudan WordPress çekirdeğine entegre olur, gereksiz overhead olmaz ve tam kontrol sende kalır.

Öte yandan bakım modu gibi senaryolarda eklentiler devre dışı bile olabilir. functions.php her koşulda çalışır.

403 Hata Sayfası Oluşturma

Temel 403 Yönlendirme Fonksiyonu

İlk adım olarak functions.php‘ye şunu ekleyelim:

// functions.php
function custom_403_handler() {
    if ( is_user_logged_in() ) {
        return;
    }
    
    // Korumalı sayfa slug'larını tanımla
    $protected_pages = array( 'uye-alani', 'dashboard', 'hesabim' );
    
    if ( is_page( $protected_pages ) ) {
        global $wp_query;
        $wp_query->set_404(); // önce 404 resetle
        status_header( 403 );
        nocache_headers();
        include( get_template_directory() . '/403.php' );
        exit();
    }
}
add_action( 'template_redirect', 'custom_403_handler' );

Bu kod çalışınca WordPress, korumalı sayfalara giriş yapmamış kullanıcılar erişmeye çalıştığında 403.php şablonunu yükleyecek.

403.php Şablonunu Oluşturma

Temanın ana dizininde 403.php dosyasını oluştur:

<?php
// 403.php - Özel erişim engeli şablonu
get_header();
?>

<div class="error-container error-403">
    <div class="error-icon">🔒</div>
    <h1>Erişim Engellendi</h1>
    <p>Bu sayfayı görüntülemek için giriş yapmanız gerekiyor.</p>
    <p>Hesabınız yoksa <a href="<?php echo esc_url( wp_registration_url() ); ?>">buradan kayıt olabilirsiniz</a>.</p>
    <a href="<?php echo esc_url( wp_login_url( get_permalink() ) ); ?>" class="btn btn-primary">
        Giriş Yap
    </a>
    <a href="<?php echo esc_url( home_url('/') ); ?>" class="btn btn-secondary">
        Ana Sayfaya Dön
    </a>
</div>

<?php get_footer(); ?>

Rol Tabanlı 403 Kontrolü

Daha gelişmiş bir senaryo düşünelim: belirli kullanıcı rolleri bazı sayfalara erişebilir, bazıları erişemez. WooCommerce’li bir sitede toptan satış sayfalarını sadece “wholesaler” rolündeki kullanıcılara açmak istiyorsun diyelim:

function role_based_403_handler() {
    if ( ! is_page( 'toptan-satis' ) ) {
        return;
    }
    
    // Giriş yapmamış kullanıcıları yönlendir
    if ( ! is_user_logged_in() ) {
        status_header( 403 );
        nocache_headers();
        include( get_template_directory() . '/403.php' );
        exit();
    }
    
    // Giriş yapmış ama rolü uygun değilse
    $current_user = wp_get_current_user();
    $allowed_roles = array( 'wholesaler', 'administrator', 'editor' );
    
    $user_roles = $current_user->roles;
    $has_access = array_intersect( $allowed_roles, $user_roles );
    
    if ( empty( $has_access ) ) {
        status_header( 403 );
        nocache_headers();
        
        // Bu sefer farklı bir şablon kullanabiliriz
        include( get_template_directory() . '/403-no-permission.php' );
        exit();
    }
}
add_action( 'template_redirect', 'role_based_403_handler' );

Bu örnekte iki farklı 403 şablonu kullanıyoruz: biri “giriş yap” derken, diğeri “bu içeriğe erişim izniniz yok” diyor. Kullanıcı deneyimi açısından bu ayrım çok önemli.

IP Tabanlı 403 Engelleme

Güvenlik açısından bazı IP adreslerini engellemek isteyebilirsin. Bunu functions.php üzerinden yapabilirsin:

function ip_based_403_block() {
    $blocked_ips = array(
        '192.168.1.100',
        '10.0.0.50',
        // CIDR bloğu için ayrı bir kontrol gerekir
    );
    
    // Gerçek IP'yi al (proxy arkasında çalışıyorsa)
    $visitor_ip = '';
    
    if ( ! empty( $_SERVER['HTTP_CF_CONNECTING_IP'] ) ) {
        // Cloudflare kullananlar için
        $visitor_ip = sanitize_text_field( $_SERVER['HTTP_CF_CONNECTING_IP'] );
    } elseif ( ! empty( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) {
        $visitor_ip = sanitize_text_field( explode(',', $_SERVER['HTTP_X_FORWARDED_FOR'])[0] );
    } else {
        $visitor_ip = sanitize_text_field( $_SERVER['REMOTE_ADDR'] );
    }
    
    if ( in_array( $visitor_ip, $blocked_ips ) ) {
        status_header( 403 );
        nocache_headers();
        include( get_template_directory() . '/403-blocked.php' );
        exit();
    }
}
add_action( 'init', 'ip_based_403_block', 1 );

Burada init hook’unu template_redirect yerine kullandığımıza dikkat et. init daha erken çalışır ve bu tür güvenlik kontrollerinde tercih edilmeli.

503 Hata Sayfası: Bakım Modu

WordPress’in Yerleşik Bakım Modu Sorunu

WordPress’in kendi bakım modu (wp_maintenance_mode) oldukça basit ve özelleştirme imkanı sunmuyor. Üstelik çok temel bir HTML çıktısı veriyor. Bunu tamamen ele geçirelim.

function custom_maintenance_mode() {
    // Sadece bakım modu aktifse devam et
    if ( ! get_option('site_maintenance_mode', false) ) {
        return;
    }
    
    // Yöneticileri ve editörleri geçir
    if ( current_user_can( 'manage_options' ) ) {
        return;
    }
    
    // WordPress admin alanına erişimi engelleme (isteğe bağlı)
    if ( is_admin() && current_user_can( 'manage_options' ) ) {
        return;
    }
    
    // Login sayfasını açık bırak
    if ( $GLOBALS['pagenow'] === 'wp-login.php' ) {
        return;
    }
    
    // 503 başlığı gönder - Retry-After önemli!
    status_header( 503 );
    nocache_headers();
    header( 'Retry-After: 3600' ); // 1 saat sonra tekrar dene
    
    include( get_template_directory() . '/503.php' );
    exit();
}
add_action( 'init', 'custom_maintenance_mode', 9 );

Retry-After header’ı Google’a “1 saat sonra geri gel” demek için kullanılıyor. Bu SEO açısından kritik. Belirtmezsen arama motoru sitenin kalıcı olarak erişilemez olduğunu düşünebilir.

503.php Şablonunu Oluşturma

<?php
// 503.php - Bakım modu şablonu
// WordPress yüklenmiş olmayabilir, bu yüzden dikkatli ol
?>
<!DOCTYPE html>
<html lang="tr">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta name="robots" content="noindex, nofollow">
    <title>Bakım Modu | <?php echo get_bloginfo('name'); ?></title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            min-height: 100vh;
            display: flex;
            align-items: center;
            justify-content: center;
            color: white;
        }
        .container { text-align: center; padding: 2rem; max-width: 600px; }
        h1 { font-size: 3rem; margin-bottom: 1rem; }
        p { font-size: 1.2rem; opacity: 0.9; line-height: 1.6; }
        .maintenance-icon { font-size: 5rem; margin-bottom: 1.5rem; }
        .countdown { 
            background: rgba(255,255,255,0.2); 
            border-radius: 10px; 
            padding: 1rem 2rem; 
            margin: 2rem auto;
            display: inline-block;
        }
    </style>
</head>
<body>
    <div class="container">
        <div class="maintenance-icon">🔧</div>
        <h1>Bakım Yapılıyor</h1>
        <p>Sitemizi sizin için daha iyi hale getiriyoruz. Kısa süre içinde geri döneceğiz.</p>
        
        <?php
        $back_time = get_option('maintenance_back_time', '');
        if ( ! empty($back_time) ) : ?>
        <div class="countdown">
            <p>Tahmini bitiş: <strong><?php echo esc_html($back_time); ?></strong></p>
        </div>
        <?php endif; ?>
        
        <p style="margin-top: 2rem; font-size: 0.9rem; opacity: 0.7;">
            Sorularınız için: <?php echo antispambot( get_option('admin_email') ); ?>
        </p>
    </div>
</body>
</html>

Bakım Modunu Admin Panelinden Kontrol Etme

Her seferinde functions.php‘yi düzenlemek zahmetli. Bunu WordPress admin panelinden kontrol edebilir hale getirelim:

// Admin menüsüne bakım modu seçeneği ekle
function maintenance_mode_admin_menu() {
    add_options_page(
        'Bakım Modu',
        'Bakım Modu',
        'manage_options',
        'maintenance-mode',
        'maintenance_mode_settings_page'
    );
}
add_action( 'admin_menu', 'maintenance_mode_admin_menu' );

function maintenance_mode_settings_page() {
    if ( isset($_POST['save_maintenance_settings']) ) {
        check_admin_referer( 'maintenance_mode_nonce' );
        
        $mode = isset($_POST['maintenance_mode']) ? 1 : 0;
        $back_time = sanitize_text_field( $_POST['back_time'] ?? '' );
        
        update_option( 'site_maintenance_mode', $mode );
        update_option( 'maintenance_back_time', $back_time );
        
        echo '<div class="updated"><p>Ayarlar kaydedildi.</p></div>';
    }
    
    $is_active = get_option( 'site_maintenance_mode', false );
    $back_time = get_option( 'maintenance_back_time', '' );
    ?>
    <div class="wrap">
        <h1>Bakım Modu Ayarları</h1>
        <form method="post">
            <?php wp_nonce_field('maintenance_mode_nonce'); ?>
            <table class="form-table">
                <tr>
                    <th>Bakım Modu</th>
                    <td>
                        <label>
                            <input type="checkbox" name="maintenance_mode" 
                                <?php checked($is_active, 1); ?> value="1">
                            Aktif
                        </label>
                    </td>
                </tr>
                <tr>
                    <th>Tahmini Bitiş Zamanı</th>
                    <td>
                        <input type="text" name="back_time" 
                            value="<?php echo esc_attr($back_time); ?>" 
                            placeholder="Örn: 15 Ocak 2025, 14:00">
                    </td>
                </tr>
            </table>
            <?php submit_button( 'Kaydet', 'primary', 'save_maintenance_settings' ); ?>
        </form>
    </div>
    <?php
}

Gerçek Dünya Senaryoları

Senaryo 1: WooCommerce Güncellemesi Sırasında 503

WooCommerce büyük bir güncelleme alıyor ve veritabanı migrasyonu gerekiyor. Bu sürede siteni 503 moduna alman lazım. Ama ödeme işlemi yapan müşteriler ne olacak?

function woocommerce_update_maintenance() {
    // Güncelleme yapılıyor mu kontrol et
    if ( ! get_transient('woo_update_in_progress') ) {
        return;
    }
    
    // Checkout sayfası ve ödeme callback'lerini açık bırak
    $open_pages = array( 'checkout', 'order-received', 'cart' );
    
    // WooCommerce ödeme gateway callback'leri için
    if ( isset($_GET['wc-api']) ) {
        return;
    }
    
    if ( is_page($open_pages) || is_checkout() ) {
        return;
    }
    
    if ( current_user_can('manage_options') ) {
        return;
    }
    
    status_header(503);
    header('Retry-After: 1800');
    nocache_headers();
    include( get_template_directory() . '/503-woo-update.php' );
    exit();
}
add_action( 'template_redirect', 'woocommerce_update_maintenance' );

Bu kodda set_transient('woo_update_in_progress', true, 3600) ile bakım modunu geçici olarak açıp, güncelleme bitince delete_transient('woo_update_in_progress') ile kapatabilirsin.

Senaryo 2: Üyelik Sistemi ile Katmanlı 403

Bir online kurs sitesi düşün. “Temel”, “Pro” ve “Premium” üyelik paketlerin var. Her paketin erişebildiği içerikler farklı:

function course_access_403_handler() {
    if ( ! is_singular('course') ) {
        return;
    }
    
    if ( ! is_user_logged_in() ) {
        status_header(403);
        nocache_headers();
        set_query_var('error_type', 'not_logged_in');
        include( get_template_directory() . '/403-course.php' );
        exit();
    }
    
    $post_id = get_the_ID();
    $required_level = get_post_meta($post_id, '_required_membership', true);
    
    if ( empty($required_level) ) {
        return; // Serbest erişim
    }
    
    $user_id = get_current_user_id();
    $user_level = get_user_meta($user_id, 'membership_level', true);
    
    $levels = array( 'basic' => 1, 'pro' => 2, 'premium' => 3 );
    
    $user_level_num = $levels[$user_level] ?? 0;
    $required_level_num = $levels[$required_level] ?? 99;
    
    if ( $user_level_num < $required_level_num ) {
        status_header(403);
        nocache_headers();
        set_query_var('error_type', 'insufficient_level');
        set_query_var('required_level', $required_level);
        include( get_template_directory() . '/403-course.php' );
        exit();
    }
}
add_action( 'template_redirect', 'course_access_403_handler' );

Hata Sayfalarını Loglama

Hataları sadece göstermek değil, loglamak da önemli. Kim neye erişmeye çalışıyor, hangi IP’lerden 403 geliyor? Bunu takip etmek güvenlik açısından değerli:

function log_access_errors( $error_code, $user_id = 0 ) {
    if ( ! WP_DEBUG_LOG ) {
        return;
    }
    
    $log_data = array(
        'time'       => current_time('mysql'),
        'error'      => $error_code,
        'url'        => esc_url_raw( $_SERVER['REQUEST_URI'] ?? '' ),
        'ip'         => sanitize_text_field( $_SERVER['REMOTE_ADDR'] ?? '' ),
        'user_id'    => $user_id,
        'user_agent' => sanitize_text_field( $_SERVER['HTTP_USER_AGENT'] ?? '' ),
    );
    
    $log_message = sprintf(
        '[%s] %d Hatası - URL: %s - IP: %s - User: %d',
        $log_data['time'],
        $error_code,
        $log_data['url'],
        $log_data['ip'],
        $log_data['user_id']
    );
    
    error_log( $log_message );
    
    // Veritabanına da kaydedebiliriz
    $existing_logs = get_option('access_error_logs', array());
    array_unshift($existing_logs, $log_data);
    
    // Son 100 kaydı tut
    $existing_logs = array_slice($existing_logs, 0, 100);
    update_option('access_error_logs', $existing_logs);
}

Bu fonksiyonu hata şablonlarını yüklemeden önce çağırabilirsin.

Dikkat Edilmesi Gereken Noktalar

  • Cache ile çakışma: Eğer site önünde bir cache katmanı varsa (Varnish, Redis, Nginx FastCGI Cache) hata sayfaları cache’e alınmamalı. nocache_headers() fonksiyonu WordPress tarafındaki cache’i temizler ama sunucu tarafı cache için ayrıca konfigürasyon yapman gerekir.
  • CDN uyumu: Cloudflare gibi CDN’ler 503 hatalarını bazen kendi sayfalarıyla değiştirebilir. Cloudflare Page Rules ile bu davranışı override edebilirsin.
  • Child theme kullanımı: functions.php değişikliklerini her zaman child theme üzerinden yap. Tema güncellemelerinde değişikliklerin silinmemesi için bu şart.
  • exit() vs die() kullanımı: İkisi aynı işi yapıyor ama exit() daha okunabilir ve sysadmin dünyasında daha yaygın.
  • Header’ları doğru sırala: status_header() ve nocache_headers() çağrıları her zaman include() çağrısından önce gelmeli. Aksi halde “headers already sent” hatası alırsın.
  • WP-CLI ile bakım modu yönetimi: wp option update site_maintenance_mode 1 komutuyla sunucu tarafında, SSH üzerinden de bakım modunu açıp kapatabilirsin. Deployment scriptlerinde bu çok işe yarıyor.
  • Şablon dosyası bulunamazsa ne olur: Her zaman file_exists() kontrolü yap. Şablon dosyası yoksa PHP hatası alırsın ve bu hata normal sayfada gösterilir, son derece kötü görünür.

Sonuç

WordPress’te özel 403 ve 503 hata sayfaları oluşturmak, siteyi daha profesyonel göstermekten çok daha fazlası. Bu sayfalar doğru HTTP başlıkları gönderir, SEO’yu korur, kullanıcıyı yönlendirir ve güvenlik katmanı oluşturur. functions.php üzerinden bu kontrolü almanın sana verdiği esneklik, herhangi bir eklentinin verebileceğinden çok daha fazla.

Özellikle WooCommerce sitelerinde bakım modu yönetimi kritik. Yanlış yapılandırılmış bir 503 sayfası, ödeme callback’lerini engelleyebilir ve tamamlanmamış siparişlere yol açabilir. Bu yazıdaki WooCommerce senaryosundaki detayları göz ardı etme.

Son olarak şunu söyleyeyim: bu kodları production’a almadan önce staging ortamında test et. Özellikle init hook’unda çalışan kodlar tüm WordPress yükleme döngüsünü etkiler ve beklenmedik sonuçlar doğurabilir. Bir sysadmin olarak “önce test et, sonra deploy et” kuralı burada da geçerli.

Bir yanıt yazın

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