
Hace unos días terminé una tarea pendiente que era necesaria, pero me daba un perezón tremendo meterle mano. Se recuperaron todas las imágenes destacadas faltantes y añadí todos los ALT faltantes a las imágenes. También terminé de escribir todas las meta descripciones faltantes. En total, más de 15K registros añadidos o editados usando tres snippets propios de usar y tirar. Hoy comparto por aquí el snippet de recuperación de las imágenes destacadas faltantes, que también usé para hacer esta tarea en otras páginas.
Si tienes un blog con cierto recorrido, es bastante probable que tengas un buen número de entradas antiguas sin imagen destacada asignada. Esto solía pasar mucho en la época del editor clásico, cuando insertábamos la imagen directamente en el cuerpo del post pero nos olvidábamos de marcarla en la caja de "Imagen destacada".
Dejar artículos sin imagen destacada no es solo un problema estético, entre otras cosas; afecta directamente a la optimización de tu sitio y a la experiencia de usuario:
- SEO social y Open Graph: Al compartir la entrada en redes sociales, apps de mensajería o agregadores de contenido, las etiquetas Open Graph (
og:image) no encuentran un recurso claro que mostrar, reduciendo drásticamente el CTR (porcentaje de clics). - Coherencia visual en el theme: Las plantillas modernas (como las maquetadas con GeneratePress o GenerateBlocks) dependen de la imagen destacada para construir los grids de archivo, los listados de entradas relacionadas o las cabeceras. Sin ella, el diseño se rompe o deja huecos desalineados.
- Descubrimiento y feeds (RSS / Google Discover): Herramientas como Google Discover o lectores RSS priorizan o exigen contenido con una miniatura asociada en alta resolución para enriquecer la tarjeta del artículo.
Para resolver esto sin recurrir a plugins (como este) ni procesos lentos, hice este snippet simple en PHP ligero que permite asignar automáticamente la primera imagen de cada post como destacada, directamente desde el escritorio de WordPress. Si tu post no tiene ninguna imagen, podrás asignarle una por defecto tras añadir su URL en el código.
¿Qué hace exactamente este snippet?
El código añade una herramienta de procesamiento inteligente y por lotes integrada en la lista de entradas de WordPress (wp-admin/edit.php):
- Añade una columna personalizada: Crea la columna "Destacada Faltante" en el listado de entradas para identificar de un vistazo qué posts no tienen miniatura.
- Procesamiento en 3 capas: Al activar la asignación para un post, busca la imagen en este orden estricto:
- Busca si hay imágenes adjuntas directamente a la entrada en la Biblioteca de Medios.
- Si no hay adjuntos directos (por ejemplo, si se insertaron por bloque de imagen o HTML), escanea el contenido en busca de la primera etiqueta
<img>para extraer su ID medianteattachment_url_to_postid(). - Si la entrada no contiene absolutamente ninguna imagen, asigna una imagen por defecto (fallback) especificada en la configuración.
- Procesamiento masivo por AJAX (en lote): Incluye un botón en la cabecera ("⚡ Asignar todas en esta página") que procesa en cascada todas las entradas visibles en la pantalla mediante peticiones asíncronas consecutivas. También puedes hacerlo manualmente con el botón individual de cada post de la lista.
- Filtra borradores: Excluye automáticamente las entradas en estado de borrador para centrarse únicamente en el contenido publicado.
- Columna ordenable: Permite ordenar el listado para agrupar en primer lugar todas las entradas sin imagen destacada.
¿Por qué es ultraligero y no afecta al rendimiento del servidor?
Una preocupación habitual al usar este tipo de herramientas automáticas es el miedo a bloquear la base de datos o saturar la memoria PHP en sitios con miles de artículos. Este snippet se ha diseñado bajo los siguientes criterios de optimización:
1. Solo se ejecuta en el Admin (is_admin())
El código está empaquetado exclusivamente para la pantalla de entradas del panel de administración (edit.php). No añade ni una sola línea de sobrecarga a las peticiones del sitio público. Tus usuarios y las mediciones de velocidad no se verán afectados en absoluto.
2. Consultas directas a la Meta Cache
Para determinar si un post tiene o no imagen, el snippet no realiza búsquedas complejas en la base de datos por cada fila. Utiliza la clave metadato _thumbnail_id, la cual WordPress carga en memoria de forma nativa mediante la caché de metadatos al renderizar el listado. La carga de la tabla es prácticamente instantánea.
3. Procesamiento asíncrono en cascada (Bucle AJAX secuencial)
En lugar de lanzar una consulta masiva en la base de datos para actualizar 700 o 1000 entradas de golpe (lo que provocaría un Time Out o consumiría toda la memoria del servidor), el botón de asignar todas procesa las entradas una por una mediante promesas en JavaScript:
Post 1 (AJAX)⟶Respuesta OK⟶Post 2 (AJAX)⟶Respuesta OK…
Esto garantiza un consumo de recursos estable y plano, permitiéndote ver el progreso en tiempo real fila por fila.
4. Cero duplicación de archivos
El snippet no descarga, recombina ni duplica imágenes en el disco. Simplemente toma el ID de la imagen ya existente en tu Biblioteca de Medios y lo vincula a la entrada asignando el meta_value correspondiente en la tabla wp_postmeta.
El Código (PHP + JavaScript)
Puedes añadir este código en el archivo functions.php de tu tema hijo, un plugin propio o a través de un plugin de funciones personalizadas como Code Snippets o similar.
Nota: Recuerda configurar la variable
$default_image_urlen la línea 56 con la URL completa de la imagen genérica que quieras asignar a las entradas que no tengan ninguna imagen en el texto.
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
}
¿Cómo se usa?
- Dirígete a la lista de Entradas en tu panel de administración.
- Abre la pestaña superior Opciones de pantalla y cambia el número de elementos a mostrar a 50 o 100 por página. (Empieza probando con cantidades pequeñas y sube poco a poco si tu servidor responde bien).

- Haz clic en la cabecera de la columna Destacada Faltante para ordenar y agrupar las entradas pendientes. En los filtros puedes seleccionar entre "Tiene", "falta" o Todas"
- Cuando tengas claro que puedes empezar a asignar imágenes destacadas faltantes por lotes, pulsas el botón azul de "Asignar todas en esta página" y deja que el proceso en cascada termine de asignar las miniaturas de ese lote. Si lo prefieres, puedes asignarlas una a una.
- Pasa a la siguiente página y repite hasta completar la limpieza de tu biblioteca.
Opcional, snippet de asignación automática
Para evitar tener que repetir esta tarea en el futuro, puedes añadir este pequeño snippet. Se ejecuta en segundo plano únicamente cuando guardas o publicas una entrada: si detecta que no has seleccionado manualmente ninguna imagen destacada, asigna automáticamente la primera del contenido (o la de reserva por defecto, que deberás añadir en $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 );
}
}
}
Cuatro formas de añadir un snippet ¿Cuál es la mejor?
Y como siempre que trasteo con un snippet doy por hecho que sabes cómo añadirlo y qué vía es la mejor para tu entorno, dejo este vídeo de Fernando Tellado por si no es tu caso.














