Comment créer un agrégateur de flux RSS sous WordPress (sans nuire aux performances de votre site)

Aucun commentaire

30.08.2026|

Aucun commentaire

Tiempo de lectura Lectura: 28 min, 5 s
Número de palabras Palabras: 5198
Número de visitas Visitas: 28
Icono de traducción
Cet extrait de code permet d'ajouter deux groupes de flux. L'un est présenté horizontalement avec un extrait, et l'autre sous forme de grille sans extrait, destiné aux images.

Comme tu l'as peut-être déjà remarqué, la page «Actualités » a été rétablie et celle des «Bandes dessinées » – qui dépendaient d'un service externe (Elfsight) – a été remaniée. Pour cela, un petit moteur a été créé pour servir d'agrégateur/lecteur, et le chargement est quasi instantané. Dans cet article, tu trouveras le code générique, au cas où tu souhaiterais l'essayer. Tu peux également l'utiliser sur une page privée comme simple lecteur.

En ce qui concerne les bandes dessinées, une pagination a été ajoutée, avec une limite de 10 pages à raison de 15 cases par page, ce qui suffit pour consulter l'historique d'environ 4 jours. Pour les actualités,les 10 publications les plus récentes sont affichéesau total, classées par ordre chronologique et avec une pagination permettant de consulter les précédentes jusqu'à unelimite de 10 pages, soit environ quatre jours également (en fonction du flux de publication de l'ensemble des sources).

 Vous pouvez proposer de nouvelles sources (à condition qu'elles disposent d'un flux RSS). Pour vérifier si l'adresse que vous proposez est valide, vous pouvezvous rendre sur Simple Pieet la tester au préalable.

Essor et déclin de la syndication : XML et flux

Avant d'aborder le code, il est nécessaire de comprendre ce que nous utilisons. Un fichier XML (eXtensible Markup Language) est un langage de balisage conçu pour stocker et transmettre des données de manière structurée. C'est sur cette base qu'est né le flux RSS (Really Simple Syndication), un fichier standardisé que les sites web génèrent automatiquement chaque fois qu'ils publient du contenu.

  • L'âge d'or : dans les années 2000, avec l'explosion des blogs, le RSS régnait en maître. Des outils aujourd’hui disparus, comme Google Reader, permettaient aux utilisateurs de s’abonner à des centaines de sites et de les consulter en un seul endroit sans avoir à visiter chaque page web individuellement ; il existe toutefois encore des alternatives pour la lecture des flux.
  • Le déclin actuel : aujourd’hui, les flux RSS sont en déclin pour plusieurs raisons. Avec la disparition massive de blogs chaque jour et l’irruption des réseaux sociaux, des algorithmes de rétention et des newsletters fermées, sans compter les IA des moteurs de recherche qui parasitent les résumés (ce que l’on appelle désormais la « Zero Click Search », ou recherche sans clic), de nombreux médias ont supprimé leurs flux RSS afin de contraindre l’utilisateur à naviguer sur leurs sites. Il est donc de plus en plus difficile de trouver des sources thématiques disponibles via RSS.

Comment fonctionne ce moteur de flux dans WordPress ?

Cet extrait de code est conçu selon une architecture qui privilégie les performances et le WPO.

  • Traitement asynchrone (AJAX invisible) : ne bloque pas le chargement de la page. Le serveur télécharge les flux en arrière-plan, par petits lots de 5, déclenché par le trafic naturel des visiteurs.
  • Aucun impact sur la base de données : les données traitées sont enregistrées dans la table des options avec le statut « autoload = false ». Cela signifie qu'elles n'encombrent pas la mémoire globale du site et ne sont chargées que lorsque la page de lecture en fait la demande.
  • Liens propres : affichez les articles externes avec les attributs target="_blank" et rel="noopener nofollow", afin de protéger le référencement naturel de votre site et d'éviter les fuites d'autorité.

Conseils concernant l'utilisation de contenus tiers

Bien que ce format soit conçu pour diffuser du contenu sur des lecteurs et des pages, lorsque vous ajoutez du contenu provenant d'autres sources, il est indispensable d'appliquer les bonnes pratiques éthiques et techniques afin de ne nuire ni à votre serveur ni aux auteurs originaux :

  • Liens directs et blocage des images : l'extrait de code charge les images directement depuis le serveur d'origine (liens directs). Cela permet d'économiser de l'espace sur votre disque, mais les auteurs ou propriétaires peuvent imposer des limitations et des restrictions ; s'ils ont mis en place des protections anti-liens directs dans leur fichier .htaccess, les images s'afficheront de manière incorrecte.
  • Respectez la bande passante : n’abusez pas de la fréquence de synchronisation. Envoyer des requêtes en continu à des serveurs tiers peut amener leur pare-feu (comme Cloudflare) à bloquer l’adresse IP de votre serveur, en le considérant comme un bot malveillant.
  • Propriété intellectuelle : N'extrait que le nom de l'auteur et un bref résumé (le code limite celui-ci à 25 mots). L'objectif d'un agrégateur éthique est de rediriger le trafic vers l'auteur d'origine, et non de lui voler l'intégralité de son contenu.
  • Le contenu diffusé peut changer. C'est un point important à prendre en compte. La source peut modifier le contenu affiché, voire supprimer le flux. Cela peut entraîner sa disparition de votre site ou l'affichage d'un contenu différent de celui auquel vous vous attendiez. Vérifiez vos sources de temps en temps.

Code

Son utilisation est très simple. Ajoutez le code dans le fichier functions.php de votre thème ou dans votre plugin de snippets, et la zone d'administration apparaîtra dans Paramètres/Gestionnaire de flux, avec les shortcodes et les paramètres permettant d'ajouter, de mettre en pause ou de supprimer les liens ajoutés, ainsi que d'exporter et d'importer un fichier JSON contenant vos listes.

Sous le code, vous trouverez quelques détails techniques concernant les performances.

// ========================================================================
// 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;
    }

    $batch = array_slice($active_urls, $current_idx, 5);
    update_option($idx_opt, $current_idx + 5, 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);
    
    $custom_cache = function(){ return 300; }; 
    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);
    }
    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() {
    $tipo = isset($_GET['tipo']) ? sanitize_text_field($_GET['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;

    // EXPORTAR JSON
    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;
    }

    // IMPORTAR JSON
    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.');

    // IMPORTAR OPML
    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> | <span style="color: #0073aa; font-weight: bold;">Caché lista: <?php echo $cache_g1; ?> items.</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</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> | <span style="color: #0073aa; font-weight: bold;">Caché lista: <?php echo $cache_g2; ?> items.</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</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>fetch("'.admin_url('admin-ajax.php').'?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) {
        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; object-fit:cover;" /></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').'?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>fetch("'.admin_url('admin-ajax.php').'?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').'?action=lector_procesar_fondo_gen&tipo=grupo_2"); }, 2000); });</script>';
    return ob_get_clean();
}

À propos du code : performances, transitoires et options

Lorsqu'on développe un agrégateur de flux pour WordPress, les principaux ennemis sont le temps de blocage et la saturation de la base de données. Un script mal optimisé qui lit 20 ou 30 flux RSS en temps réel peut entraîner un temps de chargement de plusieurs secondes pour votre site (ou pire, provoquer un délai d'expiration du serveur). Pour éviter cela, cet extrait de code applique trois niveaux d'optimisation technique :

1. Le stockage persistant (options de WordPress vs base de données)

Au lieu de créer des tables personnalisées (ce qui ajouterait une complexité et une charge de maintenance inutiles), le script stocke les résultats déjà traités dans la table wp_options à l'aide de la fonction native update_option().

  • Quelles informations sont conservées ? D'une part, les URL des flux et leur statut (actifs/suspendus). D'autre part, le tableau géant (array) contenant les 100 ou 150 derniers éléments déjà compilés, nettoyés et classés par ordre chronologique en fonction de leur horodatage.

2. WPO : adieu à l'autoload

Par défaut, toute option que vous enregistrez avec la fonction ` update_option()` dans WordPress est créée avec le paramètre ` autoload = yes`. Cela oblige MySQL à charger ces données dans la mémoire RAM globale à chaque requête reçue par votre site, que l'utilisateur consulte la page d'accueil, un article ou une page, et non l'URL où le shortcode du lecteur de flux a été ajouté.

  • Solution mise en œuvre : toutes les opérations d'enregistrement du script définissent explicitement le troisième paramètre sur « false » (update_option(..., false)).
  • Résultat : le poids des données compilées n'entre pas en ligne de compte dans le chargement automatique global. La base de données reste inactive et les données ne sont consultées qu'à la demande exclusive du shortcode.

3. Gestion du cache et caractère temporaire des flux RSS

WordPress intègre nativement la bibliothèque SimplePie via la fonction `fetch_feed()` dans son noyau. Cependant, sa gestion par défaut du cache des fichiers XML externes peut s'avérer capricieuse.

  • Délais d'attente stricts : nous imposons un délai d'attente de 3 secondes ($feed->set_timeout(3);). Si une source externe est hors service ou lente, le script ne bloque pas votre serveur ; il interrompt poliment la connexion et passe au lot suivant.
  • Cache temporaire de récupération : nous avons appliqué un filtre afin que la durée de vie du cache interne temporaire de WordPress pour les flux RSS soit de 5 minutes (return 300;). Cela évite que plusieurs visites successives génèrent un nombre massif de requêtes inutiles vers les serveurs des auteurs d'origine.

4. Traitement asynchrone (AJAX invisible)

Afin que l'utilisateur qui visite le site web ne remarque absolument rien, la synchronisation ne s'effectue jamais lors du chargement visible de la page (front-end).

  • On utilise un hook AJAX silencieux (admin-ajax.php) qui est déclenché par un petit script JavaScript asynchrone avec un léger délai (setTimeout).
  • Cela permet de répartir la charge en lots de 5 sources chacun, garantissant ainsi que le serveur traite les informations par tranches, de manière totalement transparente pour le visiteur.

Laisser un commentaire

Vous avez quelque chose à dire ?

Este blog se aloja en LucusHost

LucusHost, el mejor hosting