WordPress attachments και διαγραφή περιεχομένου

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.