
🗑 Програмне очищення медіатеки WordPress: PHP-скрипти для видалення сміттєвих файлів
Медіатека WordPress влаштована як горище: видалили пост, зображення залишилися. Змінили тему, старі розміри зображень лежать мертвим вантажем. Перенесли сайт, половина ескізів б’є в 404.
Ручне чищення через адмінку на сайті з парою тисяч файлів, заняття для медитації. Але є спосіб швидше: п’ять PHP-функцій, які знаходять і видаляють сміття за один прохід. Без плагінів, зі зрозумілим кодом і контролем над кожним видаленим файлом.
Перед запуском, повний бекап. Функції видаляють файли безповоротно: ні кошика, ні «undo» у них немає. Якщо сумніваєтеся, проженіть на staging-копії.
💡 Швидкий огляд:
- Видалення неприкріплених вкладень, файлів, що залишилися після видалення постів
- Зачистка медіафайлів конкретного довільного типу запису (CPT)
- Очищення медіатеки від битих посилань, 404-вкладень без файлу на сервері
- Пошук і видалення файлів у
wp-content/uploads, не зареєстрованих як вкладення WordPress - Сценарій для сайтів, де зображення зберігаються в довільних полях (ACF, Meta Box), а не як вкладення
Про що важливо знати до запуску
Код нижче видаляє файли фізично, з диска та з бази. Три моменти, які збережуть сайт.
Перше: зображення на сторінках архівів тегів або в SEO-описах часто не прив’язані до жодного посту. Вони висять «сиротами», але сайту потрібні. Якщо у вас є такі, виключіть їх з області дії функцій або доопрацюйте умови.
Друге: WordPress створює кілька розмірів кожного зображення. Мініатюри успадковують post_parent оригіналу, тому функція delete_unattached_attachments() їх не чіпає, вона фільтрує строго за post_parent = 0. Проблема виникає, тільки якщо сам оригінал втратив прив’язку до посту.
Третє: якщо на файл, що видаляється, є посилання в тексті посту, після чистки воно стане битим. Перед запуском проженіть сайт через Screaming Frog або аналог і складіть карту посилань.
1. Видалення неприкріплених вкладень
Найчастіший сценарій: видалили пост, вкладення залишилися в базі з post_type = 'attachment' і post_parent = 0. Займають місце на диску і в бекапах.
Функція нижче знаходить усі такі записи та видаляє їх. Розмістіть у functions.php дочірньої теми або через плагін для сніпетів на кшталт WPCode. Сама по собі вона не запуститься — це визначення, якому потрібен виклик.
1 function delete_unattached_attachments() { 2 $attachments = get_posts( array( 3 'post_type' => 'attachment', 4 'numberposts' => -1, 5 'fields' => 'ids', 6 'post_parent' => 0, 7 ) ); 8 9 if ( $attachments ) { 10 foreach ( $attachments as $attachment_id ) { 11 $attachment_path = get_attached_file( $attachment_id ); 12 wp_delete_attachment( $attachment_id, true ); 13 unlink( $attachment_path ); 14 } 15 } 16 }
get_posts() вибирає всі вкладення без батьківського запису. wp_delete_attachment() з параметром true стирає і запис у базі, і файл з міні-копіями. Додатковий unlink(), страховка: якщо файл з якоїсь причини залишився на диску, він видаляється примусово.
Зверніть увагу: featured images (зображення запису) теж мають post_parent = 0 у деяких конфігураціях. Перед бойовим запуском замініть wp_delete_attachment на echo $attachment_id . '<br>', побачите список ID, які потраплять під видалення. Переконалися, що все правильно, повертайте бойовий варіант.
Після одноразового запуску функцію з functions.php приберіть. Нема чого тримати її на кожному init.
2. Видалення вкладень конкретного CPT
Колишній інтернет-магазин на WooCommerce, старий розділ портфоліо, видалений custom post type, усі їхні зображення продовжують лежати на сервері. Функція нижче вичищає вкладення, прикріплені до записів зазначеного типу.
1 function delete_cpt_attachments( $cpt = 'card' ) { 2 $attachments = get_posts( array( 3 'post_type' => 'attachment', 4 'numberposts' => -1, 5 ) ); 6 7 if ( $attachments ) { 8 foreach ( $attachments as $attachment ) { 9 $parent_id = $attachment->post_parent; 10 11 if ( $cpt === get_post_type( $parent_id ) ) { 12 $attachment_path = get_attached_file( $attachment->ID ); 13 wp_delete_attachment( $attachment->ID, true ); 14 unlink( $attachment_path ); 15 } 16 } 17 } 18 }
Замініть 'card' на slug вашого CPT. Для WooCommerce-товарів, 'product'. Якщо CPT уже видалено, get_post_type() поверне false, вкладення такого типу зачеплені не будуть. Для видалених CPT логіку потрібно скоригувати: перевіряти не тип батька, а належність до таксономії або мета-поля.
На великих базах будьте обережні: 'numberposts' => -1 без 'fields' => 'ids' завантажує повні об’єкти WP_Post. На 10 000+ вкладень це може впертися в memory_limit. Для production-обсягів додайте 'fields' => 'ids' і отримуйте тільки ID, get_post_type() відпрацює і за ID батька.
3. Очищення медіатеки від 404-вкладень
Биті ескізи в медіатеці, симптом того, що файл на диску видалено (вручну, крахом хостингу або кривим плагіном), а запис у базі залишився. WordPress показує сірий прямокутник, але при кліку, 404.
Функція опитує кожен URL вкладення і видаляє ті, що віддають 404.
1 function delete_404_attachments() { 2 $attachments = get_posts( array( 3 'post_type' => 'attachment', 4 'numberposts' => -1, 5 'fields' => 'ids', 6 ) ); 7 8 if ( $attachments ) { 9 foreach ( $attachments as $attachment_id ) { 10 $file_url = wp_get_attachment_url( $attachment_id ); 11 $file_headers = @get_headers( $file_url ); 12 13 if ( $file_headers && strpos( $file_headers[0], '404' ) !== false ) { 14 wp_delete_attachment( $attachment_id, true ); 15 } 16 } 17 } 18 }
Важливо: функція ресурсоємна. Кожен виклик get_headers() — це HTTP-запит до вашого ж сервера. На тисячі вкладень ви робите тисячу HTTP-запитів за один прохід. Результат: довго, навантаження на сервер, деякі хостинги вбивають процес за таймаутом.
Для великих медіатек розбийте на порції через 'offset' та 'numberposts' або запускайте через WP-CLI з лімітом на пакет. Якщо сайт за CDN або проксі, замініть перевірку на wp_remote_head() з таймаутом, get_headers() не завжди коректно обробляє редиректи та не підтримує автентифікацію.
4. Зворотна перевірка: файли в uploads без запису в базі
Попередні три функції чистять базу, видаляють записи вкладень. Але в wp-content/uploads можуть лежати файли, які взагалі не зареєстровані як вкладення: завантажені через FTP, залишені плагінами, згенеровані кешем.
Ця функція йде зворотним шляхом: не від бази до файлів, а від файлів до бази. Рекурсивно сканує wp-content/uploads і для кожного файлу перевіряє через attachment_url_to_postid(), чи є він вкладенням. Якщо ні, видаляє.
1 function clean_uploads_from_nonattachments() { 2 $uploads_dir = wp_upload_dir(); 3 $search = $uploads_dir['basedir']; 4 $replace = $uploads_dir['baseurl']; 5 $root = $uploads_dir['basedir']; 6 7 $iter = new RecursiveIteratorIterator( 8 new RecursiveDirectoryIterator( $root, RecursiveDirectoryIterator::SKIP_DOTS ), 9 RecursiveIteratorIterator::SELF_FIRST, 10 RecursiveIteratorIterator::CATCH_GET_CHILD 11 ); 12 13 foreach ( $iter as $fileinfo ) { 14 if ( $fileinfo->isFile() ) { 15 $image_path = $fileinfo->getPathname(); 16 $image_url = str_replace( $search, $replace, $image_path ); 17 $attachment_id = attachment_url_to_postid( $image_url ); 18 19 if ( ! $attachment_id ) { 20 unlink( $image_path ); 21 } 22 } 23 } 24 }
На тестовому сервері з 1 ГБ завантажень функція відпрацювала за 15 секунд і звільнила 700 МБ, залишилося 300 МБ реально використовуваних файлів. Для папок, більших за 5 ГБ, розбийте сканування за роками: замініть $root на $uploads_dir['basedir'] . '/2025/', потім '/2024/' і так далі.
Спочатку запустіть варіант без видалення, замініть unlink( $image_path ) на echo $image_path . PHP_EOL. Побачите повний список файлів, які функція вважає сміттям. Перевірили очима, повертайте unlink().
5. Сценарій із довільними полями: коли зображення не є вкладеннями
Найскладніший випадок: сайт, де зображення зберігаються не як вкладення WordPress, а як URL у довільних полях (ACF, Meta Box, самописні поля теми). Типовий приклад, книжковий інтернет-магазин: обкладинка книги в полі bookcover, фото автора в bookauthor_picture, зображення списку в book_list_pictrue.
У такій архітектурі для всіх файлів в uploads attachment_url_to_postid() поверне 0. Попередня функція видалить узагалі все, включно з реально використовуваними зображеннями. Потрібен інший підхід.
5.1. Складаємо білий список
Спочатку збираємо URL усіх зображень з усіх потрібних довільних полів. У прикладі нижче, три CPT і три поля:
1 $all_good_pictures = array(); 2 3 // Обложки книг (CPT 'post', поле 'bookcover') 4 $posts = get_posts( array( 5 'post_type' => 'post', 6 'posts_per_page' => -1, 7 'post_status' => 'any', 8 'fields' => 'ids', 9 ) ); 10 foreach ( $posts as $post_id ) { 11 $cover = get_field( 'bookcover', $post_id ); 12 if ( $cover ) { 13 $all_good_pictures[] = $cover; 14 } 15 } 16 17 // Изображения списков (CPT 'book_list', поле 'book_list_pictrue') 18 $lists = get_posts( array( 19 'post_type' => 'book_list', 20 'posts_per_page' => -1, 21 'post_status' => 'any', 22 'fields' => 'ids', 23 ) ); 24 foreach ( $lists as $list_id ) { 25 $pic = get_field( 'book_list_pictrue', $list_id ); 26 if ( $pic ) { 27 $all_good_pictures[] = $pic; 28 } 29 } 30 31 // Фото авторов (CPT 'bookauthor', поле 'bookauthor_picture') 32 $authors = get_posts( array( 33 'post_type' => 'bookauthor', 34 'posts_per_page' => -1, 35 'post_status' => 'any', 36 'fields' => 'ids', 37 ) ); 38 foreach ( $authors as $author_id ) { 39 $pic = get_field( 'bookauthor_picture', $author_id ); 40 if ( $pic ) { 41 $all_good_pictures[] = $pic; 42 } 43 } 44 45 $all_good_pictures = array_filter( $all_good_pictures );
На реальному проєкті, книжковому інтернет-магазині, цей підхід дозволив виявити більшість сміттєвих файлів і звільнити значну частину дискового простору.
5.2. Видаляємо все, чого немає в білому списку
Тепер проходимо по wp-content/uploads і видаляємо кожен файл, якого немає в $all_good_pictures:
1 $uploads_dir = wp_upload_dir(); 2 $search = $uploads_dir['basedir']; 3 $replace = $uploads_dir['baseurl']; 4 $root = $uploads_dir['basedir']; 5 6 $iter = new RecursiveIteratorIterator( 7 new RecursiveDirectoryIterator( $root, RecursiveDirectoryIterator::SKIP_DOTS ), 8 RecursiveIteratorIterator::SELF_FIRST, 9 RecursiveIteratorIterator::CATCH_GET_CHILD 10 ); 11 12 foreach ( $iter as $fileinfo ) { 13 if ( $fileinfo->isFile() ) { 14 $image_path = $fileinfo->getPathname(); 15 $image_url = str_replace( $search, $replace, $image_path ); 16 17 if ( ! in_array( $image_url, $all_good_pictures, true ) ) { 18 unlink( $image_path ); 19 } 20 } 21 }
Метод in_array() із суворим порівнянням на масиві з 10 000+ елементів, не найшвидший. Для production-обсягів замініть звичайний масив на асоціативний: $all_good_pictures = array_fill_keys( $all_good_pictures, true ) і перевіряйте через isset(). Різниця на 40 000 елементів, з десятка секунд до часток секунди.
Як запустити ці функції
Усі сніпети вище — це визначення функцій. Вони нічого не роблять, поки ви їх не викличете. Три безпечні способи запуску:
Спосіб | Коли використовувати | Відкат |
|---|---|---|
WP-CLI | Одноразове чищення, доступ до консолі | Немає, лише бекап |
Хук | Немає консолі, потрібно запустити з адмінки | Немає, лише бекап |
Плагін для сніпетів (WPCode) | Зручне зберігання та ввімкнення/вимкнення | Вимкнули сніпет, функція не активна |
Приклад разового запуску через адмінку:
1 add_action( 'admin_init', 'run_cleanup_once' ); 2 function run_cleanup_once() { 3 if ( isset( $_GET['cleanup'] ) && 'confirmed' === $_GET['cleanup'] ) { 4 delete_unattached_attachments(); 5 } 6 }
Переходьте за https://вашсайт.com/wp-admin/?cleanup=confirmed, функція відпрацьовує одноразово. Після виконання сніпет видаліть.
Для WP-CLI, рекомендований спосіб для production, збережіть код функції у тимчасовий файл і виконайте:
1 wp eval-file cleanup.php
Перед очищенням корисно побачити процес візуально. У відео нижче, покроковий розбір чищення медіатеки WordPress ручними й автоматичними методами.
⁉️🤔 Часті запитання
Чи можна відновити файли після видалення?
Ні. Функції використовують
wp_delete_attachment()зtrueтаunlink(), файли видаляються фізично, оминаючи кошик. Єдиний захист: повний бекап файлів і бази даних перед запуском. Перевірте, чи є у хостинг-провайдера автоматичні щоденні бекапи, у Kinsta, WP Engine та SiteGround вони ввімкнені за замовчуванням. Це дає додаткову точку відновлення, крім вашого ручного бекапу.
Чому функція не спрацювала, файли залишилися на місці?
Найчастіша причина: ви додали визначення функції у
functions.php, але не викликали її. Блокfunction ... { }— це лише інструкція. Щоб код виконався, функцію потрібно прив’язати до хука черезadd_action()або запустити вручну через WP-CLI. У розділі «Як запустити», три способи, оберіть під свій рівень доступу до сервера.
Чи видалить функція неприкріплені вкладення-мініатюри використовуваних зображень?
Ні. Мініатюри (thumbnail, medium, large) мають той самий
post_parent, що й оригінальне вкладення. Функція фільтрує строго заpost_parent = 0, тільки записи, у яких узагалі немає батьківського поста. Розміри оригіналів успадковуютьpost_parentі у вибірку не потрапляють. Проблема виникає, лише якщо сам оригінал втратив прив’язку, тоді функція видалить його разом з усіма розмірами.
Що робити, якщо частина зображень у довільних полях, а частина, як звичайні вкладення?
Комбінуйте підходи з розділів 4 і 5. Спочатку зберіть білий список із довільних полів (розділ 5.1). Потім під час сканування uploads (розділ 4) для кожного файлу перевіряйте обидві умови: чи є файл вкладенням WordPress через
attachment_url_to_postid()І чи присутній він у білому списку. Файл видаляється, тільки якщо не виконується жодна з умов:if ( ! $attachment_id && ! isset( $good_pictures[ $image_url ] ) ) { unlink( $image_path ); }.
Наскільки це безпечно для WooCommerce-сайту?
WooCommerce зберігає зображення товарів як стандартні вкладення WordPress, вони прикріплені до поста типу
product. Функція видалення неприкріплених вкладень (розділ 1) їх не зачепить. А от функція для конкретного CPT (розділ 2), так, якщо передати'product'. Для WooCommerce найбезпечніше використовувати метод із розділу 5 (білий список): він оперує тим, що реально використовується, а не тим, що прикріплено. Перед запуском зробіть вивантаження ID усіх продуктових вкладень для звіряння.
Що ставити на своєму проєкті: підсумковий розклад
Вибір методу залежить від архітектури сайту:
- Стандартний блог або новинний сайт, вистачить функцій із розділу 1 (неприкріплені вкладення) та розділу 3 (404-вкладення). Запустіть раз на пів року, медіатека буде в порядку.
- Сайт зі старими CPT (портфоліо, каталог, дошка оголошень), додайте розділ 2. Точково вичистіть сміття від видалених або закинутих типів записів.
- Проєкт на ACF/Meta Box із кастомними полями для зображень, ваш варіант: розділ 5. Зберіть білий список, решту видаліть. Один раз налаштувати, потім тільки повторювати за потреби.
- Усе разом і незрозуміло що, почніть із рекурсивного сканування uploads (розділ 4). Подивіться, скільки сміття лежить на диску. Потім точково застосуйте розділи 1-3 і 5 за ситуацією.
Жоден із цих скриптів не замінить регулярної гігієни сайту. Але один раз написавши потрібну функцію та зберігши її в документації проєкту, ви зекономите години ручної роботи під час наступної ревізії.
І так, бекап ви вже зробили.




