التعيين الجماعي للصور المميزة المفقودة في ووردبريس

No comments

05.09.2026|

No comments

Tiempo de lectura Lectura: 7 min, 1 s
Número de palabras Palabras: 1299
Número de visitas Visitas: 22
Icono de traducción
تعيين الصور المميزة المفقودة بشكل جماعي في ووردبريس. قائمة تُظهر الصور المفقودة والموجودة على حد سواء.

قبل بضعة أيام، انتهيت من مهمة طال انتظارها وكان لا بد من إنجازها، لكنني كنت أؤجلها منذ فترة طويلة. قمتباستعادة جميعالصور المميزة المفقودة وإضافة النص البديل المفقود إلى الصور. كما انتهيت من كتابة جميع الأوصاف التعريفية المفقودة. إجمالًا، أضفت أو عدلت أكثر من 15,000 سجل باستخدام ثلاثة من مقتطفاتي البرمجية التي صممتها بنفسي لمرة واحدة. اليوم، أشارك هنا المقتطف البرمجي لاستعادة الصور المميزة المفقودة، والذي استخدمته أيضًا لتنفيذ هذه المهمة على مواقع أخرى.

إذا كنت تكتب مدونة منذ فترة، فمن المرجح أن يكون لديك عدد لا بأس به من المنشورات القديمة التي لم تُخصص لها صورة مميزة. كان هذا يحدث كثيرًا في أيام المحرر الكلاسيكي، عندما كنا ندرج الصورة مباشرةً في نص المنشور ولكننا ننسى تحديدها في مربع «الصورة المميزة».

إن نشر المقالات دون صورة مميزة ليس مجرد مسألة جمالية، من بين أمور أخرى؛ بل إنه يؤثر بشكل مباشر على تحسين محركات البحث لموقعك وتجربة المستخدم:

  1. تحسين محركات البحث على وسائل التواصل الاجتماعي و«أوبن غراف»: عند مشاركة المنشور على وسائل التواصل الاجتماعي أو تطبيقات المراسلة أو منصات تجميع المحتوى، لا تتمكن علامات «أوبن غراف» (og:image) من العثور على مصدر واضح لعرضه، مما يؤدي إلى انخفاض كبير في معدل النقر (CTR).
  2. الاتساق البصري في السمة: تعتمد القوالب الحديثة (مثل تلك المصممة باستخدام GeneratePress أو GenerateBlocks) على الصورة المميزة لإنشاء شبكات الأرشيف أو قوائم المنشورات ذات الصلة أو العناوين. وبدونها، يتعطل التخطيط أو تظهر فجوات غير متوازنة.
  3. الاكتشاف والموجزات (RSS / Google Discover): تُعطي أدوات مثل Google Discover أو برامج قراءة RSS الأولوية للمحتوى المصحوب بصورة مصغرة عالية الدقة، أو تشترط وجودها، من أجل تحسين مظهر بطاقة المقالة.

لحل هذه المشكلة دون اللجوء إلى المكوناتالإضافية (مثل هذه) أو العمليات البطيئة، قمت بإنشاء هذا المقتطف البرمجي البسيط والخفيف الوزن بلغة PHP، والذي يقوم تلقائيًّا بتعيين الصورة الأولى في كل منشور كصورة مميزة، مباشرةً من لوحة تحكم ووردبريس. إذا لم يحتوي منشورك على أي صور، فيمكنك تعيين صورة افتراضية عن طريق إضافة عنوان URL الخاص بها إلى الكود.

ما الذي يفعله هذا المقتطف بالضبط؟

يضيف هذا الكود أداة معالجة مجمعة ذكية ومدمجة إلى قائمة منشورات ووردبريس (wp-admin/edit.php):

  • إضافة عمود مخصص: قم بإنشاء عمود «الصورةالمميزة المفقودة» في قائمة المنشورات لتتمكن من معرفة المنشورات التي تفتقد إلى صورة مصغرة بنظرة سريعة.
  • معالجة ثلاثية المراحل: عند تمكين ميزة التخصيص لمنشور ما، يتم البحث عن الصورة بالترتيب التالي بالضبط:
    1. تحقق مما إذا كانت هناك أي صور مرفقة مباشرةً بالمنشور في مكتبة الوسائط.
    2. في حالة عدم وجود مرفقات مباشرة (على سبيل المثال، إذا تم إدراجها عبر كتلة صورة أو كود HTML)، فابحث في المحتوى عن أول علامة <img> لاستخراج معرّفها باستخدام الدالة attachment_url_to_postid().
    3. إذا لم تحتوِ المشاركة على أي صور على الإطلاق، فإنها تُعيّن صورة افتراضية (احتياطية) محددة في الإعدادات.
  • المعالجة الجماعية عبر AJAX (المعالجة الدفعية): تتضمن زرًا في العنوان («⚡ تعيين كل ما في هذه الصفحة») يقوم بمعالجة جميع الإدخالات الظاهرة على الشاشة بالتسلسل باستخدام طلبات غير متزامنة متتالية. يمكنك أيضًا القيام بذلك يدويًّا باستخدام الزر الفردي الخاص بكل منشور في القائمة.
  • تصفية المسودات: تستبعد تلقائيًا الإدخالات التي لا تزال في حالة «مسودة» حتى تتمكن من التركيز حصريًّا على المحتوى المنشور.
  • عمود قابل للفرز: يتيح لك فرز القائمة بحيث يتم تجميع جميع العناصر التي لا تحتوي على صورة مميزة معًا في الأعلى.

لماذا هو خفيف الوزن إلى هذا الحد، وهل يؤثر ذلك على أداء الخادم؟

من المخاوف الشائعة عند استخدام هذا النوع من الأدوات الآلية الخوف من تجميد قاعدة البيانات أو إثقال كاهل ذاكرة PHP في المواقع التي تحتوي على آلاف المقالات. وقد صُمم هذا المقتطف وفقًا لمعايير التحسين التالية:

1. لا يعمل إلا في قسم الإدارة (is_admin())

تم تجميع هذا الكود خصيصًا لشاشة الإدخال في لوحة الإدارة (edit.php). وهو لا يضيف أي عبء إضافي على الطلبات الواردة إلى الموقع العام. ولن يتأثر مستخدموكم ومقاييس السرعة بأي شكل من الأشكال.

2. توجيه الاستعلامات إلى ذاكرة التخزين المؤقتة (Meta Cache)

لتحديد ما إذا كانت إحدى المشاركات تحتوي على صورة أم لا، لا يقوم المقتطف بإجراء عمليات بحث معقدة في قاعدة البيانات لكل صف. بل يستخدم مفتاح البيانات الوصفية _thumbnail_id، الذي يقوم ووردبريس بتحميله إلى الذاكرة بشكل أصلي عبر ذاكرة التخزين المؤقتة للبيانات الوصفية عند عرض القائمة. ويتم تحميل الجدول بشكل شبه فوري.

3. المعالجة غير المتزامنة المتتالية (حلقة AJAX التسلسلية)

بدلاً من تنفيذ استعلام ضخم على قاعدة البيانات لتحديث 700 أو 1,000 سجل دفعة واحدة (مما قد يؤدي إلى انتهاء مهلة الانتظار أو استنفاد ذاكرة الخادم بالكامل)، يقوم زر «تعيين الكل» بمعالجة السجلات واحدًا تلو الآخر باستخدام وعود JavaScript:

POST 1 (AJAX)⟶استجابة "OK"⟶POST 2 (AJAX)⟶استجابة "OK"…

وهذا يضمن استخدامًا مستقرًا ومتسقًا للموارد، مما يتيح لك متابعة التقدم المحرز في الوقت الفعلي، صفًا تلو الآخر.

4. عدم تكرار الملفات

لا يقوم هذا المقتطف بتنزيل الصور أو إعادة تجميعها أو نسخها على القرص الصلب الخاص بك. بل يقت صر دوره على أخذ معرّف الصورة الموجودة بالفعل في مكتبة الوسائط الخاصة بك وربطها بالمنشور عن طريق تعيين القيمة meta_value المقابلة في جدول wp_postmeta.

الكود (PHP + JavaScript)

يمكنك إضافة هذا الكود إلى ملف functions.php الخاص بقالبك الفرعي، أو إلى مكون إضافي مخصص، أو عبر مكون إضافي للوظائف المخصصة مثل Code Snippets أو ما شابهه.

ملاحظة: تذكر أن تضبط المتغير $default_image_url في السطر 56 على عنوان URL الكامل للصورة العامة التي ترغب في تخصيصها للمشاركات التي لا تحتوي على أي صور في النص.

PHP


// 1. Añadir columna personalizada en el listado de entradas
add_filter( 'manage_posts_columns', 'custom_featured_status_column' );
function custom_featured_status_column( $columns ) 
    $columns['featured_missing'] = 'Destacada Faltante';
    return $columns;
}

// 2. Hacer la columna ordenable al hacer clic en el encabezado
add_filter( 'manage_edit-post_sortable_columns', 'custom_featured_sortable_column' );
function custom_featured_sortable_column( $columns ) {
    $columns['featured_missing'] = 'featured_missing';
    return $columns;
}

// 3. Modificar la consulta para ordenar por presencia de _thumbnail_id
add_action( 'pre_get_posts', 'custom_featured_column_orderby' );
function custom_featured_column_orderby( $query ) {
    if ( ! is_admin() || ! $query->is_main_query() ) return;

    if ( $query->get( 'orderby' ) === 'featured_missing' ) {
        $query->set( 'meta_key', '_thumbnail_id' );
        $query->set( 'orderby', 'meta_value_num' );
        $query->set( 'meta_compare', 'NOT EXISTS' );
    }
}

// 3.1. Añadir desplegable de filtro nativo en el listado de entradas
add_action( 'restrict_manage_posts', 'custom_featured_filter_dropdown' );
function custom_featured_filter_dropdown( $post_type ) {
    if ( $post_type !== 'post' ) return;

    $selected = isset( $_GET['filter_featured_status'] ) ? sanitize_text_field( $_GET['filter_featured_status'] ) : '';
    ?>
    <select name="filter_featured_status" id="filter_featured_status">
        <option value=""><?php _e( 'Todas las destacadas', 'textdomain' ); ?></option>
        <option value="has_img" <?php selected( $selected, 'has_img' ); ?>>Tiene</option>
        <option value="missing_img" <?php selected( $selected, 'missing_img' ); ?>>Falta</option>
    </select>
    <?php
}

// 3.2. Aplicar el filtro del desplegable a la consulta de WP_Query
add_filter( 'parse_query', 'custom_featured_filter_apply' );
function custom_featured_filter_apply( $query ) {
    global $pagenow;
    if ( ! is_admin() || $pagenow !== 'edit.php' || ! $query->is_main_query() ) return;

    if ( isset( $_GET['filter_featured_status'] ) && $_GET['filter_featured_status'] !== '' ) {
        $filter_value = sanitize_text_field( $_GET['filter_featured_status'] );
        $meta_query   = (array) $query->get( 'meta_query' );

        if ( $filter_value === 'has_img' ) {
            $meta_query[] = array(
                'key'     => '_thumbnail_id',
                'compare' => 'EXISTS',
            );
        } elseif ( $filter_value === 'missing_img' ) {
            $meta_query[] = array(
                'key'     => '_thumbnail_id',
                'compare' => 'NOT EXISTS',
            );
        }

        $query->set( 'meta_query', $meta_query );
    }
}

// 4. Mostrar el estado y el botón rápido en la columna (Excluyendo borradores)
add_action( 'manage_posts_custom_column', 'custom_featured_status_column_content', 10, 2 );
function custom_featured_status_column_content( $column, $post_id ) {
    if ( $column === 'featured_missing' ) {
        if ( get_post_type( $post_id ) !== 'post' || get_post_status( $post_id ) === 'draft' ) {
            echo '<span style="color: #999;">—</span>';
            return;
        }

        $has_thumbnail = get_post_meta( $post_id, '_thumbnail_id', true );

        if ( $has_thumbnail ) {
            echo '<span style="color: #46b450; font-weight: bold;">✔ Tiene</span>';
        } else {
            echo '<span style="color: #dc3232; font-weight: bold;">❌ Falta</span> ';
            echo '<button type="button" class="button button-small set-first-img-btn" data-postid="' . esc_attr( $post_id ) . '">Asignar imagen</button>';
            echo '<span class="set-img-status" style="margin-left:5px;"></span>';
        }
    }
}

// 5. Procesar la asignación por AJAX (con fallback a la imagen por defecto)
add_action( 'wp_ajax_set_first_image_as_featured', 'ajax_set_first_image_as_featured' );
function ajax_set_first_image_as_featured() {
    check_ajax_referer( 'set_first_image_nonce', 'nonce' );

    $post_id = isset( $_POST['post_id'] ) ? intval( $_POST['post_id'] ) : 0;
    if ( ! $post_id || ! current_user_can( 'edit_post', $post_id ) ) {
        wp_send_json_error( 'Sin permisos o ID inválido' );
    }

    if ( get_post_status( $post_id ) === 'draft' ) {
        wp_send_json_error( 'La entrada es un borrador' );
    }

    // =========================================================================
    // CONFIGURACIÓN: Reemplaza esta URL por la URL completa de tu imagen por defecto
    // =========================================================================
    $default_image_url = 'https://tu-dominio.com/wp-content/uploads/imagen-por-defecto.jpg';

    // A. Buscar adjuntos directos al post
    $attachments = get_posts( array(
        'post_type'      => 'attachment',
        'posts_per_page' => 1,
        'post_parent'    => $post_id,
        'post_mime_type' => 'image',
        'orderby'        => 'menu_order',
        'order'          => 'ASC',
        'fields'         => 'ids'
    ) );

    if ( ! empty( $attachments ) ) {
        set_post_thumbnail( $post_id, $attachments[0] );
        wp_send_json_success( '¡Asignada primera!' );
    }

    // B. Si no hay adjuntos, buscar <img> en el contenido
    $post = get_post( $post_id );
    preg_match_all( '/<img.+?src=[\'"]([^\'"]+)[\'"]/i', $post->post_content, $matches );

    if ( isset( $matches[1][0] ) ) {
        $attachment_id = attachment_url_to_postid( $matches[1][0] );
        if ( $attachment_id ) {
            set_post_thumbnail( $post_id, $attachment_id );
            wp_send_json_success( '¡Asignada primera!' );
        }
    }

    // C. Si no hay ninguna imagen en el post, asignar la imagen por defecto
    if ( ! empty( $default_image_url ) ) {
        $default_id = attachment_url_to_postid( $default_image_url );

        if ( $default_id ) {
            set_post_thumbnail( $post_id, $default_id );
            wp_send_json_success( '¡Asignada por defecto!' );
        }
    }

    wp_send_json_error( 'No se encontró ninguna imagen en el post ni ID de fallback' );
}

// 6. Cargar JS y botón masivo en el admin (edit.php)
add_action( 'admin_footer-edit.php', 'custom_featured_ajax_script' );
function custom_featured_ajax_script() {
    $screen = get_current_screen();
    if ( $screen->post_type !== 'post' ) return;
    
    $nonce = wp_create_nonce( 'set_first_image_nonce' );
    ?>
    <script>
    jQuery(document).ready(function($) {
        $('<button type="button" id="process-all-page-btn" class="page-title-action" style="margin-left:10px; background:#2271b1; color:#fff; border-color:#2271b1;">⚡ Asignar todas en esta página</button>')
            .insertAfter('.wp-heading-inline');

        function processButton($btn) {
            return new Promise(function(resolve) {
                var $status = $btn.siblings('.set-img-status');
                var postId = $btn.data('postid');

                $btn.prop('disabled', true).text('Procesando...');

                $.post(ajaxurl, {
                    action: 'set_first_image_as_featured',
                    post_id: postId,
                    nonce: '<?php echo $nonce; ?>'
                }, function(response) {
                    if (response.success) {
                        $btn.parent().html('<span style="color: #46b450; font-weight: bold;">✔ ' + response.data + '</span>');
                    } else {
                        $status.css('color', '#dc3232').text(response.data);
                        $btn.prop('disabled', false).text('Asignar imagen');
                    }
                    resolve();
                }).fail(function() {
                    $btn.prop('disabled', false).text('Error');
                    resolve();
                });
            });
        }

        $(document).on('click', '.set-first-img-btn', function(e) {
            e.preventDefault();
            processButton($(this));
        });

        $('#process-all-page-btn').on('click', async function(e) {
            e.preventDefault();
            var $batchBtn = $(this);
            var $pendingButtons = $('.set-first-img-btn:not(:disabled)');

            if ($pendingButtons.length === 0) {
                alert('No hay entradas pendientes de asignar en esta página.');
                return;
            }

            $batchBtn.prop('disabled', true).text('Procesando por lote...');

            for (var i = 0; i < $pendingButtons.length; i++) {
                await processButton($($pendingButtons[i]));
            }

            $batchBtn.text('¡Lote completado!').css('background', '#46b450');
            setTimeout(function() {
                $batchBtn.prop('disabled', false).text('⚡ Asignar todas en esta página').css('background', '#2271b1');
            }, 3000);
        });
    });
    </script>
    <?php
}

كيف يتم استخدامه؟

  • انتقل إلى قائمة «المنشورات» في لوحة الإدارة الخاصة بك.
  • افتح علامة التبويب «خيارات العرض» في الأعلى وقم بتغيير عدد العناصر المراد عرضها إلى 50 أو 100 عنصرًا في كل صفحة. (ابدأ بتجربة أرقام أصغر وزدها تدريجيًّا إذا كان الخادم يستجيب بشكل جيد.)

التعيين الجماعي للصور المميزة المفقودة في ووردبريس. خيارات العرض

  • انقر على عنوان عمود «المقتطفات المفقودة» لفرز وتجميع الإدخالات المعلقة. وفي عوامل التصفية، يمكنك الاختيار بين «يحتوي على» أو «مفقود» أو «الكل»
  • بمجرد أن تتأكد من استعدادك لبدء تعيين الصور المميزة المفقودة على دفعات، انقر على الزر الأزرق « » المسمى «تعيين الكل في هذه الصفحة»، واترك العملية التتابعية تكمل تعيين الصور المصغرة لتلك الدفعة. وإذا كنت تفضل ذلك، يمكنك تعيينها واحدة تلو الأخرى.
  • انتقل إلى الصفحة التالية وكرر العملية حتى تنتهي من تنظيم مكتبتك.

اختياري: مقتطف التخصيص التلقائي

لتجنب تكرار هذه المهمة في المستقبل، يمكنك إضافة هذا المقتطف القصير. يتم تشغيله في الخلفية فقط عند حفظ أو نشر منشور: إذا اكتشف أنك لم تختر صورة مميزة يدويًّا، فإنه يقوم تلقائيًّا بتعيين الصورة الأولى في المحتوى (أو الصورة الاحتياطية الافتراضية، والتي ستحتاج إلى إضافتها في $default_image_url =).

PHP

// Asignar imagen destacada automáticamente al publicar/guardar nuevas entradas
add_action( 'save_post_post', 'auto_set_featured_image_on_save', 10, 3 );
function auto_set_featured_image_on_save( $post_id, $post, $update ) {
    // Evitar autoguardados, revisiones o peticiones AJAX/REST innecesarias
    if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) return;
    if ( wp_is_post_revision( $post_id ) || $post->post_status === 'auto-draft' ) return;

    // Si la entrada ya tiene imagen destacada marcada, no hacemos nada
    if ( has_post_thumbnail( $post_id ) ) return;

    $default_image_url = 'https://tu-dominio.com/wp-content/uploads/imagen-por-defecto.jpg';

    // 1. Buscar primera imagen en el contenido
    preg_match_all( '/<img.+?src=[\'"]([^\'"]+)[\'"]/i', $post->post_content, $matches );

    if ( isset( $matches[1][0] ) ) {
        $attachment_id = attachment_url_to_postid( $matches[1][0] );
        if ( $attachment_id ) {
            set_post_thumbnail( $post_id, $attachment_id );
            return;
        }
    }

    // 2. Si el post no tiene imágenes, asignar la imagen por defecto
    if ( ! empty( $default_image_url ) ) {
        $default_id = attachment_url_to_postid( $default_image_url );
        if ( $default_id ) {
            set_post_thumbnail( $post_id, $default_id );
        }
    }
}

أضف تعليق

هل لديك أي شيء تقوله؟

Este blog se aloja en LucusHost

LucusHost, el mejor hosting