Как добавить свой статус записи в WordPress
В WordPress пост-статусы — это боль. Зарегистрировать кастомный статус просто, но интегрировать его в UI — сплошной костыль.
Самая большая проблема: на странице редактирования поста твой статус просто не появляется. WordPress заточен под свои publish, draft, pending и т.д. Все остальные надо вручную внедрять в интерфейс и контролировать при сохранении.
Что приходится делать:
- Добавлять кастомные статусы в выпадающий список через JS (обычно jQuery).
- Следить за сохранением: WordPress может сам поменять твой статус на дефолтный. Поэтому иногда нужно хукаться на transition_post_status, wp_insert_post_data и т.п.
- Контролировать, когда и как показывать статус — вручную. Это касается и фильтрации, и прав доступа, и логики отображения.
Люди пробуют, страдают, делают. WordPress не предусматривает нормальной поддержки кастомных статусов в админке.
Если хочешь использовать кастомные статусы:
- готовься писать JS, PHP и фильтры руками;
- тестировать каждый кейс отдельно;
- и задумываться, может, лучше вообще обойтись без статусов — через мета, таксономии, или свой флоу.
Это не та часть WP, которую можно "просто добавить". Однако, если ты готов к потенциальным трудностям, чтобы сделать все "красиво", то читай далее
Класс для регистрации статуса поста
Этот класс - это попытка завернуть всю вышеописанную рутинку по регистрации статуса в объект с параметрами. Разумеется он не всегда подойдет, но его можно взять за основу и поправить код как нужно.
/** * Wrapper class for registering custom post statuses in WordPress. * * @version 1.0.0 */ class Kama_Post_Status_Registrator { private string $slug; private string $label; /** * Additional arguments for the post status. * * @see register_post_status() */ private array $args; /** * @param string $slug The unique slug for the post status. * @param array $args Additional arguments {@see register_post_status()}. */ public function __construct( string $slug, array $args = [] ) { $this->slug = $slug; $this->label = $args['label'] ?? ucfirst( $slug ); $this->args = wp_parse_args( $args, [ 'label' => $this->label, 'label_count' => _n_noop( "{$this->label} <span class=\"count\">(%s)</span>", "{$this->label} <span class=\"count\">(%s)</span>" ), 'public' => true, 'show_in_admin_status_list' => true, 'show_in_admin_all_list' => true, ] ); } public function register(): void { add_action( 'init', [ $this, '_register' ] ); } public function _register(): void { register_post_status( $this->slug, $this->args ); $this->classic_editor_hooks(); add_action( 'admin_print_footer_scripts-edit.php', [ $this, '_quick_edit_dropdown' ] ); add_filter( 'display_post_states', [ $this, '_show_status_next_to_post_title' ], 10, 2 ); } private function classic_editor_hooks(): void { add_action( 'post_submitbox_misc_actions', [ $this, '_editor_dropdown' ] ); add_filter( 'wp_insert_post_data', [ $this, '_fix_new_post_status' ], 10, 4 ); } public function _editor_dropdown( $post ): void { $option = sprintf( '<option value="%s">%s</option>', esc_attr( $this->slug ), esc_html( $this->label ) ); ?> <script> document.addEventListener( 'DOMContentLoaded', function(){ const statusSelect = document.querySelector( 'select#post_status' ); statusSelect.insertAdjacentHTML( 'beforeend', <?= wp_json_encode( $option ) ?> ); <?php if ( $this->slug === $post->post_status ) { ?> document.querySelector( '#post-status-display' ).textContent = <?= wp_json_encode( $this->label ) ?>; statusSelect.value = <?= wp_json_encode( $this->slug ) ?>; <?php } ?> } ); </script> <?php } public function _quick_edit_dropdown(): void { $option = sprintf( '<option value="%s">%s</option>', esc_attr( $this->slug ), esc_html( $this->label ) ); ?> <script> document.addEventListener( 'DOMContentLoaded', function(){ document.querySelector( 'select[name="_status"]' ).insertAdjacentHTML( 'beforeend', <?= wp_json_encode( $option ) ?> ); }); </script> <?php } public function _show_status_next_to_post_title( $states, $post ) { if ( $this->slug === $post->post_status && $this->slug !== get_query_var( 'post_status' ) ) { $states[] = $this->label; } return $states; } /** * When creating a new post with a custom status and clicking "Publish", * the post gets "publish" status anyway. This fixes that behavior. * Tested on WP 6.8. */ public function _fix_new_post_status( array $data, array $_, $__, $update ): array { /** * NOTE: We must use the $_REQUEST superglobal to determine the real * REQUEST post_status, because at the moment WP hard changes the * $data['post_status'] and $_POST['post_status'] to "publish" when * we push "Publish" button for a new post (tested on WP 6.8). * @see _wp_translate_postdata() */ $status = $_REQUEST['post_status'] ?? ''; /** * NOTE: We should do this on update only because on add-new-post an 'auto-draft' created and then it is updated. */ $is_fix_needed = $update && is_admin() && ! in_array( $status, [ 'auto-draft', 'inherit' ] ); if ( $is_fix_needed && $status === $this->slug ) { $data['post_status'] = $this->slug; } return $data; } }
Использование:
$registrator = new Kama_Post_Status_Registrator( 'featured' ); $registrator->register();
Или с параметрами:
$registrator = new Kama_Post_Status_Registrator( 'featured', [ 'label' => __( 'Featured', 'dom' ), 'public' => true, 'show_in_admin_status_list' => false, 'show_in_admin_all_list' => false, ] ); $registrator->register();
Шаг за шагом
Регистрация статуса
Для начала — регистрируем собственный статус записи. Это делается аналогично регистрации типа записи или таксономии, только используется функция register_post_status():
add_action( 'init', 'register_mynew_post_status' ); function register_mynew_post_status() { register_post_status( 'featured', [ 'label' => 'Featured', 'label_count' => _n_noop( 'Featured <span class="count">(%s)</span>', 'Featured <span class="count">(%s)</span>' ), 'public' => true, ] ); }
Добавляем этот код в functions.php вашей темы или в плагин. И... Ничего не произошло!
Но register_post_status() что-то все же делает — теперь статус доступен в WordPress. Однако, чтобы он появился в админке, нужно выполнить ещё несколько шагов.
Отображение в списке статусов в админке
Чтобы статус отображался в списке фильтра по статусам в админке, убедитесь, что:
- Есть хотя бы один пост с этим статусом.
- Вы включили параметры для register_post_status():
register_post_status( 'featured', [ ... 'show_in_admin_status_list'=> true, 'show_in_admin_all_list' => true, ] );

Добавление статуса в метабокс редактирования записи (Classic Editor)
WordPress по умолчанию не отображает кастомные статусы в выпадающем списке. Решение — jQuery-костыль:
add_action( 'post_submitbox_misc_actions', 'add_mynew_post_status_to_edit_post_list' ); function add_mynew_post_status_to_edit_post_list( $post ) { ?> <script> document.addEventListener( 'DOMContentLoaded', function(){ const statusSelect = document.querySelector( 'select#post_status' ); statusSelect.insertAdjacentHTML( 'beforeend', '<option value="featured">Featured</option>' ); <?php if ( 'featured' === $post->post_status ) { ?> document.querySelector( '#post-status-display' ).textContent = 'Featured'; statusSelect.value = 'featured'; <?php } ?> } ); </script> <?php }
При обновлении. Сохранение статуса работает сразу - нет необходимости делать что-то еще на хуке save_post.
Однако, при добавлении поста, если выбрать свой статус и нажать "Опубликовать", то выбранный статус не сохранится, а превратится в "publish". Чтобы он сохранился нужно еще раз выбрать статус и еще раз обновить пост. См. класс ниже там эта проблема решена.
Проверялось на WP 6.8.
Добавление статуса в Quick Edit
Еще костыль, чтобы добавить статус в меню “Быстрое редактирование”:
add_action( 'admin_footer-edit.php', 'add_mynew_post_status_to_quick_edit' ); function add_mynew_post_status_to_quick_edit() { ?> <script> document.addEventListener( 'DOMContentLoaded', function(){ document.querySelector( 'select[name="_status"]' ).insertAdjacentHTML( 'beforeend', '<option value="featured">Featured</option>' ); }); </script> <?php }
Отображение статуса рядом с заголовком записи
Используем хук display_post_states, чтобы отобразить ваш статус в списке записей рядом с заголовками:
add_filter( 'display_post_states', 'show_mynew_post_status_next_to_post_title', 10, 2 ); function show_mynew_post_status_next_to_post_title( $states, $post ) { if ( 'featured' === get_query_var( 'post_status' ) ) { return $states; } if ( 'featured' === $post->post_status ) { $states[] = 'Featured'; } return $states; }
Перевод поста в новый статус
Для создания поста с нужным статусом можно использовать wp_insert_post() или wp_update_post():
wp_update_post( [ 'ID' => 12345, 'post_status' => 'featured', ] );
Зачем вообще может нужно использовать статусы
Основная причина использования статусов - это то что в запросах (WP_Query, REST) передаёшь post_status и без meta_query получаешь нужный набор записей - это производительнее и удобнее.
Также в админке сразу появляется удобный фильтр постов по статусу.
Чаще всего статусы могут пригодится для произвольных типов записей. Например у нас есть тип записи (донаты) и каждый типа может иметь свои статусы, которые вообще никак не сочетаются с дефолтными: draft, publish, private:

Еще примеры, где статусы постов могут хорошо подойти:
-
Нестандартный тип записи
Тип записи, который не относится к статьям, а содержит какую-то информацию, например об оплаченных заявках.
Статусы: submited, funded, failed
Как работает: Создается заявка, затем она оплачивается или нет. -
Редакционный флоу
Статусы: draft -> review -> design-ready -> publish
Как работает: каждый переход триггерит transition_post_status, нужный отдел видит свои записи через фильтр по статусу, фронт не покажет пост пока статус не "publish". -
Модерация
Статусы: pending -> flagged -> awaiting-recheck -> publish|trash
Как работает: жалоба ставит flagged и убирает материал из публичного запроса, модератор после проверки меняет статус, REST и WP_Query отдают только publish. -
Платный контент
Статусы: paywall-pending -> subscription-only -> publish
Как работает: статья создаётся как "paywall-pending", оплата/крон переводит в "subscription-only", pre_get_posts пропускает статус только для подписчиков, позже можно открыть материал для всех, переведя в статус "publish". -
Этапы события
Статусы: registration-open -> event-live -> event-after -> archive
Как работает: крон по датам меняет статус, видимость страниц события регулируется одним параметром post_status, без meta_query и сравнений дат. - Многошаговые заявки
Статусы: incomplete -> awaiting-payment -> approved -> fulfilled
Как работает: пользователь сохраняет черновик как "incomplete", платёжный webhook переводит в "awaiting-payment", после поступления денег ставит "funded", менеджер при выполнении услуги переключает на "fulfilled" для отчетности.