AJAX в WordPress: wp_ajax, admin-ajax.php и fetch API

Динамическая подгрузка контента, отправка форм без перезагрузки страницы, бесконечная прокрутка — всё это AJAX. В WordPress AJAX работает через специальный эндпоинт admin-ajax.php и систему хуков. Разберём полный цикл: от регистрации обработчика на сервере до отправки запроса из браузера.

Серверная часть: wp_ajax_ хуки

WordPress предоставляет два хука для AJAX-запросов:

  • wp_ajax_{action} — для авторизованных пользователей
  • wp_ajax_nopriv_{action} — для неавторизованных (гостей)

Если нужно обрабатывать запросы от всех посетителей, регистрируйте оба хука. Создадим обработчик, который возвращает записи по категории:

// functions.php или отдельный файл плагина
add_action( 'wp_ajax_load_posts', 'ajax_load_posts_handler' );
add_action( 'wp_ajax_nopriv_load_posts', 'ajax_load_posts_handler' );

function ajax_load_posts_handler() {
    // 1. Проверяем nonce
    if ( ! check_ajax_referer( 'load_posts_nonce', 'security', false ) ) {
        wp_send_json_error( array(
            'message' => 'Ошибка безопасности',
        ), 403 );
    }

    // 2. Получаем и валидируем параметры
    $category = isset( $_POST['category'] ) ? absint( $_POST['category'] ) : 0;
    $page     = isset( $_POST['page'] ) ? absint( $_POST['page'] ) : 1;

    if ( ! $category ) {
        wp_send_json_error( array(
            'message' => 'Категория не указана',
        ), 400 );
    }

    // 3. Запрос записей
    $query = new WP_Query( array(
        'cat'            => $category,
        'paged'          => $page,
        'posts_per_page' => 6,
        'post_status'    => 'publish',
    ) );

    if ( ! $query->have_posts() ) {
        wp_send_json_error( array(
            'message' => 'Записей не найдено',
        ), 404 );
    }

    // 4. Формируем HTML
    ob_start();
    while ( $query->have_posts() ) {
        $query->the_post();
        get_template_part( 'template-parts/content', 'card' );
    }
    $html = ob_get_clean();
    wp_reset_postdata();

    // 5. Отправляем ответ
    wp_send_json_success( array(
        'html'      => $html,
        'max_pages' => $query->max_num_pages,
        'found'     => $query->found_posts,
    ) );
}

Функции wp_send_json_success и wp_send_json_error автоматически устанавливают заголовок Content-Type: application/json, оборачивают данные в стандартную структуру { success: true/false, data: ... } и завершают выполнение скрипта через wp_die().

Подключение скрипта и передача данных

Для отправки AJAX-запроса на фронтенде нужно знать URL эндпоинта и nonce. Передаём их через wp_localize_script:

add_action( 'wp_enqueue_scripts', 'ajax_demo_enqueue' );

function ajax_demo_enqueue() {
    wp_enqueue_script(
        'ajax-load-posts',
        get_stylesheet_directory_uri() . '/js/load-posts.js',
        array(),   // без зависимостей, используем fetch
        '1.0.0',
        true
    );

    wp_localize_script( 'ajax-load-posts', 'AjaxLoadPosts', array(
        'url'      => admin_url( 'admin-ajax.php' ),
        'nonce'    => wp_create_nonce( 'load_posts_nonce' ),
        'loading'  => 'Загрузка...',
        'no_more'  => 'Больше записей нет',
    ) );
}

Клиентская часть: fetch API

Современный способ отправки AJAX-запросов — fetch API. Он встроен во все современные браузеры и не требует jQuery:

// js/load-posts.js
document.addEventListener( 'DOMContentLoaded', function() {
    const container = document.getElementById( 'posts-container' );
    const loadBtn   = document.getElementById( 'load-more' );

    if ( ! loadBtn || ! container ) return;

    let currentPage = 1;

    loadBtn.addEventListener( 'click', function() {
        const category = this.dataset.category;

        // Блокируем кнопку
        loadBtn.disabled = true;
        loadBtn.textContent = AjaxLoadPosts.loading;

        // Формируем данные
        const formData = new FormData();
        formData.append( 'action', 'load_posts' );      // имя AJAX-действия
        formData.append( 'security', AjaxLoadPosts.nonce ); // nonce
        formData.append( 'category', category );
        formData.append( 'page', currentPage + 1 );

        // Отправляем запрос
        fetch( AjaxLoadPosts.url, {
            method: 'POST',
            credentials: 'same-origin',  // отправлять куки
            body: formData,
        } )
        .then( function( response ) {
            if ( ! response.ok ) {
                throw new Error( 'HTTP ' + response.status );
            }
            return response.json();
        } )
        .then( function( result ) {
            if ( result.success ) {
                // Вставляем HTML
                container.insertAdjacentHTML( 'beforeend', result.data.html );
                currentPage++;

                // Скрываем кнопку, если записи кончились
                if ( currentPage >= result.data.max_pages ) {
                    loadBtn.textContent = AjaxLoadPosts.no_more;
                    loadBtn.disabled = true;
                    return;
                }
            } else {
                console.error( 'Error:', result.data.message );
            }
        } )
        .catch( function( error ) {
            console.error( 'Fetch error:', error );
            alert( 'Произошла ошибка при загрузке' );
        } )
        .finally( function() {
            if ( currentPage < parseInt( loadBtn.dataset.maxPages ) ) {
                loadBtn.disabled = false;
                loadBtn.textContent = 'Загрузить ещё';
            }
        } );
    } );
} );

HTML-разметка для кнопки:

<div id="posts-container">
    <!-- Записи выводятся здесь -->
</div>

<button id="load-more"
        data-category="5"
        data-max-pages="<?php echo esc_attr( $query->max_num_pages ); ?>">
    Загрузить ещё
</button>

Альтернатива: jQuery.ajax

Если на сайте уже загружен jQuery (а в WordPress он есть по умолчанию), можно использовать jQuery.ajax. Этот подход до сих пор встречается в большинстве туториалов:

jQuery( document ).ready( function( $ ) {
    $( '#load-more' ).on( 'click', function() {
        var $btn = $( this );

        $.ajax( {
            url:  AjaxLoadPosts.url,
            type: 'POST',
            data: {
                action:   'load_posts',
                security: AjaxLoadPosts.nonce,
                category: $btn.data( 'category' ),
                page:     currentPage + 1,
            },
            beforeSend: function() {
                $btn.prop( 'disabled', true ).text( AjaxLoadPosts.loading );
            },
            success: function( result ) {
                if ( result.success ) {
                    $( '#posts-container' ).append( result.data.html );
                    currentPage++;
                }
            },
            error: function( xhr, status, error ) {
                console.error( 'AJAX error:', status, error );
            },
            complete: function() {
                $btn.prop( 'disabled', false ).text( 'Загрузить ещё' );
            },
        } );
    } );
} );

Выбор между fetch и jQuery.ajax зависит от проекта. Если jQuery уже загружен — используйте его. Если стремитесь к минимализму и не нужна поддержка IE — берите fetch.

Отправка формы через AJAX

Частая задача — отправка формы обратной связи без перезагрузки страницы. Серверная часть:

add_action( 'wp_ajax_submit_contact', 'handle_contact_form' );
add_action( 'wp_ajax_nopriv_submit_contact', 'handle_contact_form' );

function handle_contact_form() {
    check_ajax_referer( 'contact_form_nonce', 'security' );

    $name    = sanitize_text_field( $_POST['name'] ?? '' );
    $email   = sanitize_email( $_POST['email'] ?? '' );
    $message = sanitize_textarea_field( $_POST['message'] ?? '' );

    // Валидация
    $errors = array();
    if ( empty( $name ) )    $errors[] = 'Укажите имя';
    if ( ! is_email( $email ) ) $errors[] = 'Неверный email';
    if ( empty( $message ) ) $errors[] = 'Напишите сообщение';

    if ( $errors ) {
        wp_send_json_error( array( 'errors' => $errors ), 422 );
    }

    // Отправка письма
    $to      = get_option( 'admin_email' );
    $subject = 'Сообщение с сайта от ' . $name;
    $body    = "Имя: {$name}\nEmail: {$email}\n\n{$message}";
    $headers = array( 'Reply-To: ' . $email );

    $sent = wp_mail( $to, $subject, $body, $headers );

    if ( $sent ) {
        wp_send_json_success( array(
            'message' => 'Сообщение отправлено!',
        ) );
    } else {
        wp_send_json_error( array(
            'message' => 'Ошибка отправки. Попробуйте позже.',
        ), 500 );
    }
}

Клиентская часть с обработкой ошибок валидации:

document.getElementById( 'contact-form' ).addEventListener( 'submit', function( e ) {
    e.preventDefault();

    const form     = this;
    const formData = new FormData( form );
    formData.append( 'action', 'submit_contact' );
    formData.append( 'security', ContactForm.nonce );

    const statusEl = document.getElementById( 'form-status' );
    statusEl.textContent = 'Отправка...';
    statusEl.className = 'form-status sending';

    fetch( ContactForm.url, {
        method: 'POST',
        credentials: 'same-origin',
        body: formData,
    } )
    .then( res => res.json() )
    .then( function( result ) {
        if ( result.success ) {
            statusEl.textContent = result.data.message;
            statusEl.className = 'form-status success';
            form.reset();
        } else {
            // Показываем ошибки валидации
            var msg = result.data.errors
                ? result.data.errors.join( ', ' )
                : result.data.message;
            statusEl.textContent = msg;
            statusEl.className = 'form-status error';
        }
    } )
    .catch( function() {
        statusEl.textContent = 'Ошибка сети. Проверьте соединение.';
        statusEl.className = 'form-status error';
    } );
} );

Безопасность AJAX-запросов

Три обязательных правила для любого AJAX-обработчика:

  • Nonce — всегда проверяйте через check_ajax_referer. Без nonce злоумышленник может подделать запрос от имени пользователя.
  • Санитизацияsanitize_text_field, absint, sanitize_email. Никогда не используйте $_POST напрямую в SQL-запросах или выводе.
  • Проверка прав — если действие требует авторизации, проверяйте current_user_can(). Хук wp_ajax_ гарантирует только авторизацию, но не конкретную роль.

Отладка AJAX

AJAX-запросы сложнее отлаживать, потому что ошибки PHP не видны в браузере. Несколько приёмов:

// Включите WP_DEBUG в wp-config.php
define( 'WP_DEBUG', true );
define( 'WP_DEBUG_LOG', true );      // логи в wp-content/debug.log
define( 'WP_DEBUG_DISPLAY', false ); // не показывать в HTML

// В обработчике используйте error_log для отладки
function ajax_debug_handler() {
    error_log( 'AJAX called. POST data: ' . print_r( $_POST, true ) );

    // Для быстрой отладки — верните raw-данные
    wp_send_json( array(
        'post_data' => $_POST,
        'user_id'   => get_current_user_id(),
        'is_admin'  => is_admin(), // в AJAX всегда true!
    ) );
}

// Проверяйте в DevTools:
// Network tab → фильтр XHR → клик на запрос → Response

Важный нюанс: функция is_admin() в контексте AJAX всегда возвращает true, потому что admin-ajax.php находится в директории wp-admin. Не используйте её для проверки, является ли пользователь администратором — для этого есть current_user_can( 'manage_options' ).

REST API как альтернатива admin-ajax.php

Начиная с WordPress 4.7, для AJAX-подобных задач можно использовать REST API. Он работает быстрее admin-ajax.php, потому что загружает меньше ядра WordPress. Но admin-ajax.php по-прежнему актуален для простых задач и обратной совместимости. Выбирайте REST API для новых проектов, особенно если строите SPA или работаете с мобильным приложением.

AJAX в WordPress — это связка серверного обработчика и клиентского скрипта. Серверная часть — PHP-функция с проверками безопасности. Клиентская — fetch или jQuery.ajax с правильной обработкой ошибок. Освоив эту связку, вы сможете создавать интерактивные интерфейсы без перезагрузки страницы.