Como criar um agregador de feeds RSS no WordPress (sem prejudicar seu WPO)

No comments

11.09.2026|

No comments

Tiempo de lectura Lectura: 28 min, 42 s
Número de palabras Palabras: 5310
Número de visitas Visitas: 47
Icono de traducción
Como criar um agregador de feeds RSS no WordPress (sem prejudicar seu WPO)
O snippet permite adicionar dois grupos de feeds. Um com disposição horizontal e trecho de texto, e outro em formato de grade, sem trecho de texto, destinado a imagens.

Como talvez você já tenha percebido, a página“Notícias” foi restaurada e a página“Vignetas” — que dependiam de um serviço externo (Elfsight) — foi reformulada. Para isso, foi criado um pequeno mecanismo que funciona como agregador/leitor, e o carregamento é quase instantâneo. Nesta postagem, você encontrará o código genérico, caso queira testá-lo. Também é possível usá-lo em uma página privada como um simples leitor.

No caso das tirinhas, foi adicionada uma paginação com um limite de 10 páginas, com 15 tirinhas por página, o que é suficiente para consultar um histórico de cerca de 4 dias. Para as notícias, são exibidasas 10 publicações mais recentes no total, ordenadas cronologicamente e com paginação para que seja possível ler as anteriores até umlimite de 10 páginas, o que também corresponde a aproximadamente quatro dias (dependendo do fluxo de publicações do conjunto de fontes). A atualização é esporádica. A atualização ocorre a cada 4 horas, mas você pode atualizar manualmente a qualquer momento.

 Você pode sugerir novas fontes (desde que tenham feed RSS). Para saber se o endereço que você propõe é válido,acesse o Simple Piee teste-o antes.

Ascensão e declínio da sindicação: XML e feeds

Antes de entrarmos no código, é preciso entender o que estamos usando. Um arquivo XML (eXtensible Markup Language) é uma linguagem de marcação projetada para armazenar e transmitir dados de forma estruturada. Com base nisso, surgiu o Feed RSS (Really Simple Syndication), um arquivo padronizado que os sites geram automaticamente sempre que publicam conteúdo.

  • A era de ouro: Na década de 2000, com a explosão dos blogs, o RSS era o rei. Ferramentas que já não existem mais, como o Google Reader (2005–2013), permitiam que os usuários se inscrevessem em centenas de sites e os lessem em um único lugar, sem precisar visitar cada página da web individualmente; mesmo assim, ainda existem alternativas para a leitura de feeds.
  • O desuso atual: Hoje em dia, os feeds estão em declínio por vários motivos. Com os blogs desaparecendo aos montes a cada dia e o surgimento das redes sociais, dos algoritmos de retenção e das newsletters fechadas, além das IAs dos mecanismos de busca que se aproveitam dos resumos (o que já é conhecido como “Zero Click Search”, ou busca sem cliques), muitos veículos de comunicação eliminaram seus feeds RSS para forçar o usuário a navegar em seus sites. Isso tem tornado cada vez mais complicado encontrar fontes temáticas disponíveis com RSS.

Como funciona esse gerador de feeds no WordPress

Este snippet foi projetado com uma arquitetura que prioriza o desempenho e o WPO.

  • Processamento assíncrono (AJAX invisível): Não bloqueia o carregamento da página. O servidor baixa os feeds em segundo plano, em pequenos lotes de 5 por vez, acionado pelo tráfego orgânico dos visitantes.
  • Impacto nulo no banco de dados: os dados processados são armazenados na tabela de opções com o status autoload = false. Isso significa que eles não sobrecarregam a memória global do site e só são carregados quando a página de leitura os solicita.
  • Links limpos: Exiba os artigos externos com os atributos target="_blank" e rel="noopener nofollow", protegendo o SEO do seu site e evitando a perda de autoridade.

Dicas sobre o uso de conteúdo de terceiros

Embora o formato tenha sido concebido para apresentar o conteúdo em leitores e páginas, ao adicionar conteúdo de terceiros, é imprescindível seguir boas práticas éticas e técnicas para não prejudicar nem o seu servidor nem os autores originais:

  • Hotlinking e bloqueio de imagens: O snippet carrega as imagens diretamente do servidor original (hotlinking). Isso economiza espaço no seu disco, mas os autores ou proprietários podem impor limitações e restrições; se houver proteções contra hotlinking no arquivo .htaccess, as imagens aparecerão corrompidas.
  • Respeito à largura de banda: Não abuse da frequência de sincronização. Fazer solicitações constantes a servidores de terceiros pode fazer com que o firewall deles (como o Cloudflare) bloqueie o endereço IP do seu servidor, considerando-o um bot malicioso.
  • Propriedade intelectual: Extraia apenas o título e um breve resumo (o código limita a 25 palavras). O objetivo de um agregador ético é redirecionar o tráfego para o autor original, não roubar o conteúdo completo dele.
  • O conteúdo exibido pode mudar. É importante ficar atento a isso. A fonte pode alterar o conteúdo exibido ou até mesmo remover o feed. Isso pode fazer com que ele desapareça do seu site ou que seja exibido algo diferente do que você esperava. Verifique suas fontes de vez em quando.

Código

Serviço de otimização do WordPress

É muito simples de usar. Basta adicionar o código no arquivo functions.php do seu tema ou no seu plugin de snippets, e a área de administração aparecerá em Configurações/Gerenciador de Feeds, com os shortcodes e as configurações para adicionar, pausar ou excluir os links inseridos, além de exportar e importar um arquivo JSON com suas listas.

Abaixo do código, você encontrará alguns detalhes técnicos sobre o desempenho.

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

Sobre o código: desempenho, transientes e opções

Ao criar um agregador de feeds para o WordPress, os maiores inimigos são o tempo de bloqueio e a saturação do banco de dados. Um script mal otimizado que leia 20 ou 30 feeds RSS em tempo real pode fazer com que seu site demore vários segundos para carregar (ou, pior ainda, provocar um timeout do servidor). Para evitar isso, este snippet aplica três camadas de otimização técnica:

1. Armazenamento persistente (opções do WordPress x banco de dados)

Em vez de criar tabelas personalizadas (o que aumentaria a complexidade e geraria manutenção desnecessária), o script armazena os resultados já processados na tabela wp_options por meio da função nativa update_option().

  • O que é armazenado? Por um lado, as URLs dos feeds e os status (ativos/em pausa). Por outro, a matriz gigante (array) com os últimos 100 ou 150 elementos já compilados, limpos e ordenados cronologicamente por seu timestamp.

2. WPO: adeus ao autoload

Por padrão, qualquer opção que você salvar com a função ` update_option() ` no WordPress é criada com o parâmetro ` autoload = yes`. Isso obriga o MySQL a carregar esses dados na memória RAM global a cada solicitação recebida pelo seu site, mesmo que o usuário esteja acessando a página inicial, qualquer postagem ou página, e não a URL onde o shortcode do leitor de feeds foi adicionado.

  • A solução aplicada: Todos os comandos de salvamento do script definem explicitamente o terceiro parâmetro como false (update_option(..., false)).
  • O resultado: o tamanho dos dados compilados não é incluído no carregamento automático global. O banco de dados permanece inativo e os dados só são consultados mediante solicitação exclusiva do shortcode.

3. Gerenciamento de cache e a natureza transitória do RSS

O WordPress inclui nativamente a biblioteca SimplePie por meio da função `fetch_feed()`em seu núcleo. No entanto, seu gerenciamento padrão do cache de arquivos XML externos pode ser imprevisível.

  • Timeouts rigorosos: É definido um limite de espera de 3 segundos ($feed->set_timeout(3);). Se uma fonte externa estiver fora do ar ou lenta, o script não trava o seu servidor; ele encerra a conexão de maneira educada e segue para o próximo lote.
  • Cache temporário de extração:É aplicado um filtro para que o cache interno temporário do WordPress para o RSS tenha duração de 5 minutos (return 300;). Isso evita que várias visitas consecutivas gerem solicitações em massa e desnecessárias aos servidores dos autores originais.

4. Processamento assíncrono (AJAX invisível)

Para que o usuário que acessa o site não perceba absolutamente nada, a sincronização nunca é executada durante o carregamento visível da página (front-end).

  • É utilizado um gancho AJAX silencioso (admin-ajax.php) que é acionado por meio de um pequeno script JavaScript assíncrono com um pequeno atraso (setTimeout).
  • Isso divide a carga em lotes de 5 por 5 fontes, garantindo que o servidor processe as informações de forma fragmentada e de maneira totalmente transparente para o visitante.

Deixe um comentário

Tem algo a dizer?

Este blog se aloja en LucusHost

LucusHost,el mejor hosting