WordPress’te Sayfalama Özelleştirme: functions.php ile Pagination Yönetimi
WordPress projelerinde sayfalama, ziyaretçi deneyimini doğrudan etkileyen ama çoğu zaman “yeterince çalışıyor” diyerek geçiştirilen bir konudur. Oysa varsayılan the_posts_pagination() fonksiyonu çoğu durumda ihtiyacı karşılamaz. Özel sorgular, WooCommerce ürün listeleri, AJAX tabanlı filtreleme ya da belirli bir sayfa tasarımı için sayfalamayı sıfırdan kontrol etmeniz gerekebilir. Bu yazıda functions.php üzerinden sayfalamayı nasıl özelleştireceğinizi, performanslı ve esnek çözümler nasıl üreteceğinizi ele alacağız.
WordPress Sayfalama Nasıl Çalışır?
WordPress’in sayfalama mekanizması temel olarak iki kavram üzerine kuruludur: sorgu nesnesi ve rewrite kuralları. Bir sayfa yüklendiğinde WP_Query o anki URL’yi parse ederek paged parametresini tespit eder. Örneğin /blog/page/3/ URL’si için paged değeri 3 olur.
Ana sorguda bu değer $wp_query->query_vars['paged'] üzerinden gelir. Özel sorgu yazarken ise bunu manuel olarak geçmeniz gerekir. Bu ince farkı atlamak, “sayfalama çalışmıyor” şikayetlerinin %80’inin kaynağıdır.
# Hangi query var'larının geldiğini debug etmek için wp-config.php'ye ekleyin
define('WP_DEBUG', true);
define('WP_DEBUG_LOG', true);
Sayfa URL yapısı da önemlidir. Settings > Permalinks kısmında “Post name” gibi bir yapı seçilmemiş ve hala ?p=123 formatı kullanılıyorsa sayfalama ?paged=2 şeklinde query string ile çalışır. Bu her zaman sorunsuz olmaz, özellikle caching layer varsa.
Temel Özel Sayfalama Fonksiyonu
Hemen işe koyulalım. Aşağıdaki fonksiyon, functions.php‘ye eklenerek herhangi bir template’te kullanabileceğiniz, sayı bazlı bir sayfalama üretir.
<?php
function custom_pagination( $query = null, $args = [] ) {
if ( null === $query ) {
global $wp_query;
$query = $wp_query;
}
$total_pages = isset( $query->max_num_pages ) ? $query->max_num_pages : 1;
if ( $total_pages <= 1 ) {
return;
}
$current = max( 1, get_query_var( 'paged' ) );
$defaults = [
'mid_size' => 2,
'prev_text' => '« Önceki',
'next_text' => 'Sonraki »',
'screen_reader_text' => 'Sayfa navigasyonu',
'type' => 'list',
'total' => $total_pages,
'current' => $current,
];
$args = wp_parse_args( $args, $defaults );
echo '<nav class="custom-pagination" aria-label="' . esc_attr( $args['screen_reader_text'] ) . '">';
echo paginate_links( $args );
echo '</nav>';
}
Bu fonksiyon template dosyanızda şu şekilde çağrılır:
<?php custom_pagination(); ?>
Özel bir WP_Query nesnesi varsa:
<?php
$custom_query = new WP_Query([
'post_type' => 'portfolio',
'posts_per_page' => 9,
'paged' => get_query_var( 'paged' ) ?: 1,
]);
if ( $custom_query->have_posts() ) :
while ( $custom_query->have_posts() ) : $custom_query->the_post();
// içerik
endwhile;
custom_pagination( $custom_query );
wp_reset_postdata();
endif;
Önceki/Sonraki Tarz Sayfalama
Bazı blog tasarımlarında sayı listesi yerine sadece “Daha Eski Yazılar / Daha Yeni Yazılar” tarzı bir navigasyon istenir. Bu hem daha minimal görünür hem de mobilde daha kullanışlıdır.
<?php
function prev_next_pagination( $args = [] ) {
$defaults = [
'prev_text' => '<span class="arrow">←</span> Daha Yeni Yazılar',
'next_text' => 'Daha Eski Yazılar <span class="arrow">→</span>',
'mid_size' => 0,
'end_size' => 0,
'type' => 'plain',
];
$args = wp_parse_args( $args, $defaults );
$links = paginate_links( $args );
if ( ! $links ) {
return;
}
// Sadece prev/next linklerini al, sayıları filtrele
$links = preg_replace( '/<a[^>]+>d+</a>/', '', $links );
$links = preg_replace( '/<span[^>]*class="[^"]*current[^"]*"[^>]*>d+</span>/', '', $links );
$links = trim( $links );
if ( ! empty( $links ) ) {
echo '<nav class="simple-pagination">' . $links . '</nav>';
}
}
Bu yaklaşım özellikle Single Column blog layout kullanan temalarda çok temiz durur.
AJAX ile Sonsuz Scroll Sayfalama
Modern sitelerde “daha fazla yükle” butonu veya sonsuz scroll oldukça yaygın. Bunu functions.php‘de AJAX handler yazarak uygulayalım.
Önce AJAX action’ı kaydedelim:
<?php
function register_load_more_scripts() {
wp_enqueue_script(
'load-more',
get_template_directory_uri() . '/js/load-more.js',
[ 'jquery' ],
'1.0.0',
true
);
wp_localize_script( 'load-more', 'loadMoreParams', [
'ajaxurl' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'load_more_nonce' ),
'post_type' => 'post',
'per_page' => get_option( 'posts_per_page' ),
]);
}
add_action( 'wp_enqueue_scripts', 'register_load_more_scripts' );
function handle_load_more_ajax() {
check_ajax_referer( 'load_more_nonce', 'nonce' );
$page = isset( $_POST['page'] ) ? absint( $_POST['page'] ) : 1;
$post_type = isset( $_POST['post_type'] ) ? sanitize_text_field( $_POST['post_type'] ) : 'post';
$per_page = isset( $_POST['per_page'] ) ? absint( $_POST['per_page'] ) : get_option( 'posts_per_page' );
$query = new WP_Query([
'post_type' => $post_type,
'posts_per_page' => $per_page,
'paged' => $page,
'post_status' => 'publish',
]);
if ( ! $query->have_posts() ) {
wp_send_json_error( [ 'message' => 'Gösterilecek içerik kalmadı.' ] );
return;
}
ob_start();
while ( $query->have_posts() ) : $query->the_post();
get_template_part( 'template-parts/content', get_post_type() );
endwhile;
$html = ob_get_clean();
wp_reset_postdata();
wp_send_json_success([
'html' => $html,
'has_more' => $page < $query->max_num_pages,
'total_pages' => $query->max_num_pages,
]);
}
add_action( 'wp_ajax_load_more_posts', 'handle_load_more_ajax' );
add_action( 'wp_ajax_nopriv_load_more_posts', 'handle_load_more_ajax' );
JavaScript tarafı (load-more.js):
jQuery(document).ready(function ($) {
var currentPage = 1;
var isLoading = false;
$('#load-more-btn').on('click', function () {
if (isLoading) return;
isLoading = true;
currentPage++;
var $btn = $(this);
$btn.text('Yükleniyor...').prop('disabled', true);
$.ajax({
url: loadMoreParams.ajaxurl,
type: 'POST',
data: {
action: 'load_more_posts',
nonce: loadMoreParams.nonce,
page: currentPage,
post_type: loadMoreParams.post_type,
per_page: loadMoreParams.per_page,
},
success: function (response) {
if (response.success) {
$('#posts-container').append(response.data.html);
if (!response.data.has_more) {
$btn.remove();
} else {
$btn.text('Daha Fazla Göster').prop('disabled', false);
}
} else {
$btn.remove();
}
},
error: function () {
$btn.text('Hata oluştu, tekrar deneyin').prop('disabled', false);
},
complete: function () {
isLoading = false;
},
});
});
});
Bu yapıda nonce kontrolü kritik öneme sahiptir. wp_create_nonce ve check_ajax_referer kombinasyonu olmadan CSRF saldırılarına kapı açarsınız.
WooCommerce Ürün Sayfalama Özelleştirme
WooCommerce kendi sayfalama sistemini kullanır, ancak bunu filtreleyerek değiştirebilirsiniz. Örneğin ürün sayfasında sayfalama görünümünü tamamen özelleştirmek istiyorsanız:
<?php
// WooCommerce varsayılan sayfalama çıktısını kaldır
remove_action( 'woocommerce_after_shop_loop', 'woocommerce_pagination', 10 );
// Özel sayfalama ekle
add_action( 'woocommerce_after_shop_loop', 'custom_woocommerce_pagination', 10 );
function custom_woocommerce_pagination() {
global $wp_query;
$total_pages = $wp_query->max_num_pages;
if ( $total_pages <= 1 ) {
return;
}
$current = max( 1, get_query_var( 'paged' ) );
$base = esc_url_raw( str_replace( 999999999, '%#%', get_pagenum_link( 999999999 ) ) );
$args = [
'base' => $base,
'format' => '?paged=%#%',
'current' => $current,
'total' => $total_pages,
'mid_size' => 2,
'end_size' => 1,
'prev_text' => '<svg viewBox="0 0 24 24"><path d="M15 18l-6-6 6-6"/></svg>',
'next_text' => '<svg viewBox="0 0 24 24"><path d="M9 18l6-6-6-6"/></svg>',
'type' => 'array',
];
$pages = paginate_links( $args );
if ( is_array( $pages ) ) {
echo '<nav class="woo-pagination">';
echo '<ul class="page-numbers">';
foreach ( $pages as $page ) {
echo '<li>' . $page . '</li>';
}
echo '</ul>';
echo '</nav>';
}
}
Ayrıca WooCommerce’in sayfa başına ürün sayısını kullanıcının kendisinin değiştirebilmesine izin verebilirsiniz:
<?php
function custom_products_per_page() {
$default = 12;
$per_page = isset( $_COOKIE['products_per_page'] ) ? absint( $_COOKIE['products_per_page'] ) : $default;
$allowed = [ 12, 24, 48 ];
return in_array( $per_page, $allowed ) ? $per_page : $default;
}
add_filter( 'loop_shop_per_page', 'custom_products_per_page', 20 );
// Cookie'yi set eden AJAX handler
function set_products_per_page() {
$per_page = absint( $_POST['per_page'] );
$allowed = [ 12, 24, 48 ];
if ( in_array( $per_page, $allowed ) ) {
setcookie( 'products_per_page', $per_page, time() + ( 30 * DAY_IN_SECONDS ), '/' );
wp_send_json_success();
} else {
wp_send_json_error( 'Geçersiz değer.' );
}
}
add_action( 'wp_ajax_set_products_per_page', 'set_products_per_page' );
add_action( 'wp_ajax_nopriv_set_products_per_page', 'set_products_per_page' );
Sayfalama URL Yapısını Özelleştirme
Varsayılan olarak WordPress /page/2/ yapısını kullanır. Bunu değiştirmek için rewrite API’sini kullanabilirsiniz. Örneğin bir custom post type için /urunler/sayfa/2/ yapmak istiyorsanız:
<?php
function register_custom_portfolio_type() {
$rewrite = [
'slug' => 'portfolyo',
'with_front' => true,
'pages' => true, // Bu satır /page/ desteğini açar
'feeds' => false,
];
register_post_type( 'portfolio', [
'label' => 'Portfolyo',
'public' => true,
'rewrite' => $rewrite,
'supports' => [ 'title', 'editor', 'thumbnail' ],
'has_archive' => 'portfolyo',
]);
}
add_action( 'init', 'register_custom_portfolio_type' );
Post type kaydedildikten sonra Settings > Permalinks sayfasını bir kez kaydedin. Bu işlem rewrite kurallarını yeniler. Bunu kod üzerinden yapmak istiyorsanız:
<?php
// Tema aktif edildiğinde rewrite kurallarını flush et
function flush_rewrite_on_activation() {
register_custom_portfolio_type();
flush_rewrite_rules();
}
register_activation_hook( __FILE__, 'flush_rewrite_on_activation' );
// Tema devre dışı bırakıldığında temizle
function flush_rewrite_on_deactivation() {
unregister_post_type( 'portfolio' );
flush_rewrite_rules();
}
register_deactivation_hook( __FILE__, 'flush_rewrite_on_deactivation' );
Dikkat: flush_rewrite_rules() her sayfa yüklemesinde çağrılmamalıdır. Bu fonksiyon oldukça ağır bir işlemdir ve veritabanına yazma yapar. Sadece tema/plugin aktifleştirme hookları içinde kullanın.
Sayfalama ile İlgili Yaygın Sorunlar ve Çözümleri
Sayfa 2 404 Hatası Veriyor
Bu sorunu büyük ihtimalle ana sorgu posts_per_page değerini override ettiğiniz zaman yaşarsınız. Özel sorgularda paged parametresini doğru geçmemeniz de bu hataya yol açar.
<?php
// YANLIS kullanim
$args = [
'post_type' => 'post',
'posts_per_page' => 5,
'paged' => $_GET['paged'] ?? 1, // Doğrudan $_GET kullanmayın
];
// DOGRU kullanim
$paged = ( get_query_var( 'paged' ) ) ? get_query_var( 'paged' ) : 1;
$args = [
'post_type' => 'post',
'posts_per_page' => 5,
'paged' => $paged,
];
Statik ön sayfa kullandığınızda ise paged yerine page query var’ı kullanılır:
<?php
// Statik ön sayfa için doğru paged değeri alma
$paged = get_query_var( 'paged' ) ? get_query_var( 'paged' ) : get_query_var( 'page' );
$paged = $paged ?: 1;
pre_get_posts ile Ana Sorguyu Değiştirme
Ana sorgu üzerinde değişiklik yapmak için pre_get_posts hook’unu kullanmak, WP_Query yazmaktan çok daha performanslıdır. Çünkü veritabanı sorgusu sadece bir kez yapılır.
<?php
function modify_main_query( $query ) {
// Admin panelini ve ana sorgu dışındakileri atla
if ( is_admin() || ! $query->is_main_query() ) {
return;
}
// Blog sayfasında sayfa başına post sayısını değiştir
if ( $query->is_home() ) {
$query->set( 'posts_per_page', 6 );
$query->set( 'orderby', 'date' );
$query->set( 'order', 'DESC' );
}
// Kategori sayfalarında yazar bazlı filtreleme
if ( $query->is_category() ) {
$query->set( 'posts_per_page', 9 );
}
// Custom post type archive
if ( $query->is_post_type_archive( 'portfolio' ) ) {
$query->set( 'posts_per_page', 12 );
$query->set( 'meta_key', '_portfolio_order' );
$query->set( 'orderby', 'meta_value_num' );
$query->set( 'order', 'ASC' );
}
}
add_action( 'pre_get_posts', 'modify_main_query' );
Bu yaklaşımın güzel yanı sayfalama için herhangi bir ek konfigürasyon gerektirmemesidir. WordPress ana sorgu değiştiğinde sayfalama otomatik olarak doğru hesaplanır.
SEO Dostu Sayfalama
Google, sayfalı içerikleri doğru indeksleyebilmesi için canonical URL’leri ve rel="next/prev" linklerini bekler. Artık modern SEO yaklaşımına göre rel="next/prev" deprecate edilmiş olsa da canonical URL’ler hala kritik öneme sahiptir.
<?php
function add_pagination_meta_tags() {
if ( ! is_singular() ) {
global $wp_query;
$current_page = max( 1, get_query_var( 'paged' ) );
$total_pages = $wp_query->max_num_pages;
// Canonical URL
$canonical = get_pagenum_link( $current_page );
echo '<link rel="canonical" href="' . esc_url( $canonical ) . '" />' . "n";
// Prev link
if ( $current_page > 1 ) {
$prev_url = get_pagenum_link( $current_page - 1 );
echo '<link rel="prev" href="' . esc_url( $prev_url ) . '" />' . "n";
}
// Next link
if ( $current_page < $total_pages ) {
$next_url = get_pagenum_link( $current_page + 1 );
echo '<link rel="next" href="' . esc_url( $next_url ) . '" />' . "n";
}
}
}
add_action( 'wp_head', 'add_pagination_meta_tags' );
Yoast SEO veya RankMath gibi bir SEO eklentisi kullanıyorsanız bu kısmı onlara bırakabilirsiniz. Çakışma olmaması için sadece SEO eklentisi aktif değilken bu fonksiyonu eklemek mantıklıdır.
Sayfalama Performans Optimizasyonu
Büyük sitelerde sayfalama sorguları yavaşlayabilir. Özellikle COUNT(*) sorguları ciddi yük oluşturur. Eğer toplam sayfa sayısını bilmeniz gerekmiyorsa no_found_rows parametresini kullanın:
<?php
// Sayfa sayısı hesaplamaya gerek yoksa COUNT sorgusunu atla
$query = new WP_Query([
'post_type' => 'post',
'posts_per_page' => 10,
'paged' => $paged,
'no_found_rows' => true, // max_num_pages hesaplanmaz, sayfalama çalışmaz
]);
// Sayfalama gerekiyorsa bu parametreyi kullanmayın
// Sadece sonsuz scroll veya "daha fazla yükle" butonlarında işe yarar
// çünkü toplam sayfa sayısını zaten bilmiyorsunuz
Cache stratejisi için Transient API’yi kullanabilirsiniz:
<?php
function get_cached_paginated_posts( $page = 1, $per_page = 10 ) {
$cache_key = 'paginated_posts_' . $page . '_' . $per_page;
$cached = get_transient( $cache_key );
if ( false !== $cached ) {
return $cached;
}
$query = new WP_Query([
'post_type' => 'post',
'posts_per_page' => $per_page,
'paged' => $page,
'post_status' => 'publish',
]);
$result = [
'posts' => $query->posts,
'total_pages' => $query->max_num_pages,
'total_posts' => $query->found_posts,
];
wp_reset_postdata();
// 5 dakika cache
set_transient( $cache_key, $result, 5 * MINUTE_IN_SECONDS );
return $result;
}
// Yeni post yayınlandığında cache'i temizle
function clear_pagination_cache( $post_id ) {
if ( get_post_status( $post_id ) === 'publish' ) {
// Tüm sayfalama cache'lerini temizle
global $wpdb;
$wpdb->query( "DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_paginated_posts_%'" );
$wpdb->query( "DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_timeout_paginated_posts_%'" );
}
}
add_action( 'save_post', 'clear_pagination_cache' );
Sonuç
WordPress’te sayfalama özelleştirmesi ilk bakışta basit gibi görünse de derinleştikçe birçok katmanı olan bir konu olduğunu görürsünüz. paginate_links() fonksiyonunu doğru parametrelerle kullanmak, pre_get_posts hookunu ana sorgu optimizasyonu için tercih etmek ve AJAX tabanlı sayfalamada nonce güvenliğini atlamadan uygulamak, sağlam bir sayfalama altyapısının temel taşlarıdır.
Özellikle WooCommerce projelerinde varsayılan sayfalama yetersiz kalır ve kullanıcı deneyimi açısından ciddi kayıplar yaşanabilir. Bu yazıdaki örnekleri kendi projenizin ihtiyaçlarına göre uyarlayarak hem performanslı hem de kullanıcı dostu bir deneyim oluşturabilirsiniz.
Son olarak şunu hatırlatayım: flush_rewrite_rules() ve cache temizleme işlemlerini her zaman doğru hooklar içinde, doğru zamanda çalıştırın. Yanlış yerde çağrılan bu fonksiyonlar sitenizi yavaşlatmakla kalmaz, zaman zaman tahmin edilmesi güç hatalara da neden olabilir. Test ortamında deneyin, production’a emin olduğunuzda alın.
