Automatiza Imágenes Destacadas en WordPress con IA: Plugin Nano Banana 3 Pro

En el mundo del desarrollo web y el SEO, el tiempo es un recurso crítico. Uno de los problemas más comunes al gestionar sitios con gran volumen de contenido o al realizar migraciones es encontrarse con cientos de artículos sin imagen destacada. Esto no solo afecta la estética del sitio, sino que impacta negativamente en el CTR y el posicionamiento SEO.

Para solucionar esto, he desarrollado Nano Banana 3 Pro, un plugin personalizado que integra la potencia del modelo más reciente de Google, Gemini 3 Pro, directamente en el dashboard de WordPress.

<?php
/**
 * Plugin Name: Nano Banana 3 Pro – Featured Image Generator
 * Description: Generador automático de imágenes destacadas utilizando la API de Google Gemini 3 Pro. Optimizado para SEO.
 * Version: 1.0.0
 * Author: Alexis Mendoza
 * Author URI: https://alexism.dev
 */

if (!defined('ABSPATH')) exit;

define('NB3_GEMINI_API_KEY', 'TU_API_KEY_AQUI');
define('NB3_GEMINI_MODEL', 'gemini-3-pro-image-preview');
define('NB3_GEMINI_ENDPOINT', 'https://generativelanguage.googleapis.com/v1beta/models/' . NB3_GEMINI_MODEL . ':generateContent');
define('NB3_LOG_OPTION', 'nb3_generation_logs');
define('NB3_BATCH_SIZE', 20);

add_action('admin_menu', function () {
    add_management_page('Nano Banana SEO', 'Nano Banana SEO', 'manage_options', 'nano-banana-seo', 'nb3_admin_page');
});

function nb3_admin_page() {
    set_time_limit(300); 

    if (isset($_POST['nb3_run_scan']) && check_admin_referer('nb3_run_scan_nonce')) {
        nb3_process_batch();
        echo '<div class="notice notice-success"><p>Lote procesado correctamente.</p></div>';
    }

    $stats = nb3_get_stats();
    $logs = get_option(NB3_LOG_OPTION, []);
    $pending_posts = nb3_get_pending_posts(5);
    ?>

    <div class="wrap">
        <h1>Nano Banana 3 Pro - Panel de Control</h1>
        
        <div style="display:flex; gap:20px; margin: 20px 0;">
            <div style="background:#fff; padding:20px; border-left:4px solid #2271b1; box-shadow:0 1px 1px rgba(0,0,0,0.1);">
                <h3 style="margin:0">Total Posts</h3>
                <p style="font-size:24px; font-weight:bold; margin:5px 0"><?php echo $stats['total']; ?></p>
            </div>
            <div style="background:#fff; padding:20px; border-left:4px solid #46b450; box-shadow:0 1px 1px rgba(0,0,0,0.1);">
                <h3 style="margin:0">Con Imagen</h3>
                <p style="font-size:24px; font-weight:bold; margin:5px 0"><?php echo $stats['with_img']; ?></p>
            </div>
            <div style="background:#fff; padding:20px; border-left:4px solid #d63638; box-shadow:0 1px 1px rgba(0,0,0,0.1);">
                <h3 style="margin:0">Pendientes</h3>
                <p style="font-size:24px; font-weight:bold; margin:5px 0"><?php echo $stats['missing']; ?></p>
            </div>
        </div>

        <form method="post">
            <?php wp_nonce_field('nb3_run_scan_nonce'); ?>
            <p>
                <?php if ($stats['missing'] > 0): ?>
                    <button class="button button-primary button-hero" name="nb3_run_scan">
                        Generar Siguientes <?php echo NB3_BATCH_SIZE; ?> Imágenes
                    </button>
                <?php else: ?>
                    <button class="button button-secondary" disabled>Proceso Completado</button>
                <?php endif; ?>
            </p>
        </form>

        <hr>

        <div style="display:flex; gap:30px;">
            <div style="flex:1;">
                <h2>Próximos en cola</h2>
                <?php if (empty($pending_posts)): ?>
                    <p>No hay posts pendientes.</p>
                <?php else: ?>
                    <table class="widefat striped">
                        <thead><tr><th>ID</th><th>Título</th></tr></thead>
                        <tbody>
                            <?php foreach ($pending_posts as $p): ?>
                                <tr>
                                    <td><?php echo $p->ID; ?></td>
                                    <td><?php echo esc_html($p->post_title); ?></td>
                                </tr>
                            <?php endforeach; ?>
                        </tbody>
                    </table>
                <?php endif; ?>
            </div>

            <div style="flex:2;">
                <h2>Historial</h2>
                <table class="widefat striped">
                    <thead><tr><th>Hora</th><th>Post</th><th>Estado</th><th>Mensaje</th></tr></thead>
                    <tbody>
                        <?php if (empty($logs)) : ?>
                            <tr><td colspan="4">Sin registros.</td></tr>
                        <?php else : 
                            $recent_logs = array_slice($logs, 0, 10);
                            foreach ($recent_logs as $log) : ?>
                            <tr>
                                <td><?php echo date('H:i:s', strtotime($log['date'])); ?></td>
                                <td><?php echo esc_html($log['post']); ?></td>
                                <td>
                                    <?php echo ($log['status'] == 'OK') ? '<span style="color:green;font-weight:bold">OK</span>' : '<span style="color:red;font-weight:bold">'.esc_html($log['status']).'</span>'; ?>
                                </td>
                                <td style="font-size:11px;"><?php echo esc_html(substr($log['message'], 0, 100)); ?>...</td>
                            </tr>
                        <?php endforeach; endif; ?>
                    </tbody>
                </table>
            </div>
        </div>
    </div>
<?php }

function nb3_get_pending_posts($limit = -1) {
    return get_posts([
        'post_type'      => 'post',
        'post_status'    => 'publish',
        'posts_per_page' => $limit,
        'meta_query'     => [['key' => '_thumbnail_id', 'compare' => 'NOT EXISTS']]
    ]);
}

function nb3_get_stats() {
    $published = wp_count_posts()->publish;
    $missing = new WP_Query([
        'post_type' => 'post',
        'post_status' => 'publish',
        'meta_query' => [['key' => '_thumbnail_id', 'compare' => 'NOT EXISTS']],
        'fields' => 'ids',
        'posts_per_page' => -1
    ]);
    return ['total' => $published, 'missing' => $missing->found_posts, 'with_img' => $published - $missing->found_posts];
}

function nb3_process_batch() {
    $posts = nb3_get_pending_posts(NB3_BATCH_SIZE);
    if (empty($posts)) return;

    foreach ($posts as $post) {
        $image_binary = nb3_generate_image($post);
        if (!$image_binary) continue; 
        
        if (nb3_attach_image_seo($post, $image_binary)) {
            nb3_log($post->post_title, 'OK', 'Generada correctamente.');
        } else {
            nb3_log($post->post_title, 'DISK_ERR', 'Error al guardar archivo.');
        }
    }
}

function nb3_generate_image($post) {
    $title   = sanitize_text_field($post->post_title);
    $content = wp_trim_words(wp_strip_all_tags($post->post_content), 50);
    
    $prompt = "Create a high-quality featured image for a blog post titled '{$title}'. Context: {$content}. Style: Professional photography, photorealistic, cinematic lighting, 8k resolution, clean composition. IMPORTANT: No text in image.";
    
    $response = wp_remote_post(NB3_GEMINI_ENDPOINT . '?key=' . NB3_GEMINI_API_KEY, [
        'headers' => ['Content-Type' => 'application/json'],
        'body'    => json_encode(['contents' => [['parts' => [['text' => $prompt]]]]]),
        'timeout' => 60
    ]);

    if (is_wp_error($response)) {
        nb3_log($title, 'NET_ERR', $response->get_error_message());
        return false;
    }

    $data = json_decode(wp_remote_retrieve_body($response), true);

    if (isset($data['error'])) {
        nb3_log($title, 'API_ERR', $data['error']['message']);
        return false;
    }

    if (!empty($data['candidates'][0]['content']['parts'])) {
        foreach ($data['candidates'][0]['content']['parts'] as $part) {
            if (isset($part['inlineData']['data'])) return base64_decode($part['inlineData']['data']);
            if (isset($part['inline_data']['data'])) return base64_decode($part['inline_data']['data']);
        }
    }
    
    nb3_log($title, 'NO_IMG', 'Respuesta vacía de API.');
    return false;
}

function nb3_attach_image_seo($post, $image_binary) {
    $upload = wp_upload_dir();
    $filename = sanitize_title($post->post_title) . '-' . substr(uniqid(), -3) . '.jpg';
    $filepath = $upload['path'] . '/' . $filename;

    if (!file_exists($upload['path'])) mkdir($upload['path'], 0755, true);
    if (file_put_contents($filepath, $image_binary) === false) return false;

    $filetype = wp_check_filetype($filename, null);
    $attach_id = wp_insert_attachment([
        'post_mime_type' => $filetype['type'],
        'post_title'     => $post->post_title,
        'post_content'   => '',
        'post_status'    => 'inherit'
    ], $filepath, $post->ID);

    require_once(ABSPATH . 'wp-admin/includes/image.php');
    wp_update_attachment_metadata($attach_id, wp_generate_attachment_metadata($attach_id, $filepath));
    set_post_thumbnail($post->ID, $attach_id);
    update_post_meta($attach_id, '_wp_attachment_image_alt', $post->post_title);

    return true;
}

function nb3_log($post, $status, $message) {
    $logs = get_option(NB3_LOG_OPTION, []);
    array_unshift($logs, ['date' => current_time('Y-m-d H:i:s'), 'post' => $post, 'status' => $status, 'message' => $message]);
    if (count($logs) > 50) $logs = array_slice($logs, 0, 50);
    update_option(NB3_LOG_OPTION, $logs);
}

¿Qué es Nano Banana 3 Pro? Es una solución de automatización diseñada para desarrolladores y administradores de sistemas. A diferencia de otros plugins que dependen de bancos de imágenes genéricos, esta herramienta lee el contexto de tu artículo y genera una imagen única y relevante utilizando inteligencia artificial generativa.

Características Técnicas Principales

  1. Integración con Gemini 3 Pro: Utiliza el modelo gemini-3-pro-image-preview para obtener resultados fotorrealistas y de alta resolución (8k).
  2. Optimización SEO Automática:
    • Nombres de archivo: Convierte el título del post en un slug limpio (ej: titulo-del-post.jpg), fundamental para el SEO de imágenes.
    • Atributo ALT: Asigna automáticamente el título del artículo como texto alternativo.
  3. Procesamiento por Lotes (Batch Processing): Para evitar tiempos de espera o sobrecarga en el servidor (timeouts), el plugin procesa las imágenes en lotes configurables (por defecto 20 posts por ejecución).
  4. Sistema de Logs: Incluye un panel de control interno que registra el estado de cada generación (OK, Error de API, Error de Disco), facilitando la depuración.

¿Cómo Funciona? El flujo de trabajo del código es directo y eficiente:

  • El script escanea la base de datos en busca de posts publicados que carecen de _thumbnail_id.
  • Envía el título y un extracto del contenido a la API de Google con un prompt optimizado para fotografía profesional y limpia.
  • Recibe la imagen en base64, la decodifica y la guarda en el directorio wp-content/uploads.
  • Registra la imagen en la librería multimedia y la asigna como destacada.