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

Στο WordPress, η διαγραφή ενός άρθρου δεν σημαίνει και διαγραφή των αρχείων που το συνοδεύουν. Όταν σβήνουμε ένα post, ένα προϊόν ή οποιοδήποτε custom post type, η χαρακτηριστική εικόνα (featured image) παραμένει στη Media Library. Αυτό είναι default συμπεριφορά και δεν είναι bug.

Σε μικρά sites, αυτό συνήθως περνά απαρατήρητο. Σε sites όμως με όγκο περιεχομένου, editorial ομάδες ή e-commerce εγκαταστάσεις, το πρόβλημα δεν είναι αμελητέο. Αντίθετα, συσσωρεύεται αργά και αθόρυβα, μέχρι να γίνει τεχνικό βάρος.

Γιατί το WordPress δεν σβήνει τα αρχεία από μόνο του

Η επιλογή του WordPress να μην διαγράφει attachments μαζί με το post είναι συνειδητή. Ένα αρχείο μπορεί να χρησιμοποιείται:

  • σε περισσότερα από ένα άρθρα
  • ως featured image σε διαφορετικά post
  • μέσα σε blocks, galleries ή custom fields

Αν το σύστημα διέγραφε αυτόματα τα αρχεία, θα υπήρχε σοβαρός κίνδυνος διαγραφής αρχείου ή εικόνας η οποία χρησιμοποιείται αλλού. Αυτό σημαίνει ότι το WordPress ακολουθει την ασφαλή οδό. Δεν γνωρίζει το context κάθε site, οπότε αφήνει την ευθύνη στον developer.

Πότε αυτό γίνεται πραγματικό πρόβλημα

Σε production περιβάλλοντα με μεγάλο αριθμό posts, τα orphaned attachments αρχίζουν να έχουν κόστος:

  • Storage: χιλιάδες εικόνες που δεν χρησιμοποιούνται πουθενά
  • Database noise: κάθε upload δημιουργεί εγγραφές στη βάση
  • Backups: μεγαλύτερα αρχεία, περισσότερος χρόνος
  • Migrations: media που μεταφέρονται χωρίς λόγο
  • Media Library usability: δύσκολη διαχείριση για editors

Το πρόβλημα δεν είναι ότι “πιάνει χώρο”. Είναι ότι χαλάει την καθαρότητα του συστήματος.

Η συνειδητή απόφαση: διαγράφουμε μόνο τη featured image

Η πιο ασφαλής και ελεγχόμενη προσέγγιση δεν είναι να σβήνουμε όλα τα attachments ενός post. Είναι να σβήνουμε μόνο τη featured image, με την προϋπόθεση ότι:

  • το site δεν επαναχρησιμοποιεί featured images αλλού
  • η λογική του περιεχομένου είναι 1:1 (ένα άρθρο, μία εικόνα)

Σε editorial sites και blogs, αυτό είναι συχνά απολύτως λογικό.

Πότε γίνεται η διαγραφή: κρίσιμο σημείο

Η διαγραφή πρέπει να γίνεται μόνο όταν το post διαγράφεται οριστικά, όχι όταν πηγαίνει στα διεγραμμένα (Trash). Ευτυχώς το WordPress παρέχει hooks που μας επιτρέπουν να επέμβουμε ακριβώς σε αυτή τη στιγμή.

Ο παρακάτω κώδικας χρησιμοποιεί το hook before_delete_post και διαγράφει τη featured image όταν το post διαγράφεται οριστικά.

PHP
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);
}

Διαγραφή προϊόντος στο WooCommerce και διαχείριση των product images

Παρακάτω είναι μια εκδοχή που στοχεύει μόνο προϊόντα WooCommerce (product) και, όταν διαγράφεται οριστικά το προϊόν, διαγράφει:

  • featured image
  • όλες τις εικόνες του product gallery
  • προαιρετικά (προτείνεται) την εικόνα του variation, αν υπάρχουν variations

Και σε κάθε περίπτωση, πριν σβήσει attachment κάνει έλεγχο αν χρησιμοποιείται αλλού ως featured image σε άλλο post/product, ώστε να μη “σπάσεις” άλλο περιεχόμενο.

PHP
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();
}

Προσοχή

Με αυτόν τον κώδικα μειώνεις δραματικά τον κίνδυνο να σπάσει άλλο περιεχόμενο. ΣΗΜΑΝΤΙΚΟ: Πρέπει πάντα λαμβάνουμε υπόψιν οτι κάθε ιστοσελίδα είναι σχεδιασμένη διαφορετικά και σε κάθε περίπτωση αυτό θα πρέπει να γίνεται με προσοχή και πιθανότατα σε συνεννόηση με τον άνθρωπο ή την εταιρεία που έφτιαξε την ιστοσελίδα για να αποφύγουμε δυσάρεστα αποτελέσματα.

Αυτός ο έλεγχος προστατεύει από reuse ως featured image (ή και galleries στην περίπτωση των προϊόντων), αλλά δεν μπορεί να εγγυηθεί ότι η εικόνα δεν χρησιμοποιείται αλλού (π.χ. μέσα σε περιεχόμενο, ACF fields, builders κλπ).