Как добавить свой статус записи в 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" для отчетности.


