In WordPress, deleting content does not mean deleting its files.
When a post, product, or custom post type is removed, the associated media files remain in the Media Library. The featured image, gallery images, and any uploaded assets continue to exist unless explicitly removed. This is not a bug. It is a deliberate design choice.
On small websites, this behavior is rarely noticeable. On production systems with volume, editorial workflows, or e-commerce catalogs, it slowly turns into technical debt.
Why WordPress keeps attachments by default
WordPress has no reliable way to know how a file is used across a site.
An image may be:
- a featured image for multiple posts
- reused inside content blocks
- referenced in custom fields or theme logic
- shared across products or variations
Automatically deleting files when content is removed would introduce a high risk of data loss. As a result, WordPress chooses safety over cleanliness and leaves attachments untouched.
This is a reasonable default. It is also insufficient once a site grows.
When this becomes a real problem
In long-running production environments, orphaned attachments create measurable impact:
- disk usage grows without corresponding content
- backups become heavier and slower
- migrations move unnecessary files
- the Media Library becomes noisy and harder to manage
- the database accumulates unused metadata
The issue is not only storage. It is system hygiene. Over time, small inefficiencies compound.
A conscious constraint: deleting only the featured image
Trying to automatically clean all attachments related to a post is risky. A safer approach is to start with a narrow, well-defined rule:
Delete the featured image only when the content is permanently deleted, and only if that image is not used elsewhere as a featured image.
This assumption holds true for many blogs, editorial sites, and structured content systems where each post owns its main image.
Timing matters: permanent deletion only
Any deletion logic should run only when content is removed permanently, not when it is sent to Trash. WordPress provides hooks that allow interception at that exact point.
The following implementation uses before_delete_post, which fires only when a post is about to be deleted for good.
Safe deletion of a featured image
This version checks whether the featured image is used as a featured image by any other post. If it is, the image is preserved.
add_action('before_delete_post', 'px_delete_featured_image_with_post_safe');
function px_delete_featured_image_with_post_safe($post_id) {
$thumbnail_id = get_post_thumbnail_id($post_id);
if (!$thumbnail_id) {
return;
}
// Έλεγχος: χρησιμοποιείται αυτή η εικόνα ως featured image αλλού;
$q = new WP_Query([
'post_type' => 'any',
'post_status' => 'any',
'posts_per_page' => 1, // μας αρκεί να βρούμε 1
'fields' => 'ids',
'meta_query' => [
[
'key' => '_thumbnail_id',
'value' => (string) $thumbnail_id,
],
],
'post__not_in' => [$post_id], // αγνοούμε το post που διαγράφεται
'no_found_rows' => true,
]);
if ($q->have_posts()) {
// Χρησιμοποιείται αλλού, δεν τη διαγράφουμε.
return;
}
// Δεν βρέθηκε αλλού ως featured image, άρα είναι ασφαλές να τη σβήσουμε.
wp_delete_attachment($thumbnail_id, true);
}
This approach is intentionally conservative. It does not attempt to detect all possible usages of an image. It enforces a clear rule with known limits.
What this solves, and what it does not
This logic:
- keeps the filesystem cleaner
- avoids deleting images still in use as featured images
- works across posts, products, and custom post types
- respects the Trash lifecycle
It does not guarantee that the image is unused everywhere. Images referenced inside content, page builders, or custom fields cannot be reliably detected without much heavier logic.
That trade-off is intentional.
Deleting a WooCommerce product and handling product images
E-commerce adds another layer of complexity. A WooCommerce product can have:
- a featured image
- a gallery of images
- variation-level images
When a product is deleted permanently, leaving all of these behind often makes little sense, especially in catalogs with high turnover.
The following implementation handles this case explicitly.
Behavior and assumptions
When a product is permanently deleted:
- the featured image is considered for deletion
- all gallery images are considered for deletion
- variation images are included
- each image is deleted only if it is not used elsewhere as a featured image
This keeps the logic predictable and avoids unintended side effects.
add_action('before_delete_post', 'px_wc_delete_product_images_on_delete');
function px_wc_delete_product_images_on_delete($post_id) {
// Μόνο WooCommerce products
if (get_post_type($post_id) !== 'product') {
return;
}
// Αν για κάποιο λόγο δεν υπάρχει WC (π.χ. απενεργοποιήθηκε), μην κάνεις τίποτα
if (!class_exists('WooCommerce')) {
return;
}
// WooCommerce product object
$product = wc_get_product($post_id);
if (!$product) {
return;
}
$attachment_ids = [];
// 1) Featured image
$thumbnail_id = (int) get_post_thumbnail_id($post_id);
if ($thumbnail_id) {
$attachment_ids[] = $thumbnail_id;
}
// 2) Product gallery images
$gallery_ids = $product->get_gallery_image_ids();
if (!empty($gallery_ids)) {
foreach ($gallery_ids as $gid) {
$gid = (int) $gid;
if ($gid) {
$attachment_ids[] = $gid;
}
}
}
// 3) (Προαιρετικό αλλά χρήσιμο) Variation images
// Αν το product είναι variable, κάθε variation μπορεί να έχει δική του εικόνα.
if ($product->is_type('variable')) {
foreach ($product->get_children() as $variation_id) {
$variation_product = wc_get_product($variation_id);
if (!$variation_product) {
continue;
}
$vid = (int) $variation_product->get_image_id();
if ($vid) {
$attachment_ids[] = $vid;
}
}
}
// Καθάρισε duplicates
$attachment_ids = array_values(array_unique(array_filter($attachment_ids)));
if (empty($attachment_ids)) {
return;
}
// Διαγραφή attachments με safeguard:
// Σβήνουμε μόνο όσα ΔΕΝ χρησιμοποιούνται αλλού ως featured image.
foreach ($attachment_ids as $attachment_id) {
if (px_attachment_is_used_as_featured_elsewhere($attachment_id, $post_id)) {
continue;
}
wp_delete_attachment($attachment_id, true);
}
}
/**
* Ελέγχει αν ένα attachment χρησιμοποιείται ως featured image σε άλλο post/product.
* Σημείωση: Δεν καλύπτει χρήση μέσα στο content ή σε custom fields.
*/
function px_attachment_is_used_as_featured_elsewhere($attachment_id, $current_post_id) {
$q = new WP_Query([
'post_type' => 'any',
'post_status' => 'any',
'posts_per_page' => 1,
'fields' => 'ids',
'meta_query' => [
[
'key' => '_thumbnail_id',
'value' => (string) (int) $attachment_id,
],
],
'post__not_in' => [(int) $current_post_id],
'no_found_rows' => true,
]);
return $q->have_posts();
}
Important limitations
This logic protects against reuse as a featured image. It does not detect usage:
- inside product descriptions
- in ACF fields or theme options
- across shared media strategies
If your store relies on shared assets across multiple products, automatic deletion is not appropriate. In those cases, periodic audits or manual cleanup workflows are safer.