Come creare un aggregatore di feed RSS su WordPress (senza rovinare il tuo WPO)

No comments

23.09.2026|

No comments

Tiempo de lectura Lectura: 27 min, 44 s
Número de palabras Palabras: 5131
Número de visitas Visitas: 50
Icono de traducción
Come creare un aggregatore di feed RSS su WordPress (senza rovinare il tuo WPO)
Lo snippet consente di aggiungere due gruppi di feed: uno a disposizione orizzontale con anteprima e un altro a griglia senza anteprima, pensato per le immagini.

Come forse avrai già notato, la pagina“Notizie” è stata ripristinata e quella “Vignette” è stata rinnovata, poiché entrambe dipendevano da un servizio esterno (Elfsight). A tal fine è stato creato un piccolo motore che funge da aggregatore/lettore e il caricamento è quasi istantaneo. In questo post troverai il codice generico, nel caso volessi provarlo. Puoi anche utilizzarlo su una pagina privata come semplice lettore.

Per quanto riguarda le vignette, è stata aggiunta l’impaginazione con un limite di 10 pagine e 15 immagini per pagina, sufficienti per consultare uno storico di circa 4 giorni. Per le notizie vengono visualizzatii 10 post più recenti in totale, ordinati cronologicamente e con impaginazione che consente di leggere quelli precedenti fino a unlimite di 10 pagine, anch’essi corrispondenti a circa quattro giorni (a seconda del flusso di pubblicazione dell’insieme delle fonti). L’aggiornamento è periodico. L'aggiornamento avviene ogni 4 ore, ma è possibile aggiornare manualmente in qualsiasi momento.

 Puoi suggerire nuove fonti (purché dispongano di un feed RSS). Per verificare se l'indirizzo che proponi è valido, puoiandare su Simple Piee provarlo prima.

Ascesa e declino dei feed: XML e feed

Prima di addentrarci nel codice, è necessario capire cosa stiamo utilizzando. Un file XML (eXtensible Markup Language) è un linguaggio di marcatura progettato per memorizzare e trasmettere dati in modo strutturato. Su questa base è nato il feed RSS (Really Simple Syndication), un file standardizzato che i siti web generano automaticamente ogni volta che pubblicano contenuti.

  • L'epoca d'oro: negli anni 2000, con l'esplosione dei blog, l'RSS era il re. Strumenti ormai scomparsi come Google Reader (2005 - 2013) permettevano agli utenti di iscriversi a centinaia di siti e di leggerli in un unico posto senza dover visitare ogni singola pagina web; tuttavia, esistono ancora alternative per la lettura dei feed.
  • Il disuso attuale: al giorno d’oggi, i feed sono in declino per diversi motivi. Con i blog che chiudono a frotte ogni giorno e l’avvento dei social media, degli algoritmi di fidelizzazione e delle newsletter a accesso riservato, oltre alle intelligenze artificiali dei motori di ricerca che estraggono i riassunti (ciò che è ormai noto come Zero Click Search, ricerca a zero clic), molti media hanno eliminato i propri feed RSS per costringere l’utente a navigare sui propri siti. Ciò ha reso sempre più complicato trovare fonti tematiche disponibili tramite RSS.

Come funziona questo motore di feed in WordPress

Questo snippet è stato progettato con un’architettura che privilegia le prestazioni e il WPO.

  • Elaborazione asincrona (AJAX invisibile): non blocca il caricamento della pagina. Il server scarica i feed in background, in piccoli lotti di 5 alla volta, in base al traffico organico dei visitatori.
  • Impatto nullo sul database: i dati elaborati vengono salvati nella tabella delle opzioni con lo stato autoload = false. Ciò significa che non occupano la memoria globale del sito e vengono caricati solo quando la pagina di lettura ne fa richiesta.
  • Link puliti: inserisci gli articoli esterni con gli attributi target="_blank" e rel="noopener nofollow", proteggendo il SEO del tuo sito ed evitando la dispersione di autorità.

Consigli sull'uso dei contenuti di terze parti

Sebbene il formato sia pensato per pubblicare contenuti su lettori e pagine, quando si aggiungono contenuti di terze parti è fondamentale applicare buone pratiche etiche e tecniche per non danneggiare né il proprio server né gli autori originali:

  • Hotlinking e blocco delle immagini: lo snippet carica le immagini direttamente dal server originale (hotlinking). Ciò consente di risparmiare spazio sul disco, ma gli autori o i proprietari possono imporre limitazioni e restrizioni; se nel file .htaccess sono presenti protezioni anti-hotlinking, le immagini appariranno danneggiate.
  • Rispetta la larghezza di banda: non abusare della frequenza di sincronizzazione. Effettuare richieste costanti a server di terze parti può indurre il loro firewall (come Cloudflare) a bloccare l'IP del tuo server, ritenendolo un bot dannoso.
  • Proprietà intellettuale: estrae solo il titolo e un breve riassunto (il codice lo limita a 25 parole). L'obiettivo di un aggregatore etico è reindirizzare il traffico verso l'autore originale, non rubargli l'intero contenuto.
  • I contenuti visualizzati possono cambiare. È un aspetto importante a cui prestare attenzione. La fonte può modificare i contenuti visualizzati o addirittura eliminare il feed. Ciò potrebbe comportare la scomparsa dei contenuti dal tuo sito o la visualizzazione di contenuti diversi da quelli che ti aspettavi. Controlla le tue fonti di tanto in tanto.

Codice

Servizio di ottimizzazione di WordPress

Il suo utilizzo è molto semplice. Aggiungi il codice nel file functions.php del tuo tema o nel tuo plugin di snippet e apparirà l'area di amministrazione in Impostazioni/Gestione feed con gli shortcode e le impostazioni per aggiungere, sospendere o eliminare i link inseriti ed esportare e importare un file JSON con le tue liste.

Sotto il codice troverai alcuni dettagli tecnici sulle prestazioni.

// ========================================================================
// 1. Lector de Feeds - MOTOR DE PROCESAMIENTO POR LOTES (AJAX Y MANUAL)
// ========================================================================

function lector_procesar_lote_feed_generico($tipo) {
    $opt_urls = ($tipo === 'grupo_1') ? 'lector_feeds_g1_urls' : 'lector_feeds_g2_urls';
    $urls_data = get_option($opt_urls, []);
    $active_urls = [];
    foreach ($urls_data as $item) {
        if (is_array($item) && $item['estado'] === 'activo') $active_urls[] = $item['url'];
    }

    if (empty($active_urls)) return 0;

    $idx_opt = 'lector_ajax_idx_' . $tipo;
    $current_idx = (int) get_option($idx_opt, 0);

    if ($current_idx >= count($active_urls)) {
        $current_idx = 0;
    }

    // Aumentamos a 10 para acelerar el barrido
    $batch = array_slice($active_urls, $current_idx, 10);
    update_option($idx_opt, $current_idx + 10, false);

    if (empty($batch)) return 0;

    include_once(ABSPATH . WPINC . '/feed.php');
    
    $custom_timeout = function($feed) { $feed->set_timeout(3); };
    add_action('wp_feed_options', $custom_timeout);
    
    // Caché a 4 horas (14400 segundos)
    $custom_cache = function(){ return 14400; }; 
    add_filter('wp_feed_cache_transient_lifetime', $custom_cache);
    
    $feed = fetch_feed($batch);
    
    remove_action('wp_feed_options', $custom_timeout);
    remove_filter('wp_feed_cache_transient_lifetime', $custom_cache);
    
    $procesados = 0;
    if (!is_wp_error($feed)) {
        $items = $feed->get_items(0, 30); 
        
        $compilados_opt = 'lector_items_compilados_' . $tipo;
        $existentes = get_option($compilados_opt, []); 
        $imagen_por_defecto = 'https://s.w.org/images/core/emoji/14.0.0/svg/1f4f0.svg';
        
        $nuevos = [];
        foreach ($items as $item) {
            $enlace = $item->get_permalink();
            if (!$enlace) continue;

            $timestamp = $item->get_date('U');
            if (!$timestamp) $timestamp = time();

            $imagen_url = '';
            if ($enclosure = $item->get_enclosure()) { $imagen_url = $enclosure->get_link(); } 
            if (empty($imagen_url)) {
                preg_match('/<img[^>]+src=[\'"]([^\'"]+)[\'"][^>]*>/i', $item->get_content(), $matches);
                if (!empty($matches[1])) { $imagen_url = $matches[1]; }
            }
            if (empty($imagen_url)) { $imagen_url = $imagen_por_defecto; }
            
            $fuente_titulo = '';
            if ($feed_parent = $item->get_feed()) { $fuente_titulo = $feed_parent->get_title(); }
            
            $nuevo_item = [
                'enlace' => $enlace,
                'titulo' => $item->get_title(),
                'timestamp' => $timestamp,
                'imagen' => $imagen_url,
                'fuente' => $fuente_titulo
            ];
            
            if ($tipo === 'grupo_1') {
                $nuevo_item['extracto'] = wp_trim_words(strip_tags($item->get_description()), 25, '...');
            }
            
            $nuevos[$enlace] = $nuevo_item;
            $procesados++;
        }
        
        foreach ($nuevos as $link => $data) {
            $existentes[$link] = $data;
        }
        
        uasort($existentes, function($a, $b) {
            return $b['timestamp'] <=> $a['timestamp'];
        });
        
        $limite = ($tipo === 'grupo_1') ? 100 : 150;
        $existentes = array_slice($existentes, 0, $limite, true);
        
        update_option($compilados_opt, $existentes, false);
    }
    
    // Guardamos la última hora de ejecución
    update_option('lector_ultimo_ajax_gen_' . $tipo, wp_date('d/m/Y H:i:s'), false);
    
    return $procesados;
}

add_action('wp_ajax_lector_procesar_fondo_gen', 'lector_ajax_handler_gen');
add_action('wp_ajax_nopriv_lector_procesar_fondo_gen', 'lector_ajax_handler_gen');
function lector_ajax_handler_gen() {
    // Usamos $_REQUEST para aceptar POST
    $tipo = isset($_REQUEST['tipo']) ? sanitize_text_field($_REQUEST['tipo']) : '';
    if (in_array($tipo, ['grupo_1', 'grupo_2'])) {
        lector_procesar_lote_feed_generico($tipo);
    }
    wp_send_json_success();
}


// ========================================================================
// 2. PROCESAR IMPORTACIÓN/EXPORTACIÓN DE ARCHIVOS
// ========================================================================
add_action('admin_init', 'lector_feeds_gen_procesar_archivos');
function lector_feeds_gen_procesar_archivos() {
    if (!current_user_can('manage_options')) return;

    if (isset($_POST['exportar_g1']) && check_admin_referer('exportar_g1_nonce')) {
        $data = get_option('lector_feeds_g1_urls', []);
        header('Content-disposition: attachment; filename=feeds-grupo-1.json');
        header('Content-type: application/json'); echo wp_json_encode($data); exit;
    }
    if (isset($_POST['exportar_g2']) && check_admin_referer('exportar_g2_nonce')) {
        $data = get_option('lector_feeds_g2_urls', []);
        header('Content-disposition: attachment; filename=feeds-grupo-2.json');
        header('Content-type: application/json'); echo wp_json_encode($data); exit;
    }

    function procesar_importacion_json_gen($file_input, $option_name, $nonce_name, $success_msg) {
        if (isset($_POST[$file_input]) && check_admin_referer($nonce_name) && !empty($_FILES['archivo_'.$file_input]['tmp_name'])) {
            $json = json_decode(file_get_contents($_FILES['archivo_'.$file_input]['tmp_name']), true);
            if (is_array($json)) {
                $limpios = [];
                foreach ($json as $item) {
                    if (!empty($item['url'])) {
                        $limpios[] = ['url' => sanitize_url($item['url']), 'estado' => (isset($item['estado']) && $item['estado'] === 'pausado') ? 'pausado' : 'activo'];
                    }
                }
                update_option($option_name, $limpios, false);
                add_settings_error('lector_mensajes_gen', 'import_exito', $success_msg, 'success');
            } else { add_settings_error('lector_mensajes_gen', 'import_error', 'JSON no válido.', 'error'); }
        }
    }
    procesar_importacion_json_gen('importar_g1', 'lector_feeds_g1_urls', 'importar_g1_nonce', 'JSON del Grupo 1 importado.');
    procesar_importacion_json_gen('importar_g2', 'lector_feeds_g2_urls', 'importar_g2_nonce', 'JSON del Grupo 2 importado.');

    function procesar_importacion_opml_gen($file_input, $option_name, $nonce_name) {
        if (isset($_POST[$file_input]) && check_admin_referer($nonce_name) && !empty($_FILES['archivo_'.$file_input]['tmp_name'])) {
            libxml_use_internal_errors(true);
            $xml = simplexml_load_file($_FILES['archivo_'.$file_input]['tmp_name']);
            if ($xml) {
                $existentes = get_option($option_name, []);
                $urls_actuales = array_column($existentes, 'url');
                $outlines = $xml->xpath('//outline[@xmlUrl]');
                $anadidos = 0;
                if (!empty($outlines)) {
                    foreach ($outlines as $node) {
                        $url = sanitize_url((string)$node['xmlUrl']);
                        if ($url && !in_array($url, $urls_actuales)) {
                            $existentes[] = ['url' => $url, 'estado' => 'activo'];
                            $urls_actuales[] = $url;
                            $anadidos++;
                        }
                    }
                    update_option($option_name, $existentes, false);
                    add_settings_error('lector_mensajes_gen', 'import_opml_exito', "OPML importado. $anadidos fuentes nuevas.", 'success');
                }
            } else { add_settings_error('lector_mensajes_gen', 'import_opml_error', 'OPML no válido.', 'error'); }
        }
    }
    procesar_importacion_opml_gen('importar_opml_g1', 'lector_feeds_g1_urls', 'importar_opml_g1_nonce');
    procesar_importacion_opml_gen('importar_opml_g2', 'lector_feeds_g2_urls', 'importar_opml_g2_nonce');
}


// ========================================================================
// 3. INTERFAZ DE ADMINISTRACIÓN GENÉRICA
// ========================================================================
add_action('admin_menu', 'lector_feeds_gen_menu');
function lector_feeds_gen_menu() {
    add_options_page('Gestor de Feeds', 'Gestor de Feeds', 'manage_options', 'lector-feeds-gen', 'lector_feeds_gen_admin_page');
}

function lector_feeds_gen_admin_page() {
    if (!current_user_can('manage_options')) return;

    if (isset($_POST['sinc_g1']) && check_admin_referer('sinc_g1_nonce')) {
        $extraidos = lector_procesar_lote_feed_generico('grupo_1');
        echo '<div class="updated"><p>Sincronización manual: Lote Grupo 1 procesado ('.$extraidos.' items comprobados).</p></div>';
    }
    if (isset($_POST['sinc_g2']) && check_admin_referer('sinc_g2_nonce')) {
        $extraidos = lector_procesar_lote_feed_generico('grupo_2');
        echo '<div class="updated"><p>Sincronización manual: Lote Grupo 2 procesado ('.$extraidos.' items comprobados).</p></div>';
    }

    if (isset($_POST['vaciar_g1']) && check_admin_referer('vaciar_g1_nonce')) {
        delete_option('lector_items_compilados_grupo_1');
        delete_option('lector_ajax_idx_grupo_1');
        echo '<div class="updated"><p>Historial Grupo 1 vaciado.</p></div>';
    }
    if (isset($_POST['vaciar_g2']) && check_admin_referer('vaciar_g2_nonce')) {
        delete_option('lector_items_compilados_grupo_2');
        delete_option('lector_ajax_idx_grupo_2');
        echo '<div class="updated"><p>Historial Grupo 2 vaciado.</p></div>';
    }

    function procesar_guardado_gen($nuevo_feed, $url_feed, $nonce, $option_name) {
        $urls = get_option($option_name, []);
        if (isset($_POST[$nuevo_feed]) && !empty($_POST[$url_feed]) && check_admin_referer($nonce)) {
            $nueva_url = sanitize_url($_POST[$url_feed]);
            $existe = false;
            foreach ($urls as $item) { if ($item['url'] === $nueva_url) { $existe = true; break; } }
            if (!$existe) {
                $urls[] = ['url' => $nueva_url, 'estado' => 'activo'];
                update_option($option_name, $urls, false);
                echo '<div class="updated"><p>Feed añadido.</p></div>';
            } else { echo '<div class="error"><p>Ese feed ya está registrado.</p></div>'; }
        }
    }
    procesar_guardado_gen('nuevo_feed_g1', 'url_feed_g1', 'guardar_g1_nonce', 'lector_feeds_g1_urls');
    procesar_guardado_gen('nuevo_feed_g2', 'url_feed_g2', 'guardar_g2_nonce', 'lector_feeds_g2_urls');

    function procesar_actualizacion_gen($btn_actualizar, $nonce, $array_eliminar, $array_estados, $option_name) {
        if (isset($_POST[$btn_actualizar]) && check_admin_referer($nonce)) {
            $urls = get_option($option_name, []);
            if (!empty($_POST[$array_eliminar])) foreach ($_POST[$array_eliminar] as $i) unset($urls[$i]);
            if (!empty($_POST[$array_estados])) {
                foreach ($_POST[$array_estados] as $i => $est) {
                    if (isset($urls[$i])) $urls[$i]['estado'] = ($est === 'pausado') ? 'pausado' : 'activo';
                }
            }
            update_option($option_name, array_values($urls), false);
            echo '<div class="updated"><p>Cambios guardados.</p></div>';
        }
    }
    procesar_actualizacion_gen('actualizar_g1', 'gestionar_g1_nonce', 'g1_eliminar', 'estado_g1', 'lector_feeds_g1_urls');
    procesar_actualizacion_gen('actualizar_g2', 'gestionar_g2_nonce', 'g2_eliminar', 'estado_g2', 'lector_feeds_g2_urls');
    
    $urls_g1 = get_option('lector_feeds_g1_urls', []);
    $urls_g2 = get_option('lector_feeds_g2_urls', []);
    $cache_g1 = count(get_option('lector_items_compilados_grupo_1', []));
    $cache_g2 = count(get_option('lector_items_compilados_grupo_2', []));
    ?>
    <div class="wrap">
        <h1>Gestión de Feeds</h1>
        <?php settings_errors('lector_mensajes_gen'); ?>
        <div style="display: flex; flex-wrap: wrap; gap: 20px; margin-top: 20px;">
            
            <!-- BLOQUE GRUPO 1 -->
            <div style="flex: 1; min-width: 450px; background: #fff; border: 1px solid #ccd0d4; padding: 20px;">
                <h2 style="margin-top: 0; border-bottom: 1px solid #eee; padding-bottom: 10px; display: flex; justify-content: space-between;">
                    <span>📁 Feeds - Grupo 1 (Lista con extracto)</span>
                    <span style="font-size: 13px; font-weight: normal; background: #e0e0e0; padding: 3px 10px; border-radius: 20px;">Total: <?php echo count($urls_g1); ?></span>
                </h2>
                <p>Shortcode: <code>[feeds_grupo_1]</code><br>
                <span style="color: #0073aa; font-weight: bold;">Caché lista: <?php echo $cache_g1; ?> items.</span><br>
                <span style="color: #d63638; font-size: 0.9em;">⏱️ Última ejecución silenciosa: <strong><?php echo esc_html(get_option('lector_ultimo_ajax_gen_grupo_1', 'Nunca')); ?></strong></span></p>
                
                <div style="margin-bottom: 15px; display:flex; gap:10px; border-bottom: 1px dashed #ccc; padding-bottom: 15px; flex-wrap: wrap;">
                    <form method="post" action="">
                        <?php wp_nonce_field('sinc_g1_nonce'); ?>
                        <button type="submit" name="sinc_g1" value="1" class="button button-primary">🔄 Sincronizar Lote (10)</button>
                    </form>
                    <form method="post" action="">
                        <?php wp_nonce_field('vaciar_g1_nonce'); ?>
                        <button type="submit" name="vaciar_g1" value="1" class="button" style="color: #d63638; border-color: #d63638;">🗑️ Vaciar Datos</button>
                    </form>
                </div>
                
                <form method="post" action="" style="margin-bottom: 20px; margin-top: 20px;">
                    <?php wp_nonce_field('guardar_g1_nonce'); ?>
                    <input type="url" name="url_feed_g1" class="regular-text" required placeholder="https://..." style="width: 70%;" />
                    <?php submit_button('Añadir Feed', 'secondary', 'nuevo_feed_g1', false); ?>
                </form>

                <form method="post" action="">
                    <?php wp_nonce_field('gestionar_g1_nonce'); ?>
                    <?php if (!empty($urls_g1)) : ?>
                        <div style="margin-bottom: 10px; display:flex; gap: 5px;">
                            <button type="button" class="button button-small" onclick="marcarTodosGen('g1', 'activo')">✓ Todos Activos</button>
                            <button type="button" class="button button-small" onclick="marcarTodosGen('g1', 'pausado')">⏸ Todos Pausados</button>
                            <button type="button" class="button button-small" onclick="marcarBorrarGen('g1')">🗑 Invertir Borrado</button>
                        </div>
                        <table class="widefat striped">
                            <thead><tr><th>URL</th><th>Estado</th><th>Quitar</th></tr></thead>
                            <tbody>
                                <?php foreach ($urls_g1 as $index => $item) : ?>
                                    <tr>
                                        <td><?php echo esc_html($item['url']); ?></td>
                                        <td>
                                            <select name="estado_g1[<?php echo $index; ?>]" class="estado_g1_select">
                                                <option value="activo" <?php selected($item['estado'], 'activo'); ?>>Activo</option>
                                                <option value="pausado" <?php selected($item['estado'], 'pausado'); ?>>Pausado</option>
                                            </select>
                                        </td>
                                        <td><label><input type="checkbox" name="g1_eliminar[]" value="<?php echo $index; ?>" class="g1_chk_borrar"> Borrar</label></td>
                                    </tr>
                                <?php endforeach; ?>
                            </tbody>
                        </table>
                        <br><?php submit_button('Guardar Cambios Grupo 1', 'primary', 'actualizar_g1', false); ?>
                    <?php else: ?>
                        <p>No hay feeds en el Grupo 1.</p>
                    <?php endif; ?>
                </form>

                <div style="margin-top: 30px; padding-top: 15px; border-top: 1px dashed #ccc;">
                    <h4 style="margin-top:0;">Herramientas JSON y OPML</h4>
                    <div style="display:flex; flex-direction: column; gap: 10px;">
                        <form method="post" action="">
                            <?php wp_nonce_field('exportar_g1_nonce'); ?>
                            <input type="hidden" name="exportar_g1" value="1">
                            <button type="submit" class="button">⬇️ Exportar JSON</button>
                        </form>
                        <form method="post" action="" enctype="multipart/form-data" style="display:flex; gap:5px;">
                            <?php wp_nonce_field('importar_g1_nonce'); ?>
                            <input type="file" name="archivo_importar_g1" accept=".json" required style="max-width: 190px;">
                            <button type="submit" name="importar_g1" value="1" class="button">⬆️ JSON (Sobrescribir)</button>
                        </form>
                        <form method="post" action="" enctype="multipart/form-data" style="display:flex; gap:5px;">
                            <?php wp_nonce_field('importar_opml_g1_nonce'); ?>
                            <input type="file" name="archivo_importar_opml_g1" accept=".opml,.xml" required style="max-width: 190px;">
                            <button type="submit" name="importar_opml_g1" value="1" class="button">⬆️ OPML (Añadir)</button>
                        </form>
                    </div>
                </div>
            </div>

            <!-- BLOQUE GRUPO 2 -->
            <div style="flex: 1; min-width: 450px; background: #fff; border: 1px solid #ccd0d4; padding: 20px;">
                <h2 style="margin-top: 0; border-bottom: 1px solid #eee; padding-bottom: 10px; display: flex; justify-content: space-between;">
                    <span>🖼️ Feeds - Grupo 2 (Cuadrícula Visual)</span>
                    <span style="font-size: 13px; font-weight: normal; background: #e0e0e0; padding: 3px 10px; border-radius: 20px;">Total: <?php echo count($urls_g2); ?></span>
                </h2>
                <p>Shortcode: <code>[feeds_grupo_2]</code><br>
                <span style="color: #0073aa; font-weight: bold;">Caché lista: <?php echo $cache_g2; ?> items.</span><br>
                <span style="color: #d63638; font-size: 0.9em;">⏱️ Última ejecución silenciosa: <strong><?php echo esc_html(get_option('lector_ultimo_ajax_gen_grupo_2', 'Nunca')); ?></strong></span></p>

                <div style="margin-bottom: 15px; display:flex; gap:10px; border-bottom: 1px dashed #ccc; padding-bottom: 15px; flex-wrap: wrap;">
                    <form method="post" action="">
                        <?php wp_nonce_field('sinc_g2_nonce'); ?>
                        <button type="submit" name="sinc_g2" value="1" class="button button-primary">🔄 Sincronizar Lote (10)</button>
                    </form>
                    <form method="post" action="">
                        <?php wp_nonce_field('vaciar_g2_nonce'); ?>
                        <button type="submit" name="vaciar_g2" value="1" class="button" style="color: #d63638; border-color: #d63638;">🗑️ Vaciar Datos</button>
                    </form>
                </div>
                
                <form method="post" action="" style="margin-bottom: 20px; margin-top: 20px;">
                    <?php wp_nonce_field('guardar_g2_nonce'); ?>
                    <input type="url" name="url_feed_g2" class="regular-text" required placeholder="https://..." style="width: 70%;" />
                    <?php submit_button('Añadir Feed', 'secondary', 'nuevo_feed_g2', false); ?>
                </form>

                <form method="post" action="">
                    <?php wp_nonce_field('gestionar_g2_nonce'); ?>
                    <?php if (!empty($urls_g2)) : ?>
                        <div style="margin-bottom: 10px; display:flex; gap: 5px;">
                            <button type="button" class="button button-small" onclick="marcarTodosGen('g2', 'activo')">✓ Todos Activos</button>
                            <button type="button" class="button button-small" onclick="marcarTodosGen('g2', 'pausado')">⏸ Todos Pausados</button>
                            <button type="button" class="button button-small" onclick="marcarBorrarGen('g2')">🗑 Invertir Borrado</button>
                        </div>
                        <table class="widefat striped">
                            <thead><tr><th>URL</th><th>Estado</th><th>Quitar</th></tr></thead>
                            <tbody>
                                <?php foreach ($urls_g2 as $index => $item) : ?>
                                    <tr>
                                        <td><?php echo esc_html($item['url']); ?></td>
                                        <td>
                                            <select name="estado_g2[<?php echo $index; ?>]" class="estado_g2_select">
                                                <option value="activo" <?php selected($item['estado'], 'activo'); ?>>Activo</option>
                                                <option value="pausado" <?php selected($item['estado'], 'pausado'); ?>>Pausado</option>
                                            </select>
                                        </td>
                                        <td><label><input type="checkbox" name="g2_eliminar[]" value="<?php echo $index; ?>" class="g2_chk_borrar"> Borrar</label></td>
                                    </tr>
                                <?php endforeach; ?>
                            </tbody>
                        </table>
                        <br><?php submit_button('Guardar Cambios Grupo 2', 'primary', 'actualizar_g2', false); ?>
                    <?php else: ?>
                        <p>No hay feeds en el Grupo 2.</p>
                    <?php endif; ?>
                </form>

                <div style="margin-top: 30px; padding-top: 15px; border-top: 1px dashed #ccc;">
                    <h4 style="margin-top:0;">Herramientas JSON y OPML</h4>
                    <div style="display:flex; flex-direction: column; gap: 10px;">
                        <form method="post" action="">
                            <?php wp_nonce_field('exportar_g2_nonce'); ?>
                            <input type="hidden" name="exportar_g2" value="1">
                            <button type="submit" class="button">⬇️ Exportar JSON</button>
                        </form>
                        <form method="post" action="" enctype="multipart/form-data" style="display:flex; gap:5px;">
                            <?php wp_nonce_field('importar_g2_nonce'); ?>
                            <input type="file" name="archivo_importar_g2" accept=".json" required style="max-width: 190px;">
                            <button type="submit" name="importar_g2" value="1" class="button">⬆️ JSON (Sobrescribir)</button>
                        </form>
                        <form method="post" action="" enctype="multipart/form-data" style="display:flex; gap:5px;">
                            <?php wp_nonce_field('importar_opml_g2_nonce'); ?>
                            <input type="file" name="archivo_importar_opml_g2" accept=".opml,.xml" required style="max-width: 190px;">
                            <button type="submit" name="importar_opml_g2" value="1" class="button">⬆️ OPML (Añadir)</button>
                        </form>
                    </div>
                </div>
            </div>
        </div>
    </div>

    <script>
        function marcarTodosGen(tipo, estado) {
            const selectores = document.querySelectorAll('.estado_' + tipo + '_select');
            selectores.forEach(select => select.value = estado);
        }
        function marcarBorrarGen(tipo) {
            const checkboxes = document.querySelectorAll('.' + tipo + '_chk_borrar');
            if(checkboxes.length === 0) return;
            const nuevoEstado = !checkboxes[0].checked;
            checkboxes.forEach(chk => chk.checked = nuevoEstado);
        }
    </script>
    <?php
}

// ========================================================================
// 4. SHORTCODES GENÉRICOS
// ========================================================================
add_shortcode('feeds_grupo_1', 'lector_feeds_g1_shortcode');
function lector_feeds_g1_shortcode($atts) {
    $rss_items = get_option('lector_items_compilados_grupo_1', []);
    if (empty($rss_items)) {
        return '<p>Procesando... Recarga la página en unos minutos.</p>
                <script>
                document.addEventListener("DOMContentLoaded", function() {
                    fetch("'.admin_url('admin-ajax.php').'", {
                        method: "POST",
                        headers: { "Content-Type": "application/x-www-form-urlencoded" },
                        body: "action=lector_procesar_fondo_gen&tipo=grupo_1"
                    });
                });
                </script>';
    }
    
    $por_pagina = 10;
    $pagina_actual = isset($_GET['pag_g1']) ? max(1, intval($_GET['pag_g1'])) : 1;
    $paged_items = array_slice($rss_items, ($pagina_actual - 1) * $por_pagina, $por_pagina);

    ob_start();
    echo '<div style="display:flex; flex-direction:column; gap:1.5rem;">';
    foreach ($paged_items as $item) {
        $estilo_img = (strpos($item['imagen'], 'emoji') !== false) ? 'object-fit: contain; padding: 2rem;' : 'object-fit: cover;';
        
        echo '<a href="' . esc_url($item['enlace']) . '" target="_blank" rel="noopener nofollow" style="display:flex; border:1px solid #eaeaea; border-radius:8px; overflow:hidden; background:#fff; text-decoration:none; color:inherit; box-shadow: 0 2px 5px rgba(0,0,0,0.03); transition: transform 0.2s ease;" onmouseover="this.style.transform=\'translateY(-3px)\'" onmouseout="this.style.transform=\'none\'">';
        echo '<div style="width:35%; min-width:250px; background:#f9f9f9; display:flex; align-items:center; justify-content:center;"><img src="' . esc_url($item['imagen']) . '" alt="img" loading="lazy" style="width:100%; height:100%; max-height:220px; ' . $estilo_img . '" /></div>';
        echo '<div style="width:65%; padding:1.5rem; display:flex; flex-direction:column; justify-content:center;">';
        echo '<div style="font-size:0.85rem; color:#666; margin-bottom:0.5rem; display:flex; flex-wrap:wrap; gap:10px; align-items:center;">';
        if (!empty($item['fuente'])) { echo '<span style="background:#eee; padding:2px 8px; border-radius:4px; font-weight:600; color:#333;">' . esc_html($item['fuente']) . '</span>'; }
        echo '<span>' . wp_date('j \d\e F \d\e Y, H:i', $item['timestamp']) . '</span></div>';
        echo '<h3 style="font-size:1.3rem; margin:0 0 0.8rem 0; line-height:1.3; color:#222;">' . esc_html($item['titulo']) . '</h3>';
        if (isset($item['extracto'])) { echo '<p style="font-size:0.95rem; color:#555; margin:0; line-height:1.5;">' . $item['extracto'] . '</p>'; }
        echo '</div></a>';
    }
    echo '</div>';
    
    $total_paginas = ceil(count($rss_items) / $por_pagina);
    if ($total_paginas > 1) {
        $btn_style = "display:inline-block; padding:8px 16px; background:#0073aa; color:#fff; text-decoration:none; border-radius:4px; font-weight:600;";
        echo '<div style="display:flex; justify-content:space-between; margin-top:30px; padding-top:20px; border-top:1px solid #eaeaea;">';
        if ($pagina_actual > 1) { echo '<a href="' . esc_url(add_query_arg('pag_g1', $pagina_actual - 1)) . '" style="'.$btn_style.'">&laquo; Anteriores</a>'; } else { echo '<div></div>'; }
        if ($pagina_actual < $total_paginas) { echo '<a href="' . esc_url(add_query_arg('pag_g1', $pagina_actual + 1)) . '" style="'.$btn_style.'">Siguientes &raquo;</a>'; }
        echo '</div>';
    }
    
    echo '<script>
        document.addEventListener("DOMContentLoaded", function() {
            setTimeout(function() { 
                fetch("'.admin_url('admin-ajax.php').'", {
                    method: "POST",
                    headers: { "Content-Type": "application/x-www-form-urlencoded" },
                    body: "action=lector_procesar_fondo_gen&tipo=grupo_1"
                });
            }, 2000);
        });
    </script>';
    
    return ob_get_clean();
}

add_shortcode('feeds_grupo_2', 'lector_feeds_g2_shortcode');
function lector_feeds_g2_shortcode($atts) {
    $rss_items = get_option('lector_items_compilados_grupo_2', []);
    if (empty($rss_items)) {
        return '<p>Procesando... Recarga la página en unos minutos.</p>
                <script>
                document.addEventListener("DOMContentLoaded", function() {
                    fetch("'.admin_url('admin-ajax.php').'", {
                        method: "POST",
                        headers: { "Content-Type": "application/x-www-form-urlencoded" },
                        body: "action=lector_procesar_fondo_gen&tipo=grupo_2"
                    });
                });
                </script>';
    }
    
    $por_pagina = 15;
    $pagina_actual = isset($_GET['pag_g2']) ? max(1, intval($_GET['pag_g2'])) : 1;
    $paged_items = array_slice($rss_items, ($pagina_actual - 1) * $por_pagina, $por_pagina);

    ob_start();
    echo '<div style="display:grid; grid-template-columns:repeat(auto-fill, minmax(280px, 1fr)); gap:1.5rem;">';
    foreach ($paged_items as $item) {
        echo '<a href="' . esc_url($item['enlace']) . '" target="_blank" rel="noopener nofollow" style="display:flex; flex-direction:column; border:1px solid #eaeaea; border-radius:8px; overflow:hidden; background:#fff; text-decoration:none; color:inherit; box-shadow: 0 2px 5px rgba(0,0,0,0.03); transition: transform 0.2s ease;" onmouseover="this.style.transform=\'translateY(-3px)\'" onmouseout="this.style.transform=\'none\'">';
        echo '<div style="width:100%; height:260px; background:#f5f5f5; display:flex; align-items:center; justify-content:center; border-bottom:1px solid #eaeaea;"><img src="' . esc_url($item['imagen']) . '" alt="img" loading="lazy" style="width:100%; height:100%; object-fit:contain;" /></div>';
        echo '<div style="padding:1rem; display:flex; flex-direction:column;">';
        if (!empty($item['fuente'])) { echo '<span style="font-size:0.75rem; font-weight:700; text-transform:uppercase; color:#555; margin-bottom:0.4rem; letter-spacing:0.5px;">' . esc_html($item['fuente']) . '</span>'; }
        echo '<h3 style="font-size:1.1rem; margin:0 0 0.5rem 0; line-height:1.3; color:#222; font-weight:600;">' . esc_html($item['titulo']) . '</h3>';
        echo '<span style="font-size:0.8rem; color:#888; margin-top:auto;">' . wp_date('j \d\e F \d\e Y', $item['timestamp']) . '</span>';
        echo '</div></a>';
    }
    echo '</div>';

    $total_paginas = ceil(count($rss_items) / $por_pagina);
    if ($total_paginas > 1) {
        $btn_style = "display:inline-block; padding:8px 16px; background:#0073aa; color:#fff; text-decoration:none; border-radius:4px; font-weight:600;";
        echo '<div style="display:flex; justify-content:space-between; margin-top:30px; padding-top:20px; border-top:1px solid #eaeaea;">';
        if ($pagina_actual > 1) { echo '<a href="' . esc_url(add_query_arg('pag_g2', $pagina_actual - 1)) . '" style="'.$btn_style.'">&laquo; Anteriores</a>'; } else { echo '<div></div>'; }
        if ($pagina_actual < $total_paginas) { echo '<a href="' . esc_url(add_query_arg('pag_g2', $pagina_actual + 1)) . '" style="'.$btn_style.'">Siguientes &raquo;</a>'; }
        echo '</div>';
    }

    echo '<script>
        document.addEventListener("DOMContentLoaded", function() {
            setTimeout(function() { 
                fetch("'.admin_url('admin-ajax.php').'", {
                    method: "POST",
                    headers: { "Content-Type": "application/x-www-form-urlencoded" },
                    body: "action=lector_procesar_fondo_gen&tipo=grupo_2"
                });
            }, 2000);
        });
    </script>';
    
    return ob_get_clean();
}

Informazioni sul codice: prestazioni, transients e opzioni

Quando si crea un aggregatore di feed per WordPress, i principali nemici sono il tempo di blocco e la saturazione del database. Uno script non ottimizzato che legga 20 o 30 feed RSS in tempo reale può far sì che il tuo sito impieghi diversi secondi a caricarsi (o, peggio ancora, causare un timeout del server). Per evitarlo, questo snippet applica tre livelli di ottimizzazione tecnica:

1. L'archiviazione persistente (opzioni di WordPress vs database)

Anziché creare tabelle personalizzate (il che comporterebbe una maggiore complessità e un carico di manutenzione superfluo), lo script memorizza i risultati già elaborati nella tabella wp_options tramite la funzione nativa update_option().

  • Cosa viene salvato? Da un lato, gli URL dei feed e i loro stati (attivi/in pausa). Dall’altro, l’array gigante contenente gli ultimi 100 o 150 elementi già compilati, puliti e ordinati cronologicamente in base al loro timestamp.

2. WPO, addio all'autoload

Per impostazione predefinita, qualsiasi opzione salvata con `update_option() ` in WordPress viene creata con il parametro ` autoload = yes`. Ciò costringe MySQL a caricare tali dati nella memoria RAM globale ad ogni singola richiesta ricevuta dal tuo sito web, indipendentemente dal fatto che l’utente stia visitando la home page, un post o una pagina e non l’URL in cui è stato inserito lo shortcode del lettore di feed.

  • La soluzione adottata: tutte le operazioni di salvataggio dello script specificano esplicitamente il terzo parametro come false (update_option(..., false)).
  • Il risultato: il peso dei dati raccolti non incide sul carico automatico complessivo. Il database rimane inattivo e i dati vengono consultati solo su richiesta specifica dello shortcode.

3. Gestione della cache e natura temporanea dei feed RSS

WordPress include nativamente la libreria SimplePie tramite la funzione `fetch_feed()`nel proprio nucleo. Tuttavia, la sua gestione predefinita della cache dei file XML esterni può risultare imprevedibile.

  • Timeout rigorosi: viene impostato un limite di attesa di 3 secondi ($feed->set_timeout(3);). Se una fonte esterna è inattiva o lenta, lo script non blocca il server; interrompe la connessione in modo corretto e passa al lotto successivo.
  • Cache temporanea di estrazione:viene applicato un filtro affinché la cache interna temporanea di WordPress per i feed RSS abbia una durata di 5 minuti (return 300;). Ciò impedisce che visite multiple consecutive generino richieste massicce e non necessarie ai server degli autori originali.

4. Elaborazione asincrona (AJAX invisibile)

Affinché l'utente che visita il sito web non si accorga di nulla, la sincronizzazione non viene mai eseguita durante il caricamento visibile della pagina (front-end).

  • Viene utilizzato un hook AJAX silenzioso (admin-ajax.php) che viene attivato tramite un breve script JavaScript asincrono con un leggero ritardo (setTimeout).
  • In questo modo il carico viene suddiviso in lotti di 5 per ciascuna delle 5 fonti, garantendo che il server elabori le informazioni in modo frazionato e del tutto trasparente per il visitatore.

Lascia un commento

Qualcosa da dire?

Este blog se aloja en LucusHost

LucusHost,el mejor hosting