Динамическая подгрузка контента, отправка форм без перезагрузки страницы, бесконечная прокрутка — всё это 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 с правильной обработкой ошибок. Освоив эту связку, вы сможете создавать интерактивные интерфейсы без перезагрузки страницы.
