Удаление записей WordPress с битой миниатюрой через WP-CLI

На новостном или медиа-сайте миниатюра записи – не «украшение», а часть вёрстки, соцпревью и SEO. Файлы пропадают: чистка uploads, сбой миграции, битый CDN, удалили attachment, а _thumbnail_id остался. В ленте дыры, в карточке пустое место.

Ниже – подход через WP-CLI: кастомная команда, которая проходит посты с миниатюрой, проверяет файл на диске и HTTP-ответ, и при проблеме удаляет запись. Это жёсткий инструмент: сначала dry-run на копии, потом прод.

Что делает скрипт

  1. Берёт посты типа post, у которых есть meta _thumbnail_id
  2. Для каждой записи смотрит attachment: путь на диске и URL
  3. Удаляет пост, если:
    • файла нет на сервере
    • URL отдаёт 404 / ошибку запроса
    • URL миниатюры пустой, хотя meta «как будто есть»
  4. Пишет в консоль прогресс и итоговую статистику

Важно: wp_delete_post( $id, true ) – удаление навсегда, не в корзину. На проде без бэкапа и тестового прогона не гоняйте.

Код команды WP-CLI

Можно положить в mu-plugin, файл плагина или подключить через wp eval-file. Регистрация команды:

if ( defined( 'WP_CLI' ) && WP_CLI ) {
    WP_CLI::add_command( 'delete_missing_images', 'delete_posts_with_missing_featured_image' );
}

function delete_posts_with_missing_featured_image() {
    $posts_per_page = 1000;
    $page           = 1;
    $total_deleted  = 0;
    $total_checked  = 0;
    $total_no_image = 0;

    $total_posts_count = count( get_posts( array(
        'post_type'      => 'post',
        'posts_per_page' => -1,
        'meta_query'     => array(
            array(
                'key'     => '_thumbnail_id',
                'compare' => 'EXISTS',
            ),
        ),
        'fields' => 'ids',
    ) ) );

    WP_CLI::log( "Записей с _thumbnail_id: $total_posts_count" );

    do {
        $post_ids = get_posts( array(
            'post_type'      => 'post',
            'posts_per_page' => $posts_per_page,
            'paged'          => $page,
            'meta_query'     => array(
                array(
                    'key'     => '_thumbnail_id',
                    'compare' => 'EXISTS',
                ),
            ),
            'fields' => 'ids',
        ) );

        $total_posts = count( $post_ids );

        if ( $total_posts > 0 ) {
            foreach ( $post_ids as $post_id ) {
                $total_checked++;
                show_progress_bar( $total_checked, $total_deleted, max( 1, $total_posts_count ) );

                $thumbnail_id  = get_post_thumbnail_id( $post_id );
                $thumbnail_url = wp_get_attachment_url( $thumbnail_id );

                if ( ! empty( $thumbnail_url ) ) {
                    $file_path = get_attached_file( $thumbnail_id );

                    if ( ! $file_path || ! file_exists( $file_path ) ) {
                        WP_CLI::log( "Нет файла на диске. Удаляем post ID: $post_id" );
                        wp_delete_post( $post_id, true );
                        $total_deleted++;
                    } else {
                        $response    = wp_remote_get( $thumbnail_url, array( 'timeout' => 10 ) );
                        $status_code = wp_remote_retrieve_response_code( $response );

                        if ( is_wp_error( $response ) || (int) $status_code === 404 ) {
                            WP_CLI::log( "URL недоступен (код $status_code). Удаляем post ID: $post_id" );
                            wp_delete_post( $post_id, true );
                            $total_deleted++;
                        }
                    }
                } else {
                    WP_CLI::log( "Пустой URL миниатюры. Удаляем post ID: $post_id" );
                    $total_no_image++;
                    wp_delete_post( $post_id, true );
                    $total_deleted++;
                }
            }
        }

        $page++;
    } while ( $total_posts > 0 );

    WP_CLI::log( '' );
    WP_CLI::log( "Проверено: $total_checked" );
    WP_CLI::log( "Пустой URL: $total_no_image" );
    WP_CLI::log( "Удалено: $total_deleted" );
    WP_CLI::success( 'Готово.' );
}

function show_progress_bar( $current, $deleted, $total ) {
    $percentage = (int) round( ( $current / $total ) * 100 );
    $filled     = (int) floor( $percentage / 2 );
    $bar        = str_repeat( '=', $filled ) . str_repeat( ' ', 50 - $filled );
    echo "\r[$bar] $percentage%  checked: $current  deleted: $deleted";
}

На больших сайтах HTTP-проверка каждого URL замедлит прогон. Имеет смысл сначала смотреть только file_exists, а remote-check оставить отдельным режимом.

Как запускать

1. Зарегистрированная команда

Если код подключён как mu-plugin / плагин:

wp delete_missing_images --path=/var/www/example/htdocs

2. Отдельный PHP-файл

Положите функцию в delete_missing_images.php (с регистрацией команды или с прямым вызовом функции в конце файла) и:

cd /path/to/wordpress
wp eval-file delete_missing_images.php

3. Не вешайте удаление на admin_init

Встречается совет вида add_action('admin_init', ...). Для массового удаления это плохая идея: каждый заход в админку гоняет тяжёлый обход, легко поймать таймаут и случайно снести лишнее. Для обслуживания – CLI и cron, не page load.

4. Cron раз в сутки

# crontab -e
0 3 * * * /usr/bin/wp delete_missing_images --path=/path/to/wordpress --allow-root >> /var/log/wp-missing-images.log 2>&1

Или через wp eval-file, если команда не зарегистрирована в плагине:

0 3 * * * /usr/bin/wp eval-file /path/to/delete_missing_images.php --path=/path/to/wordpress --allow-root >> /var/log/wp-missing-images.log 2>&1

Перед продом

  • Снимок БД и uploads (или хотя бы dry-run: логировать ID без wp_delete_post)
  • Прогон на staging с тем же набором медиа
  • Понять политику: может, пост лучше оставить и снять thumbnail, а не удалять статью
  • Исключить нужные post_type / category, если «без картинки» допустимо

Итог

WP-CLI удобен для массовой уборки: пагинация, прогресс, запуск по cron без UI. Скрипт выше режет посты с битой или пустой миниатюрой. На лентах, где изображение обязательно, это спасает внешний вид. На смешанном контенте сначала смягчите правило: отчёт и ручная модерация, удаление – второй шаг.

Источники и ссылки


Комментарии загружаются…