Закладочная навигация в WordPress — организация страниц опций

По мере роста темы или плагина количество настроек неизбежно увеличивается. Три секции на одной странице — терпимо. Десять секций — уже нечитаемо. Пользователь вынужден бесконечно скроллить в поисках нужного поля, теряя контекст и время. Решение, знакомое каждому пользователю десктопных приложений — закладочная навигация (tabs).

WordPress предоставляет готовую CSS-инфраструктуру для создания вкладок в админ-панели. Классы nav-tab-wrapper и nav-tab стилизуют навигацию точно так же, как на страницах «Темы» или «Плагины». Вам остаётся только добавить логику переключения вкладок и привязку к группам опций Settings API. В этой статье мы реализуем полноценную многостраничную панель настроек с двумя вкладками, сохранением состояния активной вкладки и выводом сообщений об успехе через settings_errors().

Закладочная навигация — это не функция WordPress API, а паттерн, построенный на HTML/CSS и query-параметрах. WordPress лишь предоставляет CSS-классы для стилизации. Вся логика переключения вкладок пишется вами вручную.

План реализации

Наша цель — создать страницу опций с двумя вкладками: «Display Options» (настройки отображения) и «Social Networks» (социальные сети). При загрузке страницы активна первая вкладка. После сохранения настроек активная вкладка не сбрасывается — пользователь остаётся там же, где был до нажатия кнопки «Сохранить». При успешном сохранении выводится стандартное уведомление WordPress.

Технический план:

  • Создать HTML-структуру вкладок с классами nav-tab-wrapper и nav-tab
  • Добавить query-параметр tab для идентификации активной вкладки
  • Проверять параметр tab в колбэке страницы опций и применять класс nav-tab-active к активной вкладке
  • Выводить содержимое соответствующей группы опций в зависимости от активной вкладки
  • Добавить скрытое поле в форму, чтобы WordPress запомнил активную вкладку при сохранении
  • Вызвать settings_errors() для отображения сообщений об успехе/ошибках

HTML-структура вкладок

WordPress использует два CSS-класса для стилизации вкладок: nav-tab-wrapper — контейнер, объединяющий вкладки в строку, и nav-tab — стиль отдельной вкладки. Активная вкладка дополнительно получает класс nav-tab-active. HTML-структура выглядит так:

Sandbox Theme Options

<-- sections go here -->
[/codeblock]

Обратите внимание: вкладки — это ссылки <a>, а не кнопки или элементы списка. WordPress стилизует именно такую структуру, и отходить от неё не рекомендуется — вкладки потеряют нативный вид админ-панели.

Определение активной вкладки через query-параметр

Каждая вкладка ведёт на ту же страницу опций, но с разным значением параметра tab в URL: admin.php?page=sandbox-theme-options&tab=display. Колбэк страницы проверяет этот параметр и определяет, какую вкладку подсветить и содержимое какой группы опций показать:

function sandbox_theme_options_page_html() { if ( ! current_user_can( 'manage_options' ) ) { return; } $active_tab = isset( $_GET['tab'] ) ? sanitize_key( $_GET['tab'] ) : 'display'; ?>
Функция sanitize_key() критически важна для безопасности. Параметр $_GET['tab'] приходит от пользователя и может содержать XSS-вектор. sanitize_key() оставляет только буквы, цифры, дефисы и подчёркивания — безопасное подмножество символов.

Регистрация секций для разных страниц

Внимательный читатель заметит, что do_settings_sections() вызывается с разными page slug в зависимости от активной вкладки: 'sandbox_display_options_page' и 'sandbox_social_options_page'. Это значит, что секции должны быть зарегистрированы для разных страниц. Вот как выглядит обновлённая функция регистрации:

function sandbox_register_settings() { register_setting( 'sandbox_options_group', 'sandbox_theme_options', array( 'type' => 'array', 'sanitize_callback' => 'sandbox_sanitize_options', 'default' => array( 'show_header' => 1, 'show_sidebar' => 1, 'show_footer' => 1, 'facebook_url' => '', 'twitter_url' => '', ), ) ); add_settings_section( 'sandbox_display_section', 'Display Options', 'sandbox_display_section_callback', 'sandbox_display_options_page' ); add_settings_field( 'sandbox_show_header', 'Show Header', 'sandbox_checkbox_field_callback', 'sandbox_display_options_page', 'sandbox_display_section', array( 'label_for' => 'sandbox_show_header', 'option' => 'sandbox_theme_options', 'key' => 'show_header' ) ); add_settings_section( 'sandbox_social_section', 'Social Network Links', 'sandbox_social_section_callback', 'sandbox_social_options_page' ); add_settings_field( 'sandbox_facebook_url', 'Facebook URL', 'sandbox_url_field_callback', 'sandbox_social_options_page', 'sandbox_social_section', array( 'label_for' => 'sandbox_facebook_url', 'option' => 'sandbox_theme_options', 'key' => 'facebook_url' ) ); } add_action( 'admin_init', 'sandbox_register_settings' );[/codeblock]

Ключевой момент: все поля по-прежнему принадлежат одной группе опций (sandbox_options_group) и хранятся в одном массиве (sandbox_theme_options). Разделение происходит только на уровне интерфейса — разные секции зарегистрированы для разных page slug. При сохранении формы WordPress обновляет весь массив опций целиком, независимо от того, на какой вкладке была нажата кнопка «Сохранить».

Сохранение состояния вкладки после отправки формы

При нажатии кнопки «Сохранить» WordPress перенаправляет пользователя на URL без query-параметра tab. В результате активная вкладка сбрасывается на значение по умолчанию — пользователь всегда возвращается на первую вкладку после сохранения. Это неудобно: изменил URL соцсетей, сохранил — и оказался на вкладке «Display». Чтобы исправить это, добавим скрытое поле в форму и обработаем его при перенаправлении:

function sandbox_theme_options_page_html() { if ( ! current_user_can( 'manage_options' ) ) { return; } $active_tab = isset( $_GET['tab'] ) ? sanitize_key( $_GET['tab'] ) : 'display'; ?>

'; submit_button( 'Save Settings' ); ?>
Теперь добавим код для перенаправления с сохранением вкладки. WordPress вызывает хук wp_redirect при редиректе после сохранения настроек. Мы перехватим этот редирект и добавим параметр tab:

function sandbox_preserve_active_tab( $location ) { if ( isset( $_POST['sandbox_active_tab'] ) ) { $location = add_query_arg( 'tab', sanitize_key( $_POST['sandbox_active_tab'] ), $location ); } return $location; } add_filter( 'wp_redirect', 'sandbox_preserve_active_tab' );[/codeblock]

Этот фильтр проверяет, было ли отправлено скрытое поле sandbox_active_tab, и если да — добавляет соответствующий query-параметр к URL редиректа. После сохранения пользователь остаётся на той же вкладке, где нажал кнопку «Сохранить».

Фильтр wp_redirect срабатывает при любом редиректе в админке, не только со страницы опций. Обязательно проверяйте наличие $_POST['sandbox_active_tab'] перед модификацией URL, иначе вы сломаете редиректы других плагинов и стандартных страниц WordPress.

settings_errors() — сообщения об успехе и ошибках

Функция settings_errors() выводит стандартные уведомления WordPress об успешном сохранении, ошибках валидации или предупреждениях. Вызов должен располагаться до навигации с вкладками, чтобы сообщение было видно всегда. Без неё пользователь не узнает, сохранились ли настройки:

settings_errors( 'sandbox_theme_options' );[/codeblock]

WordPress автоматически добавляет сообщение «Settings saved.» при успешном сохранении через Settings API. Кастомные сообщения добавляются через add_settings_error() в функции санитизации:

function sandbox_sanitize_options( $input ) { $sanitized = array(); if ( isset( $input['facebook_url'] ) && ! empty( $input['facebook_url'] ) ) { $sanitized['facebook_url'] = esc_url_raw( $input['facebook_url'] ); $parsed = wp_parse_url( $sanitized['facebook_url'] ); if ( ! isset( $parsed['host'] ) || strpos( $parsed['host'], 'facebook.com' ) === false ) { add_settings_error( 'sandbox_theme_options', 'invalid_facebook_url', 'Facebook URL must point to facebook.com. Your input was not saved.', 'error' ); $sanitized['facebook_url'] = ''; } } return $sanitized; }[/codeblock]
settings_errors() принимает необязательный параметр $setting — slug опции, для которой нужно показать сообщения. Если опустить, выводятся все зарегистрированные сообщения. Указывайте slug явно, чтобы сообщения из других плагинов не попадали на вашу страницу.

Динамическая подстановка slug страницы в форму

WordPress по умолчанию отправляет форму настроек на options.php. Query-параметр tab теряется при отправке, и WordPress не знает, на какую вкладку вернуть пользователя. Одно из решений — динамически менять action формы, чтобы он включал текущий tab:

$form_action = 'options.php'; if ( $active_tab !== 'display' ) { $form_action = add_query_arg( 'tab', $active_tab, 'options.php' ); } echo '
';[/codeblock]

Этот подход работает, но имеет недостаток: WordPress ожидает в action именно options.php без query-параметров, и некоторые проверки могут дать сбой. Более надёжное решение — скрытое поле и фильтр wp_redirect, описанные выше.

Обработка отсутствия параметра tab

При первом заходе на страницу опций параметр tab отсутствует в URL. Наш код должен корректно обработать эту ситуацию и показать вкладку по умолчанию. Используйте значение по умолчанию через оператор ?? или тернарный оператор:

$active_tab = isset( $_GET['tab'] ) ? sanitize_key( $_GET['tab'] ) : 'display';[/codeblock]

Если ни один query-параметр не передан, $active_tab получает значение 'display', и страница показывает первую вкладку. Это стандартное поведение, ожидаемое пользователями.

Полный код закладочной навигации

Ниже — полный код functions.php с закладочной навигацией, двумя вкладками, сохранением состояния активной вкладки и выводом сообщений через settings_errors():

'array', 'sanitize_callback' => 'sandbox_sanitize_options', 'default' => array( 'show_header' => 1, 'show_sidebar' => 1, 'show_footer' => 1, 'facebook_url' => '', 'twitter_url' => '', ), ) ); add_settings_section( 'sandbox_display_section', 'Display Options', 'sandbox_display_section_callback', 'sandbox_display_options_page' ); $display = array( 'show_header' => 'Show Header', 'show_sidebar' => 'Show Sidebar', 'show_footer' => 'Show Footer' ); foreach ( $display as $k => $l ) { add_settings_field( 'sandbox_' . $k, $l, 'sandbox_checkbox_field_callback', 'sandbox_display_options_page', 'sandbox_display_section', array( 'label_for' => 'sandbox_' . $k, 'option' => 'sandbox_theme_options', 'key' => $k ) ); } add_settings_section( 'sandbox_social_section', 'Social Network Links', 'sandbox_social_section_callback', 'sandbox_social_options_page' ); $social = array( 'facebook_url' => 'Facebook URL', 'twitter_url' => 'Twitter URL' ); foreach ( $social as $k => $l ) { add_settings_field( 'sandbox_' . $k, $l, 'sandbox_url_field_callback', 'sandbox_social_options_page', 'sandbox_social_section', array( 'label_for' => 'sandbox_' . $k, 'option' => 'sandbox_theme_options', 'key' => $k ) ); } } add_action( 'admin_init', 'sandbox_register_settings' ); function sandbox_display_section_callback() { echo '

Toggle visibility of page containers.

'; } function sandbox_social_section_callback() { echo '

Enter your social media profile URLs.

'; } function sandbox_checkbox_field_callback( $a ) { $o = get_option( $a['option'], array() ); $c = isset( $o[ $a['key'] ] ) ? (bool) $o[ $a['key'] ] : false; echo ''; } function sandbox_url_field_callback( $a ) { $o = get_option( $a['option'], array() ); $v = isset( $o[ $a['key'] ] ) ? $o[ $a['key'] ] : ''; echo ''; } function sandbox_sanitize_options( $input ) { $out = array(); foreach ( array('show_header','show_sidebar','show_footer') as $f ) { $out[ $f ] = isset( $input[ $f ] ) && $input[ $f ] ? 1 : 0; } foreach ( array('facebook_url','twitter_url') as $f ) { $out[ $f ] = isset( $input[ $f ] ) && ! empty( $input[ $f ] ) ? esc_url_raw( $input[ $f ] ) : ''; } return $out; } function sandbox_theme_options_page_html() { if ( ! current_user_can( 'manage_options' ) ) { return; } $active_tab = isset( $_GET['tab'] ) ? sanitize_key( $_GET['tab'] ) : 'display'; ?>

'; submit_button( 'Save Settings' ); ?>
Тестирование закладочной навигации

После добавления кода в functions.php проверьте поведение:

  1. Зайдите в Appearance → Theme Options. Должна открыться вкладка «Display Options»
  2. Переключитесь на «Social Networks» — URL должен содержать tab=social, вкладка «Social» должна быть активной
  3. Введите Facebook URL и нажмите «Save Settings» — после сохранения вкладка не должна сброситься на «Display»
  4. Введите некорректный URL и сохраните — должно появиться сообщение об ошибке над вкладками

Если вкладка сбрасывается после сохранения — проверьте, что фильтр wp_redirect добавлен и скрытое поле sandbox_active_tab присутствует в форме. Если сообщения не отображаются — проверьте вызов settings_errors() и slug, совпадающий с первым параметром add_settings_error().

Сравнение функций работы с сообщениями в WordPress

WordPress предоставляет несколько функций для вывода уведомлений в админке, и важно понимать разницу между ними:

ФункцияНазначениеКонтекстКуда выводит
settings_errors()Сообщения Settings API: успех сохранения, ошибки валидацииСтраница опцийВ месте вызова на странице
add_settings_error()Регистрирует сообщение для settings_errors()sanitize_callbackВ месте вызова settings_errors()
admin_noticesХук для вывода любых уведомлений в админкеЛюбая админ-страницаВерхняя часть страницы
wp_die()Критическая ошибка, останавливает выполнениеЛюбой контекстОтдельная страница ошибки

Для страниц опций правильная комбинация — add_settings_error() в санитайзере и settings_errors() в колбэке страницы. Это даёт нативный вид уведомлений и корректно обрабатывает все состояния: успех, предупреждение, ошибка, информационное сообщение.

Динамическая генерация вкладок из массива

Если количество вкладок растёт, хардкодить каждую ссылку в nav-tab-wrapper становится неудобно. Умное решение — определить массив вкладок и генерировать HTML-структуру динамически в цикле. Это сокращает дублирование кода и упрощает добавление новых вкладок:

$tabs = array( 'display' => 'Display Options', 'social' => 'Social Networks', 'general' => 'General Settings', ); $active_tab = isset( $_GET['tab'] ) && array_key_exists( $_GET['tab'], $tabs ) ? sanitize_key( $_GET['tab'] ) : 'display'; echo '';[/codeblock]

Теперь для добавления четвёртой вкладки достаточно дописать одну строку в массив $tabs и зарегистрировать секции для соответствующего page slug. HTML-разметка сгенерируется автоматически. Этот подход — один из немногих случаев, когда генерация HTML в цикле действительно оправдана и не ухудшает читаемость кода.

Расширение системы вкладок

Текущая реализация поддерживает две вкладки. Добавление третьей требует:

  • Нового элемента в nav-tab-wrapper: ссылка с классом nav-tab и условием для nav-tab-active
  • Новой секции и полей, зарегистрированных для новой страницы (например, 'sandbox_general_options_page')
  • Нового условия в if-elseif цепочке внутри колбэка страницы
  • Нового допустимого значения в sanitize_key() или явной проверке допустимых значений tab

Если вкладок больше трёх, стоит заменить цепочку if-elseif на массив допустимых вкладок и проверку in_array(). Это предотвратит ошибки, когда пользователь вручную меняет параметр tab на несуществующее значение:

$allowed_tabs = array( 'display', 'social', 'general', 'advanced' ); $active_tab = isset( $_GET['tab'] ) && in_array( $_GET['tab'], $allowed_tabs, true ) ? sanitize_key( $_GET['tab'] ) : 'display';[/codeblock]
Никогда не используйте значение $_GET['tab'] напрямую в имени функции или include-пути. Это открывает возможность Local File Inclusion (LFI) атаки. Всегда проверяйте значение через in_array() с белым списком допустимых значений.

Сравнение подходов: вкладки против отдельных страниц

Когда стоит использовать вкладки, а когда — отдельные страницы с подменю? Выбор зависит от количества настроек и их логической связности:

КритерийВкладки (Tabs)Отдельные подменю
Количество настроек10-30 полей на 2-4 вкладках30+ полей, каждая страница самостоятельна
Связность данныхВсе настройки относятся к одной теме/плагинуРазные модули с независимыми настройками
Частота использованияПользователь часто переключается между вкладкамиКаждую страницу настраивают редко и независимо
ОбучаемостьВыше — все настройки на одной страницеНиже — нужно искать нужное подменю
Глубина меню1 пункт меню, нет подменю1 пункт + N подменю (загромождает боковую панель)

Для большинства тем и средних плагинов оптимальны вкладки: одна точка входа через меню «Внешний вид» или собственный пункт, все настройки на одной странице, переключение между группами мгновенное. Отдельные подменю оправданы для крупных плагинов вроде WooCommerce, где каждый раздел (Товары, Заказы, Доставка) — по сути, отдельное приложение.

\u{201c}

Закладочная навигация — это не техническая необходимость, а вопрос уважения к пользователю. Две вкладки с 5 полями каждая удобнее, чем одна страница с 10 полями, требующая прокрутки. Потратьте 20 минут на реализацию вкладок — пользователи скажут спасибо молчаливым отсутствием жалоб.

Часто задаваемые вопросы по закладочной навигации

Как создать вкладки в админке WordPress?

Используйте HTML-структуру с CSS-классами nav-tab-wrapper (контейнер) и nav-tab (отдельная вкладка). Активная вкладка получает дополнительный класс nav-tab-active. Переключение реализуется через query-параметр tab, который проверяется в колбэке страницы опций.

Почему вкладка сбрасывается после сохранения настроек?

WordPress по умолчанию редиректит на URL без параметра tab. Для сохранения вкладки добавьте скрытое поле с именем sandbox_active_tab и фильтр wp_redirect, который добавляет параметр tab к URL редиректа.

Обязательно ли использовать sanitize_key() для параметра tab?

Да. Параметр $_GET['tab'] приходит от пользователя и может содержать XSS-вектор. sanitize_key() оставляет только безопасные символы: буквы, цифры, дефисы и подчёркивания. Без санитизации вы рискуете XSS-уязвимостью.

Как работает settings_errors() в контексте вкладок?

settings_errors() выводит уведомления WordPress о сохранении настроек. Вызов должен быть до HTML-структуры вкладок, чтобы сообщение было видно на любой вкладке. Принимает опциональный параметр $setting — slug опции для фильтрации сообщений.

Можно ли иметь разные группы опций на разных вкладках?

Нет, одна форма поддерживает только одну группу опций (один вызов settings_fields()). Но вы можете регистрировать разные секции для разных page slug и вызывать do_settings_sections() с соответствующим slug в зависимости от активной вкладки.

Как добавить третью вкладку в существующую навигацию?

Добавьте третий элемент <a> в nav-tab-wrapper с новым значением параметра tab, зарегистрируйте секции и поля для новой страницы, добавьте условие elseif в колбэк и включите новое значение tab в белый список допустимых значений.

Чем вкладки лучше отдельных страниц с подменю?

Вкладки не загромождают боковое меню, мгновенно переключаются без перезагрузки страницы (хотя в нашей реализации перезагрузка происходит) и интуитивно понятны пользователям. Они идеальны для 2-4 групп связанных настроек в рамках одной темы или плагина.

Как стилизовать вкладки под дизайн WordPress?

Используйте нативные CSS-классы nav-tab-wrapper, nav-tab и nav-tab-active. WordPress автоматически применяет стандартные стили админ-панели к этим классам. Дополнительные стили не требуются — вкладки выглядят так же, как на страницах «Темы» и «Плагины».

Обязательно ли использовать h2 для контейнера вкладок?

Нет, WordPress использует h2 на страницах «Темы» и «Плагины», но это не требование. Вы можете использовать div — стили nav-tab-wrapper работают на любом блочном элементе. Однако h2 семантически правильнее для заголовка навигации.

Как защититься от LFI-атаки через параметр tab?

Всегда проверяйте значение tab через in_array() с белым списком допустимых значений перед использованием в имени функции, пути к файлу или имени страницы. Никогда не подставляйте $_GET['tab'] напрямую в include, require или file_get_contents.