
🗑 Nettoyage programmatique de la médiathèque WordPress : scripts PHP pour supprimer les fichiers indésirables
La médiathèque WordPress fonctionne comme un grenier: vous supprimez un article, les images restent. Vous changez de thème, les anciennes tailles d’images restent là comme du poids mort. Vous migrez le site, la moitié des vignettes renvoient une erreur 404.
Le nettoyage manuel via le panneau d’administration sur un site comptant quelques milliers de fichiers est un exercice de méditation. Mais il existe une méthode plus rapide: cinq fonctions PHP qui trouvent et suppriment les fichiers inutiles en un seul passage. Sans plugins, avec un code clair et un contrôle total sur chaque fichier supprimé.
Avant de lancer quoi que ce soit, faites une sauvegarde complète. Ces fonctions suppriment les fichiers de manière permanente: pas de corbeille, pas d’annulation possible. En cas de doute, exécutez-les d’abord sur une copie de staging.
💡 Aperçu rapide:
- Suppression des pièces jointes non attachées, les fichiers laissés après la suppression d’un article
- Nettoyage des fichiers médias d’un type de publication personnalisé (CPT) spécifique
- Vidage de la médiathèque des liens brisés, les pièces jointes 404 sans fichier sur le serveur
- Recherche et suppression des fichiers dans
wp-content/uploadsnon enregistrés comme pièces jointes WordPress - Scénario pour les sites où les images sont stockées dans des champs personnalisés (ACF, Meta Box), et non comme pièces jointes
Ce que vous devez savoir avant de lancer le script
Le code ci-dessous supprime physiquement les fichiers, du disque et de la base de données. Trois éléments qui sauveront votre site.
Premièrement: les images présentes sur les pages d’archives d’étiquettes ou dans les descriptions SEO ne sont souvent attachées à aucun article. Elles apparaissent comme «orphelines», mais le site en a besoin. Si vous avez de telles images, excluez-les du périmètre de ces fonctions ou ajustez les conditions.
Deuxièmement: WordPress crée plusieurs tailles pour chaque image. Les vignettes héritent du post_parent de l’original, donc la fonction delete_unattached_attachments() ne les touche pas, elle filtre strictement sur post_parent = 0. Le problème ne se pose que si l’original lui-même a perdu son rattachement à l’article.
Troisièmement: si un lien vers le fichier supprimé existe dans le contenu d’un article, il sera cassé après le nettoyage. Avant de lancer le script, explorez le site avec Screaming Frog ou un outil similaire et cartographiez les liens.
1. Supprimer les pièces jointes non attachées
Scénario le plus courant: vous avez supprimé un article, les pièces jointes sont restées dans la base de données avec post_type = 'attachment' et post_parent = 0. Elles occupent de l’espace sur le disque et dans les sauvegardes.
La fonction ci-dessous trouve tous ces enregistrements et les supprime. Placez-la dans le fichier functions.php d’un thème enfant ou via un plugin d’extraits de code comme WPCode. Elle ne s’exécute pas toute seule, c’est une définition qui a besoin d’un appel.
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() sélectionne toutes les pièces jointes sans article parent. wp_delete_attachment() avec le paramètre true efface à la fois l’enregistrement en base de données et le fichier avec ses vignettes. Le unlink() supplémentaire est une sécurité: si le fichier est resté sur le disque pour une raison quelconque, il est supprimé de force.
Remarque: les images à la une ont également post_parent = 0 dans certaines configurations. Avant un lancement en production, remplacez wp_delete_attachment par echo $attachment_id . '<br>', vous verrez la liste des IDs qui seront supprimés. Une fois que vous avez confirmé que tout est correct, revenez à la version de production.
Après une seule exécution, retirez la fonction de functions.php. Inutile de la conserver à chaque init.
2. Supprimer les pièces jointes d’un CPT spécifique
Ancienne boutique WooCommerce, ancienne section portfolio, type de publication personnalisé supprimé: toutes leurs images continuent de résider sur le serveur. La fonction ci-dessous nettoie les pièces jointes attachées aux publications d’un type spécifié.
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 }
Remplacez 'card' par le slug de votre CPT. Pour les produits WooCommerce, 'product'. Si le CPT est déjà supprimé, get_post_type() renverra false, les pièces jointes de ce type ne seront pas affectées. Pour les CPT supprimés, la logique doit être ajustée: vérifiez non pas le type du parent, mais l’appartenance à une taxonomie ou un champ meta.
Sur les grandes bases de données, soyez prudent: 'numberposts' => -1 sans 'fields' => 'ids' charge des objets WP_Post complets. Sur plus de 10 000 pièces jointes, cela peut atteindre la memory_limit. Pour des volumes de production, ajoutez 'fields' => 'ids' et ne récupérez que les IDs, get_post_type() fonctionnera aussi avec les IDs parents.
3. Vider la médiathèque des pièces jointes 404
Les vignettes cassées dans la médiathèque sont le symptôme que le fichier sur le disque a été supprimé (manuellement, par un crash d’hébergement ou un plugin buggé), mais que l’enregistrement en base de données est resté. WordPress affiche un rectangle gris, mais au clic, c’est une erreur 404.
La fonction interroge l’URL de chaque pièce jointe et supprime celles qui renvoient une 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 }
Important: cette fonction est gourmande en ressources. Chaque appel get_headers() est une requête HTTP vers votre propre serveur. Sur un millier de pièces jointes, vous faites un millier de requêtes HTTP en un seul passage. Résultat: lenteur, charge serveur, certains hébergeurs tuent le processus par timeout.
Pour les grandes médiathèques, découpez par blocs avec 'offset' et 'numberposts' ou exécutez via WP-CLI avec une limite de lot. Si le site est derrière un CDN ou un proxy, remplacez la vérification par wp_remote_head() avec un timeout, get_headers() ne gère pas toujours correctement les redirections et ne supporte pas l’authentification.
4. Vérification inverse: les fichiers dans uploads sans enregistrement en base de données
Les trois fonctions précédentes nettoient la base de données, elles suppriment les enregistrements de pièces jointes. Mais wp-content/uploads peut contenir des fichiers qui ne sont pas du tout enregistrés comme pièces jointes: uploadés via FTP, laissés par des plugins, générés par le cache.
Cette fonction procède dans le sens inverse: non pas de la base de données vers les fichiers, mais des fichiers vers la base de données. Elle scanne récursivement wp-content/uploads et pour chaque fichier vérifie via attachment_url_to_postid() s’il s’agit d’une pièce jointe. Si ce n’est pas le cas, elle le supprime.
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 }
Sur un serveur de test avec 1 Go d’uploads, la fonction s’est exécutée en 15 secondes et a libéré 700 Mo, laissant 300 Mo de fichiers réellement utilisés. Pour les dossiers de plus de 5 Go, segmentez le scan par années: remplacez $root par $uploads_dir['basedir'] . '/2025/', puis '/2024/' et ainsi de suite.
Lancez d’abord la version sans suppression, remplacez unlink( $image_path ) par echo $image_path . PHP_EOL. Vous verrez la liste complète des fichiers que la fonction considère comme inutiles. Vérifiez visuellement, puis revenez à unlink().
5. Scénario avec champs personnalisés: quand les images ne sont pas des pièces jointes
Le cas le plus complexe: un site où les images ne sont pas stockées comme pièces jointes WordPress, mais comme URLs dans des champs personnalisés (ACF, Meta Box, champs de thème personnalisé). Exemple typique, une librairie: couverture de livre dans le champ bookcover, photo de l’auteur dans bookauthor_picture, image de liste dans book_list_pictrue.
Dans cette architecture, pour tous les fichiers dans uploads, attachment_url_to_postid() renverra 0. La fonction précédente supprimera tout, y compris les images réellement utilisées. Une approche différente est nécessaire.
5.1. Construire une liste blanche
Collectez d’abord les URLs de toutes les images de tous les champs personnalisés nécessaires. Dans l’exemple ci-dessous, trois CPTs et trois champs:
1 $all_good_pictures = array(); 2 3 // Book covers (CPT 'post', field '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 // List images (CPT 'book_list', field '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 // Author photos (CPT 'bookauthor', field '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 );
Sur un projet réel, une librairie, cette approche a permis de calculer la plupart des fichiers inutiles et de libérer une part significative de l’espace disque.
5.2. Supprimer tout ce qui n’est pas dans la liste blanche
Parcourez maintenant wp-content/uploads et supprimez chaque fichier qui n’est pas dans $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 }
La méthode in_array() avec comparaison stricte sur un tableau de plus de 10 000 éléments n’est pas la plus rapide. Pour des volumes de production, remplacez le tableau classique par un tableau associatif: $all_good_pictures = array_fill_keys( $all_good_pictures, true ) et vérifiez via isset(). La différence sur 40 000 éléments passe de dizaines de secondes à une fraction de seconde.
Comment exécuter ces fonctions
Tous les extraits ci-dessus sont des définitions de fonctions. Elles ne font rien tant que vous ne les appelez pas. Trois méthodes sûres pour les exécuter:
Méthode | Quand l’utiliser | Retour arrière |
|---|---|---|
WP-CLI | Nettoyage ponctuel, accès console | Non, seulement la sauvegarde |
Hook | Pas de console, besoin de lancer depuis l’admin | Non, seulement la sauvegarde |
Plugin d’extraits (WPCode) | Stockage pratique et activation/désactivation | Désactiver l’extrait, fonction inactive |
Exemple d’exécution ponctuelle via l’admin:
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 }
Visitez https://yoursite.com/wp-admin/?cleanup=confirmed, la fonction s’exécute une fois. Après exécution, retirez l’extrait.
Pour WP-CLI, la méthode recommandée en production, enregistrez le code de la fonction dans un fichier temporaire et exécutez:
1 wp eval-file cleanup.php
Avant le nettoyage, il est utile de visualiser le processus. Dans la vidéo ci-dessous, une décomposition étape par étape du nettoyage de la médiathèque WordPress avec des méthodes manuelles et automatiques.
⁉️🤔 Foire aux questions
Les fichiers peuvent-ils être restaurés après suppression?
Non. Les fonctions utilisent
wp_delete_attachment()avectrueetunlink(), les fichiers sont supprimés physiquement, en contournant la corbeille. La seule sécurité: une sauvegarde complète des fichiers et de la base de données avant exécution. Vérifiez si votre hébergeur propose des sauvegardes quotidiennes automatiques, chez Kinsta, WP Engine et SiteGround elles sont activées par défaut. Cela vous donne un point de restauration supplémentaire en plus de votre sauvegarde manuelle.
Pourquoi la fonction n’a-t-elle pas fonctionné, les fichiers sont restés en place?
Raison la plus courante: vous avez ajouté la définition de la fonction à
functions.php, mais vous ne l’avez pas appelée. Un blocfunction ... { }n’est qu’une instruction. Pour que le code s’exécute, la fonction doit être accrochée à un hook viaadd_action()ou exécutée manuellement via WP-CLI. Dans la section «Comment exécuter ces fonctions», trois méthodes, choisissez en fonction de votre niveau d’accès au serveur.
La fonction supprimera-t-elle les vignettes des images utilisées si ce sont des pièces jointes non attachées?
Non. Les vignettes (thumbnail, medium, large) ont le même
post_parentque la pièce jointe originale. La fonction filtre strictement surpost_parent = 0, uniquement les enregistrements sans aucun article parent. Les tailles des originaux héritent dupost_parentet ne tombent pas dans la sélection. Le problème ne se pose que si l’original lui-même a perdu son rattachement, alors la fonction le supprimera avec toutes ses tailles.
Que faire si certaines images sont dans des champs personnalisés et d’autres sont des pièces jointes classiques?
Combinez les approches des sections 4 et 5. Collectez d’abord une liste blanche depuis les champs personnalisés (section 5.1). Ensuite, lors du scan des uploads (section 4), pour chaque fichier, vérifiez les deux conditions: le fichier est-il une pièce jointe WordPress via
attachment_url_to_postid()ET est-il présent dans la liste blanche. Un fichier n’est supprimé que si aucune des deux conditions n’est remplie:if ( ! $attachment_id && ! isset( $good_pictures[ $image_url ] ) ) { unlink( $image_path ); }.
Est-ce sûr pour un site WooCommerce?
WooCommerce stocke les images des produits comme des pièces jointes WordPress standard, elles sont attachées au type de publication
product. La fonction de suppression des pièces jointes non attachées (section 1) ne les touchera pas. En revanche, la fonction pour un CPT spécifique (section 2), si, si vous passez'product'. Pour WooCommerce, le plus sûr est la méthode de la section 5 (liste blanche): elle opère sur ce qui est réellement utilisé, pas sur ce qui est attaché. Avant exécution, exportez les IDs de toutes les pièces jointes des produits pour une contre-vérification.
Quoi utiliser sur votre projet: récapitulatif final
Le choix de la méthode dépend de l’architecture du site:
- Blog standard ou site d’actualités, les fonctions de la section 1 (pièces jointes non attachées) et de la section 3 (pièces jointes 404) suffisent. Exécutez-les une fois tous les six mois, la médiathèque se portera bien.
- Site avec d’anciens CPTs (portfolio, catalogue, petites annonces), ajoutez la section 2. Nettoyez précisément les fichiers inutiles des types de publication supprimés ou abandonnés.
- Projet sous ACF/Meta Box avec champs personnalisés pour les images, votre option: la section 5. Collectez une liste blanche, supprimez le reste. Configurez une fois, puis répétez simplement selon les besoins.
- Tout ensemble et situation floue, commencez par le scan récursif des uploads (section 4). Voyez combien de fichiers inutiles occupent le disque. Appliquez ensuite les sections 1 à 3 et 5 de manière sélective selon la situation.
Aucun de ces scripts ne remplace une hygiène régulière du site. Mais une fois que vous avez écrit la fonction nécessaire et que vous l’avez sauvegardée dans la documentation du projet, vous économiserez des heures de travail manuel lors du prochain audit.
Et oui, vous avez déjà fait une sauvegarde.




