На новостном или медиа-сайте миниатюра записи – не «украшение», а часть вёрстки, соцпревью и SEO. Файлы пропадают: чистка uploads, сбой миграции, битый CDN, удалили attachment, а _thumbnail_id остался. В ленте дыры, в карточке пустое место.
Ниже – подход через WP-CLI: кастомная команда, которая проходит посты с миниатюрой, проверяет файл на диске и HTTP-ответ, и при проблеме удаляет запись. Это жёсткий инструмент: сначала dry-run на копии, потом прод.
Что делает скрипт
- Берёт посты типа
post, у которых есть meta_thumbnail_id - Для каждой записи смотрит attachment: путь на диске и URL
- Удаляет пост, если:
- файла нет на сервере
- URL отдаёт 404 / ошибку запроса
- URL миниатюры пустой, хотя meta «как будто есть»
- Пишет в консоль прогресс и итоговую статистику
Важно: 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. Скрипт выше режет посты с битой или пустой миниатюрой. На лентах, где изображение обязательно, это спасает внешний вид. На смешанном контенте сначала смягчите правило: отчёт и ручная модерация, удаление – второй шаг.

