Özel Login Sayfası Tasarlayan WordPress Eklentisi Nasıl Yapılır
WordPress ile çalışan herkes bir noktada varsayılan /wp-login.php sayfasından bıkar. Hem görsel olarak berbat hem de güvenlik açısından açık bir hedef. Müşterilere teslim ettiğin bir projeye “admin paneline gir” dediğinde karşılarına çıkan o jenerik ekran, markaya hiç yakışmıyor. Bu yüzden bugün sıfırdan bir WordPress eklentisi yazacağız: özel bir login sayfası tasarlayan, güvenli, genişletilebilir bir çözüm.
Bunu bir eklenti olarak yazmak, temanıza bağlı kalmak yerine çok daha taşınabilir bir yapı sunar. Tema değişse bile login sayfanız ayakta kalır.
Eklentinin Temel Mimarisi
Önce klasör yapısını netleştirelim. Eklentimizin adı custom-login-page olacak ve şu yapıyı kullanacağız:
wp-content/plugins/custom-login-page/
├── custom-login-page.php # Ana eklenti dosyası
├── includes/
│ ├── class-login-handler.php # İşleyici sınıf
│ └── class-assets.php # CSS/JS yönetimi
├── templates/
│ └── login-template.php # Login sayfası şablonu
└── assets/
├── css/
│ └── login-style.css
└── js/
└── login-script.js
Bu yapı küçük görünebilir ama ciddi bir eklentinin temel iskeletini yansıtıyor. includes/ dizini PHP sınıflarını barındırırken templates/ görsel katmanı ayırıyor. Bu ayrım ilerleyen süreçte çok işinize yarayacak.
Ana Eklenti Dosyası
Başlayalım. Ana dosya WordPress’in eklentiyi tanıması için gerekli header bilgisini içermeli:
<?php
/**
* Plugin Name: Custom Login Page
* Plugin URI: https://example.com/custom-login
* Description: Markanıza özel bir WordPress giriş sayfası oluşturur.
* Version: 1.0.0
* Author: Sizin Adınız
* License: GPL-2.0+
* Text Domain: custom-login-page
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
define( 'CLP_VERSION', '1.0.0' );
define( 'CLP_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
define( 'CLP_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
require_once CLP_PLUGIN_DIR . 'includes/class-login-handler.php';
require_once CLP_PLUGIN_DIR . 'includes/class-assets.php';
function clp_init() {
$handler = new CLP_Login_Handler();
$handler->init();
$assets = new CLP_Assets();
$assets->init();
}
add_action( 'plugins_loaded', 'clp_init' );
ABSPATH kontrolü kritik. Bu satır olmadan eklenti dosyanıza doğrudan erişildiğinde PHP kodu çalışır, bu büyük bir güvenlik açığı. Bunu hiçbir zaman atlamayın.
Login Handler Sınıfı
Asıl iş burada yapılıyor. class-login-handler.php dosyası şu işleri üstleniyor: özel URL oluşturma, varsayılan login sayfasını yönlendirme ve kimlik doğrulama işlemi.
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class CLP_Login_Handler {
private $login_slug;
public function __construct() {
$this->login_slug = get_option( 'clp_login_slug', 'giris-yap' );
}
public function init() {
add_action( 'init', array( $this, 'add_login_rewrite_rule' ) );
add_action( 'template_redirect', array( $this, 'handle_login_page' ) );
add_filter( 'login_url', array( $this, 'custom_login_url' ), 10, 3 );
add_action( 'wp_login_failed', array( $this, 'handle_failed_login' ) );
add_action( 'init', array( $this, 'block_default_login' ) );
}
public function add_login_rewrite_rule() {
add_rewrite_rule(
'^' . $this->login_slug . '/?$',
'index.php?clp_login=1',
'top'
);
add_filter( 'query_vars', function( $vars ) {
$vars[] = 'clp_login';
return $vars;
});
}
public function handle_login_page() {
if ( get_query_var( 'clp_login' ) ) {
if ( is_user_logged_in() ) {
wp_redirect( admin_url() );
exit;
}
$this->process_login_form();
include CLP_PLUGIN_DIR . 'templates/login-template.php';
exit;
}
}
public function custom_login_url( $login_url, $redirect, $force_reauth ) {
$url = home_url( '/' . $this->login_slug . '/' );
if ( ! empty( $redirect ) ) {
$url = add_query_arg( 'redirect_to', urlencode( $redirect ), $url );
}
return $url;
}
public function block_default_login() {
$request_uri = isset( $_SERVER['REQUEST_URI'] )
? sanitize_text_field( $_SERVER['REQUEST_URI'] )
: '';
if ( strpos( $request_uri, 'wp-login.php' ) !== false
&& ! defined( 'DOING_AJAX' ) ) {
wp_redirect( home_url( '/' . $this->login_slug . '/' ) );
exit;
}
}
public function handle_failed_login( $username ) {
$redirect = isset( $_REQUEST['redirect_to'] )
? $_REQUEST['redirect_to']
: '';
wp_redirect(
add_query_arg(
'login',
'failed',
home_url( '/' . $this->login_slug . '/' )
)
);
exit;
}
private function process_login_form() {
if ( 'POST' !== $_SERVER['REQUEST_METHOD'] ) {
return;
}
if ( ! isset( $_POST['clp_nonce'] ) ||
! wp_verify_nonce( $_POST['clp_nonce'], 'clp_login_action' ) ) {
wp_die( 'Güvenlik doğrulaması başarısız.' );
}
$username = sanitize_user( $_POST['log'] ?? '' );
$password = $_POST['pwd'] ?? '';
$remember = isset( $_POST['rememberme'] );
$user = wp_signon( array(
'user_login' => $username,
'user_password' => $password,
'remember' => $remember,
), is_ssl() );
if ( is_wp_error( $user ) ) {
return;
}
$redirect_to = isset( $_POST['redirect_to'] )
? esc_url_raw( $_POST['redirect_to'] )
: admin_url();
wp_redirect( $redirect_to );
exit;
}
}
Burada dikkat edilmesi gereken birkaç nokta var. wp_verify_nonce kullanımı zorunlu, CSRF saldırılarına karşı temel korunmayı sağlar. sanitize_user ile kullanıcı adını temizliyoruz, ama şifreye dokunmuyoruz, çünkü WordPress wp_signon içinde zaten bunu hallediyor. Şifreyi sanitize etmek özel karakterlerin kırılmasına neden olabilir.
Assets Sınıfı
CSS ve JavaScript dosyalarını düzgün sırayla yüklemek için ayrı bir sınıf kullanmak iyi bir pratik:
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class CLP_Assets {
public function init() {
add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_login_assets' ) );
}
public function enqueue_login_assets() {
if ( ! get_query_var( 'clp_login' ) ) {
return;
}
wp_enqueue_style(
'clp-login-style',
CLP_PLUGIN_URL . 'assets/css/login-style.css',
array(),
CLP_VERSION
);
wp_enqueue_script(
'clp-login-script',
CLP_PLUGIN_URL . 'assets/js/login-script.js',
array( 'jquery' ),
CLP_VERSION,
true
);
wp_localize_script( 'clp-login-script', 'clpVars', array(
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'clp_ajax_nonce' ),
'messages' => array(
'emptyFields' => 'Kullanıcı adı ve şifre boş bırakılamaz.',
'loggingIn' => 'Giriş yapılıyor...',
),
) );
}
}
wp_localize_script burada kritik bir rol üstleniyor. JavaScript dosyanıza PHP tarafından üretilen nonce ve diğer değerleri aktarmanın doğru yolu bu. Hardcoded URL yazmak yerine her zaman bu yöntemi kullanın.
Login Template
Şimdi asıl görsel taraf. templates/login-template.php dosyası:
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
$login_failed = isset( $_GET['login'] ) && 'failed' === $_GET['login'];
$redirect_to = isset( $_GET['redirect_to'] ) ? esc_url( $_GET['redirect_to'] ) : admin_url();
?>
<!DOCTYPE html>
<html <?php language_attributes(); ?>>
<head>
<meta charset="<?php bloginfo( 'charset' ); ?>">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?php echo esc_html( get_bloginfo( 'name' ) ); ?> - Giriş</title>
<?php wp_head(); ?>
</head>
<body class="clp-login-body">
<div class="clp-login-wrapper">
<div class="clp-login-box">
<div class="clp-logo">
<?php
$custom_logo_id = get_theme_mod( 'custom_logo' );
if ( $custom_logo_id ) {
echo wp_get_attachment_image( $custom_logo_id, 'medium', false,
array( 'class' => 'clp-site-logo' ) );
} else {
echo '<h1>' . esc_html( get_bloginfo( 'name' ) ) . '</h1>';
}
?>
</div>
<?php if ( $login_failed ) : ?>
<div class="clp-error-message">
<p>Kullanıcı adı veya şifre hatalı. Lütfen tekrar deneyin.</p>
</div>
<?php endif; ?>
<form id="clp-login-form" method="post" action="">
<?php wp_nonce_field( 'clp_login_action', 'clp_nonce' ); ?>
<input type="hidden" name="redirect_to"
value="<?php echo esc_attr( $redirect_to ); ?>">
<div class="clp-form-group">
<label for="user_login">Kullanıcı Adı veya E-posta</label>
<input type="text"
id="user_login"
name="log"
class="clp-input"
autocomplete="username"
required>
</div>
<div class="clp-form-group">
<label for="user_pass">Şifre</label>
<input type="password"
id="user_pass"
name="pwd"
class="clp-input"
autocomplete="current-password"
required>
</div>
<div class="clp-form-group clp-remember">
<label>
<input type="checkbox" name="rememberme" value="forever">
Beni hatırla
</label>
</div>
<button type="submit" class="clp-submit-btn">
Giriş Yap
</button>
<div class="clp-links">
<a href="<?php echo esc_url( wp_lostpassword_url() ); ?>">
Şifremi Unuttum
</a>
</div>
</form>
</div>
</div>
<?php wp_footer(); ?>
</body>
</html>
wp_head() ve wp_footer() çağrılarına dikkat edin. Bu hooklar sayesinde diğer eklentiler de gerekli scriptlerini ve stillerini inject edebilir. Bunları çıkarmak bazen beklenmedik kırılmalara neden olur.
CSS ile Görsel Kimlik
assets/css/login-style.css dosyası için minimal ama etkili bir başlangıç:
*, *::before, *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body.clp-login-body {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.clp-login-wrapper {
width: 100%;
max-width: 420px;
padding: 20px;
}
.clp-login-box {
background: #ffffff;
border-radius: 12px;
padding: 40px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
}
.clp-logo {
text-align: center;
margin-bottom: 32px;
}
.clp-logo img.clp-site-logo {
max-width: 160px;
height: auto;
}
.clp-error-message {
background: #fff5f5;
border: 1px solid #fc8181;
border-radius: 8px;
padding: 12px 16px;
margin-bottom: 20px;
color: #c53030;
font-size: 14px;
}
.clp-form-group {
margin-bottom: 20px;
}
.clp-form-group label {
display: block;
margin-bottom: 6px;
font-size: 14px;
font-weight: 600;
color: #2d3748;
}
.clp-input {
width: 100%;
padding: 12px 16px;
border: 2px solid #e2e8f0;
border-radius: 8px;
font-size: 15px;
transition: border-color 0.2s ease;
outline: none;
}
.clp-input:focus {
border-color: #4299e1;
}
.clp-remember label {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
color: #4a5568;
cursor: pointer;
}
.clp-submit-btn {
width: 100%;
padding: 13px;
background: #4299e1;
color: white;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: background 0.2s ease;
margin-top: 8px;
}
.clp-submit-btn:hover {
background: #3182ce;
}
.clp-links {
text-align: center;
margin-top: 20px;
}
.clp-links a {
color: #4299e1;
font-size: 14px;
text-decoration: none;
}
.clp-links a:hover {
text-decoration: underline;
}
Aktivasyon ve Deaktivasyon Kancaları
Eklentinin rewrite kurallarının düzgün çalışması için aktivasyon anında flush yapılması gerekir:
<?php
// Bu kodu ana eklenti dosyasına ekleyin
register_activation_hook( __FILE__, 'clp_activate' );
register_deactivation_hook( __FILE__, 'clp_deactivate' );
function clp_activate() {
// Varsayılan seçenekleri kaydet
add_option( 'clp_login_slug', 'giris-yap' );
add_option( 'clp_redirect_after_login', '' );
// Rewrite kurallarını kaydet ve flush et
$handler = new CLP_Login_Handler();
$handler->add_login_rewrite_rule();
flush_rewrite_rules();
}
function clp_deactivate() {
flush_rewrite_rules();
// Seçenekleri temizlemiyoruz, kullanıcı veri kaybetmesin
}
Bu noktayı atlayan çok eklenti var. Rewrite kuralı eklediğinizde ama flush etmediğinizde login sayfanız 404 dönüyor. Bunu ilk kez yaşadığınızda saçlarınızı yolmak istiyorsunuz.
Güvenlik Katmanı: Brute Force Koruması
Özel login sayfası güvenlik için önemli bir adım ama yeterli değil. Basit bir rate limiting mekanizması ekleyelim:
<?php
// class-login-handler.php içindeki process_login_form metodundan önce çağırın
private function check_rate_limit( $username ) {
$ip = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
$transient_key = 'clp_attempts_' . md5( $ip . $username );
$attempts = (int) get_transient( $transient_key );
if ( $attempts >= 5 ) {
wp_die(
'Çok fazla başarısız giriş denemesi. 15 dakika sonra tekrar deneyin.',
'Erişim Engellendi',
array( 'response' => 429 )
);
}
return $attempts;
}
private function record_failed_attempt( $username ) {
$ip = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
$transient_key = 'clp_attempts_' . md5( $ip . $username );
$attempts = (int) get_transient( $transient_key );
set_transient( $transient_key, $attempts + 1, 15 * MINUTE_IN_SECONDS );
}
private function clear_rate_limit( $username ) {
$ip = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
$transient_key = 'clp_attempts_' . md5( $ip . $username );
delete_transient( $transient_key );
}
Bu çözüm production ortamı için temel bir koruma sağlıyor. Daha ciddi bir ortam için clp_attempts_ prefix’ini Redis veya Memcached destekli bir object cache ile kullanmak çok daha iyi performans verir. Shared hosting ortamında transient bazlı bu çözüm gayet iyi çalışıyor.
Yönetici Paneli Entegrasyonu
Login URL’sini özelleştirilebilir yapmak için basit bir ayar sayfası ekleyelim:
<?php
// Ana eklenti dosyasına eklenecek
add_action( 'admin_menu', 'clp_admin_menu' );
add_action( 'admin_init', 'clp_register_settings' );
function clp_admin_menu() {
add_options_page(
'Custom Login Page Ayarları',
'Custom Login',
'manage_options',
'custom-login-page',
'clp_settings_page'
);
}
function clp_register_settings() {
register_setting(
'clp_settings_group',
'clp_login_slug',
array(
'type' => 'string',
'sanitize_callback' => 'sanitize_title',
'default' => 'giris-yap',
)
);
}
function clp_settings_page() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
?>
<div class="wrap">
<h1>Custom Login Page Ayarları</h1>
<form method="post" action="options.php">
<?php settings_fields( 'clp_settings_group' ); ?>
<table class="form-table">
<tr>
<th>Giriş Sayfası URL'i</th>
<td>
<code><?php echo esc_url( home_url( '/' ) ); ?></code>
<input type="text"
name="clp_login_slug"
value="<?php echo esc_attr( get_option( 'clp_login_slug', 'giris-yap' ) ); ?>"
class="regular-text">
<p class="description">
Özel giriş sayfanızın URL slug'ı. Örnek: giris-yap
</p>
</td>
</tr>
</table>
<?php submit_button(); ?>
</form>
<div class="notice notice-info">
<p>Ayarları kaydettikten sonra
<a href="<?php echo admin_url( 'options-permalink.php' ); ?>">
Kalıcı Bağlantılar
</a>
sayfasını kaydedin.
</p>
</div>
</div>
<?php
}
Gerçek Dünya Senaryoları
Bu eklentiyi geliştirirken karşılaşacağınız birkaç pratik durumu paylaşayım:
WooCommerce uyumu: WooCommerce kendi hesap sayfasını kullanıyorsa woocommerce_login_redirect filtresini override etmeniz gerekebilir. Yoksa bazı kullanıcılar hala WooCommerce’in kendi akışına düşer.
Cache eklentileri: W3 Total Cache veya WP Rocket kullanıyorsanız login sayfanızı cache dışında bırakmanız şart. Aksi halde oturum açmış kullanıcılara da login formu gösterilir.
Multisite: Multisite kurulumda her site için ayrı slug tanımlamanız gerekir. Mevcut kodda get_option çağrısı zaten site bazlı çalışır ama block_default_login metodunu gözden geçirmeniz gerekecek.
XMLRPC ve REST API: wp-login.php‘yi bloke etmek bazı uygulamaların REST API kimlik doğrulamasını etkileyebilir. block_default_login metoduna REST_REQUEST sabiti kontrolü eklemenizi öneririm.
Eklentinin tamamını yazdıktan sonra Ayarlar > Kalıcı Bağlantılar sayfasına girip kaydedin. Bu, rewrite kurallarını refresh eder ve /giris-yap/ URL’iniz çalışmaya başlar.
Sonuç
Bu eklenti temel seviyede işlevsel ve üzerine inşa edilebilir bir yapı sunuyor. Birkaç yüz satır PHP ve CSS ile hem marka kimliğinize uyan hem de güvenliği artıran bir çözüm elde ettiniz.
Buradan ilerleyebileceğiniz birkaç yön var. Google reCAPTCHA entegrasyonu ekleyebilir, sosyal login desteği için bir OAuth kütüphanesi bağlayabilir veya login sayfasını tam anlamıyla bir no-index sayfası haline getirerek arama motorlarından gizleyebilirsiniz. Eklenti yapısı buna izin veriyor çünkü her şeyi sınıflara böldük.
En önemlisi, varsayılan wp-login.php artık özel sayfanıza yönlendiriyor. Bu tek başına botların %90’ını devre dışı bırakıyor çünkü onlar belirli URL’leri hedef alıyor. Güvenlik katmanlı bir oyun ve bu eklenti o katmanların önemli bir parçası.
