最近偷窥大佬【似水流年】的友链RSS聚合页,在流年兄的大力指导和支持下终于完美上线。

展示:朋友的圈 – 品味苏州 (pwsz.com) 导航简称(友圈)。

此页面聚合了本站友链中经常更新的朋友们的rss数据,终于不用再每次一个个点击网站查看了,据说每年可以节省一个好友鼠标,科技啊,呵~让人们开源节流!

经过大佬的悉心指导,让我也弄懂了一些原理,简单讲讲:

1、创建一个php文件,比如 rss.php ,放在页面模板的文件夹下,据说有的是在主题的 page文件夹,而我的就直接是主题的根目录,又要深深吐槽下island主题作者大伟哥的不靠谱,连个文件夹都不给人家!

<?php
/*
Template Name: 似水流年-RSS 朋友圈
*/

date_default_timezone_set('Asia/Shanghai');
get_header();

// 引入 SimplePie 类
require_once(ABSPATH. WPINC. '/class-simplepie.php');

// 主内容区域
echo '<div id="primary" class="content-area">';
echo '<main id="main" class="site-main" role="main">';
echo '<p style="color: red; font-size: 14px; margin-top: 10px; margin-bottom: 20px; text-align: center; display: flex; align-items: center; justify-content: center;">以下友情链接网站最新内容每 8 小时获取更新一次</p>';

// 获取并缓存友情链接的最新文章数据
function get_cached_friend_link_rss_data() {
    $cache_key = 'friend_link_rss_data';
    // 尝试从对象缓存中获取数据
    $cached_data = wp_cache_get($cache_key);
    if ($cached_data) {
        return $cached_data;
    }
    $all_articles = array();
    $links = get_bookmarks(array(
        'orderby' => 'name',
        'category' => 0,
        'order' => 'ASC',
        'hide_invisible' => 1,
        'categorize' => 0,
        'title_li' => '',
        'echo' => 0
    ));

    foreach ($links as $link) {
        if ($link->link_rss &&!empty($link->link_rss)) {
            $domain = parse_url($link->link_url, PHP_URL_HOST);
            if (!$domain) {
                continue;
            }

            $feed_url = $link->link_rss;
            $cache_file = get_stylesheet_directory(). '/rsscache/'. $domain. '.xml';

            // 如果缓存文件不存在或超过 8 小时,且 RSS 内容有更新,才更新缓存
            if (!file_exists($cache_file) || time() - filemtime($cache_file) > 8 * HOUR_IN_SECONDS) {
                $rss_content = get_rss_content($feed_url);
                if ($rss_content) {
                    $existing_content = file_exists($cache_file)? file_get_contents($cache_file) : false;
                    if ($existing_content!== $rss_content) {
                        file_put_contents($cache_file, $rss_content);
                    }
                }
            }

            $rss_items = get_rss_items($cache_file);
            foreach ($rss_items as $item) {
                $item_data = array(
                    'link_name' => $link->link_name,
                    'link_logo' => $link->link_image,
                    'link_url' => $link->link_url,
                    'item_title' => $item->get_title(),
                    'item_link' => $item->get_permalink(),
                    'item_date' => $item->get_date('Y-m-d H:i:s'),
                    'item_author' => $item->get_author()->get_name(),
                    'item_excerpt' => $item->get_description(),
                    'item_content' => $item->get_content()
                );
                $all_articles[$link->link_name][] = $item_data;
            }
        }
    }

    // 将数据存入对象缓存
    wp_cache_set($cache_key, $all_articles, null, 8 * HOUR_IN_SECONDS);
    return $all_articles;
}

// 获取最新的友情链接文章
function get_latest_friend_link_articles() {
    $latest_articles = get_cached_friend_link_rss_data();
    $all_articles = array();
    foreach ($latest_articles as $link_name => $articles) {
        foreach ($articles as $article) {
            $all_articles[] = $article;
        }
    }

    usort($all_articles, function ($a, $b) {
        $date1 = date('Y-m-d', strtotime($a['item_date']));
        $date2 = date('Y-m-d', strtotime($b['item_date']));
        if ($date1 === $date2) {
            return strcmp($a['link_name'], $b['link_name']);
        } else {
            return strtotime($b['item_date']) - strtotime($a['item_date']);
        }
    });

    $sorted_articles = array();
    foreach ($all_articles as $article) {
        $sorted_articles[$article['link_name']][] = $article;
    }

    foreach ($sorted_articles as $link_name => $articles) {
        usort($articles, function ($a, $b) {
            return strtotime($b['item_date']) - strtotime($a['item_date']);
        });
        $sorted_articles[$link_name] = array_slice($articles, 0, 5);
    }

    return $sorted_articles;
}

// 获取并保存最新的 RSS 内容
function get_rss_content($feed_url) {
    $response = wp_safe_remote_get($feed_url, array('sslverify' => true));
    if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200) {
        return wp_remote_retrieve_body($response);
    } else {
        $response = wp_safe_remote_get($feed_url, array('sslverify' => false));
        if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200) {
            return wp_remote_retrieve_body($response);
        } else {
            return '无法从提供 RSS 的 URL 获取 RSS 内容。';
        }
    }
}

// 使用 SimplePie 获取 RSS 项
function get_rss_items($cache_file) {
    $rss = new SimplePie();
    $rss->set_feed_url($cache_file);
    $rss->init();
    $rss->handle_content_type();
    $rss->set_output_encoding('UTF-8');
    return $rss->get_items();
}

// 获取并显示友情链接的最新文章
$latest_articles = get_latest_friend_link_articles();
foreach ($latest_articles as $link_name => $articles) {
   ?>
    <div class="friend-link-container" style="border: 1px dashed #000; padding: 10px; margin-bottom: 10px; display: flex; align-items: center;">
        <!-- 左侧显示友情链接的网站 Logo 和名称 -->
        <div class="friend-link-info" style="width: 20%; text-align: center;">
            <img src="<?php echo esc_url($articles[0]['link_logo']);?>" alt="<?php echo esc_attr($link_name);?> Logo" style="border-radius: 50%; width: 60px; height: 60px;">
            <a href="<?php echo esc_url($articles[0]['link_url']);?>" target="_blank"><p style="margin: 0; padding: 0;"><?php echo esc_html($link_name);?></p></a>
        </div>
        <!-- 右侧显示最新更新的前五篇文章 -->
        <div class="friend-link-articles" style="width: 80%; padding-left: 10px;">
            <?php foreach ($articles as $article) :?>
                <p style="margin: 0; padding: 0;">
                    <a href="<?php echo esc_url($article['item_link']);?>" target="_blank"><?php echo esc_html($article['item_title']);?></a>(<?php echo esc_html(date('Y-m-d', strtotime($article['item_date'])));?>)
                </p>
            <?php endforeach;?>
        </div>
    </div>
    <?php
}

// 侧边栏
get_sidebar();

echo '</main><!-- #main -->';
echo '</div><!-- #primary -->';

get_footer();
?>

2、创建一个新页面,填写标题,比如:朋友圈,右侧的模板里,选择刚创建的页面模板“RSS 朋友圈”,保存。以后访问这个页面,就是RSS聚合页。

3、链接里,在高级一栏中填写“图片地址”和“RSS地址”,如不填,则不会出现在聚合页,填写后,就会自动采集RSS数据,8小时更新一次。

4、在主题的根目录,创建一个新的文件夹:rsscache,采集到的RSS数据会保存在这里文件夹里,8小时后再次采集时覆盖保存。

简单四步就搞定了。但是我的博客比较奇葩,实在是太老了(又要吐槽大伟哥,钱收了、手甩了),中间出现不少小bug,流年大佬不辞辛苦搞到半夜,一遍遍的帮我排查、修改代码,最终完美呈现,心中感激无以言表,哈哈哈~再次感谢优秀80后代表流年大佬~

2024.12.16 似水流年更新,优化如下:

减少不必要的缓存获取尝试,利用 Swoole 的协程等功能来实现并发获取 RSS 内容。

同时发起多个 HTTP 请求去获取不同友情链接的 RSS 内容,减少等待时间。

减少排序时的时间格式化操作,用array_multisort 等更高效的排序函数代替usort 这种相对通用但在大数据量下效率不算特别高的排序方式。

设置合理的重试次数、采用指数退避等重试间隔策略,增强在网络不稳定等情况下获取 RSS 内容的成功率。

注意:这个要求PHP版本大于7.4,并且安装redis、swoole或者swoole5扩展。另外 // 头部图片 是我的博客自改的,通用版用老代码中 // 主内容区域 那段代替即可。

<?php
/*
Template Name: RSS 朋友圈
*/

date_default_timezone_set('Asia/Shanghai');
get_header();

// 引入 SimplePie 类
require_once(ABSPATH. WPINC. '/class-simplepie.php');

// 头部图片
echo '<div id="header_slider" 	 class=" fadeInDown animated ">';
echo '<div class="slider"><ul class="lightSlider"><a href="https://pwsz.com/hobby/4493.html#lqmxp"target="_black" title="送你一张明信片"><img width="759" height="289" src="https://pwsz.com/myimg/pydq_banner.jpg" class="attachment-large-image size-large-image wp-post-image" alt="" decoding="async" fetchpriority="high" /></a></ul></div>';

// 获取并缓存友情链接的最新文章数据
function get_cached_friend_link_rss_data() {
    $cache_key = 'friend_link_rss_data';
    $last_update_time_key = 'last_update_time_'.$cache_key;
    $last_update_time = wp_cache_get($last_update_time_key);
    if ($last_update_time && (time() - $last_update_time < 7 * HOUR_IN_SECONDS)) {
        return wp_cache_get($cache_key);
    }

    $all_articles = array();
    $links = get_bookmarks(array(
        'orderby' => 'name',
        'category' => 0,
        'order' => 'ASC',
        'hide_invisible' => 1,
        'categorize' => 0,
        'title_li' => '',
        'echo' => 0
    ));

    $feed_urls = [];
    foreach ($links as $link) {
        if ($link->link_rss &&!empty($link->link_rss)) {
            $domain = parse_url($link->link_url, PHP_URL_HOST);
            if (!$domain) {
                continue;
            }

            $feed_urls[] = $link->link_rss;
        }
    }

    // 利用Swoole协程并发获取RSS内容
    $rss_contents = get_rss_content_concurrently($feed_urls);
    $index = 0;
    foreach ($links as $link) {
        if ($link->link_rss &&!empty($link->link_rss)) {
            $domain = parse_url($link->link_url, PHP_URL_HOST);
            if (!$domain) {
                continue;
            }

            $feed_url = $link->link_rss;
            $cache_file = get_stylesheet_directory(). '/rsscache/'. $domain. '.xml';

            // 获取到的对应RSS内容
            $rss_content = $rss_contents[$index];
            $index++;

            // 如果缓存文件不存在或超过 8 小时,且 RSS 内容有更新,才更新缓存
            if (!file_exists($cache_file) || time() - filemtime($cache_file) > 8 * HOUR_IN_SECONDS) {
                if ($rss_content) {
                    $existing_content = file_exists($cache_file)? file_get_contents($cache_file) : false;
                    if ($existing_content!== $rss_content) {
                        file_put_contents($cache_file, $rss_content);
                    }
                }
            }

            $rss_items = get_rss_items($cache_file);
            foreach ($rss_items as $item) {
                $item_data = array(
                    'link_name' => $link->link_name,
                    'link_logo' => $link->link_image,
                    'link_url' => $link->link_url,
                    'item_title' => $item->get_title(),
                    'item_link' => $item->get_permalink(),
                    'item_date' => $item->get_date('Y-m-d H:i:s'),
                    'formatted_date' => date('Y-m-d', strtotime($item->get_date('Y-m-d H:i:s'))),
                    'item_author' => $item->get_author()->get_name(),
                    'item_excerpt' => $item->get_description(),
                    'item_content' => $item->get_content()
                );
                $all_articles[$link->link_name][] = $item_data;
            }
        }
    }

    // 将数据存入对象缓存,并设置本次更新时间缓存
    wp_cache_set($cache_key, $all_articles, null, 8 * HOUR_IN_SECONDS);
    wp_cache_set($last_update_time_key, time());
    return $all_articles;
}

// 利用Swoole协程并发获取RSS内容的函数,完善错误处理和重试机制
function get_rss_content_concurrently($feed_urls) {
    $results = [];
    // 拆分配置项,明确各个参数对应的值
    $host = '127.0.0.1';
    $port = 80;
    $timeout = 1.0;  // 设置连接超时时间,单位秒,可根据实际调整
    $write_timeout = 1.0; // 设置写超时时间,单位秒,可根据实际调整
    $ssl = false; // 这里先设置为不使用SSL连接,可根据实际需求修改为true,如果需要SSL加密的话

    $client = new Swoole\Coroutine\Http\Client($host, $port, $ssl);
    $client->set([
        'timeout' => $timeout,
        'write_timeout' => $write_timeout,
    ]);

    if (php_sapi_name() === 'cli') {
        // 如果是PHP CLI模式,执行协程相关代码
        $max_retries = 3; // 设置最大重试次数
        $retry_delay = 1; // 初始重试延迟时间(秒)

        foreach ($feed_urls as $index => $feed_url) {
            $retry_count = 0;
            do {
                $response = null;
                go(function () use ($index, $feed_url, &$response, $client) {
                    $response = $client->get($feed_url);
                });
                while ($response === null) {
                    Swoole\Coroutine::sleep(0.1);
                }
                if ($response->statusCode === 200) {
                    $results[$index] = $response->body;
                    break; // 获取成功则跳出重试循环
                } else {
                    $retry_count++;
                    if ($retry_count < $max_retries) {
                        // 指数退避增加重试延迟时间
                        $retry_delay *= 2;
                        Swoole\Coroutine::sleep($retry_delay);
                    } else {
                        $results[$index] = '无法从提供 RSS 的 URL 获取 RSS 内容。';
                        break;
                    }
                }
            } while ($retry_count < $max_retries);

        }
        $client->close();
    } else {
        // 如果是非CLI模式(比如Web服务器环境等),采用同步方式获取RSS内容,可参考原代码中的同步获取逻辑进行完善
        foreach ($feed_urls as $feed_url) {
            $response = wp_safe_remote_get($feed_url, array('sslverify' => false));
            if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200) {
                $results[] = wp_remote_retrieve_body($response);
            } else {
                $results[] = '无法从提供 RSS 的 URL 获取 RSS 内容。';
            }
        }
    }

    return $results;
}

// 获取最新的友情链接文章,改进排序算法
function get_latest_friend_link_articles() {
    $latest_articles = get_cached_friend_link_rss_data();
    $all_articles = [];
    foreach ($latest_articles as $link_name => $articles) {
        foreach ($articles as $article) {
            $all_articles[] = $article;
        }
    }

    // 先按照格式化日期排序(整体文章排序)
    $formatted_date_column = array_column($all_articles, 'formatted_date');
    $link_name_column = array_column($all_articles, 'link_name');
    array_multisort($formatted_date_column, SORT_DESC, $link_name_column, SORT_ASC, $all_articles);

    $sorted_articles = [];
    foreach ($all_articles as $article) {
        $sorted_articles[$article['link_name']][] = $article;
    }

    foreach ($sorted_articles as $link_name => $articles) {
        // 对每个友情链接下的文章按照原始日期再排序,取前五篇
        $date_column = array_column($articles, 'item_date');
        array_multisort($date_column, SORT_DESC, $articles);
        $sorted_articles[$link_name] = array_slice($articles, 0, 5);
    }

    return $sorted_articles;
}

// 获取并保存最新的 RSS 内容
function get_rss_content($feed_url) {
    $response = wp_safe_remote_get($feed_url, array('sslverify' => true));
    if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200) {
        return wp_remote_retrieve_body($response);
    } else {
        $response = wp_safe_remote_get($feed_url, array('sslverify' => false));
        if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200) {
            return wp_remote_retrieve_body($response);
        } else {
            return '无法从提供 RSS 的 URL 获取 RSS 内容。';
        }
    }
}

// 使用SimplePie获取RSS项
function get_rss_items($cache_file) {
    $rss = new SimplePie();
    $rss->set_feed_url($cache_file);
    $rss->init();
    $rss->handle_content_type();
    $rss->set_output_encoding('UTF-8');
    return $rss->get_items();
}

// 获取并显示友情链接的最新文章
$latest_articles = get_latest_friend_link_articles();
foreach ($latest_articles as $link_name => $articles) {
  ?>
    <div class="friend-link-container" style="border: 1px dashed #000; padding: 10px; margin-bottom: 10px; display: flex; align-items: center;">
        <!-- 左侧显示友情链接的网站 Logo 和名称 -->
        <div class="friend-link-info" style="width: 20%; text-align: center;">
            <img src="<?php echo esc_url($articles[0]['link_logo']);?>" alt="<?php echo esc_attr($link_name);?> Logo" style="border-radius: 50%; width: 60px; height: 60px;">
            <a href="<?php echo esc_url($articles[0]['link_url']);?>" target="_blank"><p style="margin: 0; padding: 0;"><?php echo esc_html($link_name);?></p></a>
        </div>
        <!-- 右侧显示最新更新的前五篇文章 -->
        <div class="friend-link-articles" style="width: 80%; padding-left: 10px;">
            <?php foreach ($articles as $article) :?>
                <p style="margin: 0; padding: 0;">
                    <a href="<?php echo esc_url($article['item_link']);?>" target="_blank"><?php echo esc_html($article['item_title']);?></a>(<?php echo esc_html($article['formatted_date']);?>)
                </p>
            <?php endforeach;?>
        </div>
    </div>
    <?php
}

// 侧边栏
get_sidebar();

echo '</main><!-- #main -->';
echo '</div><!-- #primary -->';

get_footer();
?>

还有一种也很清爽的朋友圈代码,一并分享了:

这是从【登山亦有道】兄弟那拿来的,子比主题新增一个友圈动态页面,代码放下面,几个代码都可以使用,大家自己随意拾取。

<?php
/*
Template Name: RSS 朋友圈
*/

date_default_timezone_set('Asia/Shanghai');
get_header();
require_once(ABSPATH . WPINC . '/class-simplepie.php');

// 获取缓存的数据
$data = get_transient('rss_circle_final_style');
$last_updated = '未更新'; // 默认显示未更新

// 强制清除缓存
if ($data === false) {
    // 触发缓存重新抓取
    $data = get_transient('rss_circle_final_style');
    if ($data === false) {
        $data = fetch_all_rss_items($rss_sites); // 强制重新抓取
    }
}

if ($data !== false) {
    // 获取上次缓存的时间戳
    $last_updated_timestamp = get_option('rss_circle_last_updated', 0);
    if ($last_updated_timestamp > 0) {
        // 计算距离当前时间的差异
        $time_diff = time() - $last_updated_timestamp;
        if ($time_diff < 3600) {
            $last_updated = floor($time_diff / 60) . ' 分钟前';
        } elseif ($time_diff < 86400) {
            $last_updated = floor($time_diff / 3600) . ' 小时前';
        } else {
            $last_updated = floor($time_diff / 86400) . ' 天前';
        }
    }
} else {
    // 如果缓存不存在,则显示默认文本
    $last_updated = '从未更新';
}

echo '<p style="color: red; font-size: 18px; margin: 10px 0 10px 0; text-align: center;">以下友情链接网站最新内容每 2 小时获取更新一次 (' . $last_updated . ')</p>';

$transient_key = 'rss_circle_final_style';
$cache_duration = 7200; // 缓存 2 小时

function fetch_all_rss_items($rss_sites, $timeout = 20)
{
    $mh = curl_multi_init();
    $chs = [];
    $results = [];

    foreach ($rss_sites as $i => $site) {
        if (isset($site->link_rss) && trim($site->link_rss) !== '') {
            $rss_url = $site->link_rss;
        } else {
            $rss_url = detect_rss_url($site->link_url);
        }

        if (!$rss_url) {
            echo "<!-- 未找到 RSS 地址: {$site->link_name} -->";
            error_log("未找到 RSS 地址: {$site->link_name}");
            continue;
        }

        $ch = curl_init();
        curl_setopt_array($ch, [
            CURLOPT_URL => $rss_url,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_FOLLOWLOCATION => true,
            CURLOPT_TIMEOUT => $timeout,
            CURLOPT_CONNECTTIMEOUT => 5,
            CURLOPT_SSL_VERIFYPEER => false,
            CURLOPT_USERAGENT => 'Mozilla/5.0',
        ]);
        curl_multi_add_handle($mh, $ch);
        $chs[$i] = $ch;
    }

    $running = null;
    do {
        curl_multi_exec($mh, $running);
        curl_multi_select($mh);
    } while ($running > 0);

    foreach ($chs as $i => $ch) {
        $body = curl_multi_getcontent($ch);
        curl_multi_remove_handle($mh, $ch);
        curl_close($ch);

        if (strlen(trim($body)) < 100) continue;

        $feed = new SimplePie();
        $feed->set_stupidly_fast(true);
        $feed->set_raw_data(ltrim(preg_replace('/^\xEF\xBB\xBF/', '', $body)));
        $feed->set_useragent('Mozilla/5.0');
        $feed->enable_cache(false);
        $feed->init();

        if (!$feed->error()) {
            $items = $feed->get_items(0, 6);

            // 检查是否存在至少一个非空标题
            $has_valid_title = false;
            foreach ($items as $item) {
                if (trim($item->get_title()) !== '') {
                    $has_valid_title = true;
                    break;
                }
            }

            if (!$has_valid_title) {
                echo "<!-- 所有条目标题为空,跳过: {$rss_sites[$i]->link_name} -->";
                error_log("所有条目标题为空,跳过: {$rss_sites[$i]->link_name}");
                continue;
            }

            foreach ($items as $item) {
                $results[] = (object)[
                    'title' => strip_tags(html_entity_decode((string)$item->get_title())),
                    'link' => $item->get_link(),
                    'date' => $item->get_date('U'),
                    'source_name' => $rss_sites[$i]->link_name,
                    'source_link' => $rss_sites[$i]->link_url,
                    'source_avatar' => $rss_sites[$i]->link_image,
                ];
            }
        }
    }

    curl_multi_close($mh);
    return $results;
}


// 自动探测 RSS 地址(优先尝试直接 URL 和 /feed)
function detect_rss_url($site_url)
{
    // 先尝试 /rss
    $rss1 = rtrim($site_url, '/') . '/rss';
    if (is_rss_feed($rss1)) {
        // echo "<!-- 探测RSS1成功: $rss1 -->";
        return $rss1;
    }

    // 再尝试 /feed(保底)
    $rss2 = rtrim($site_url, '/') . '/feed';
    if (is_rss_feed($rss2)) {
        // echo "<!-- 探测RSS2成功: $rss2 -->";
        return $rss2;
    }

    echo "<!-- 探测失败: $site_url -->";
    return false;
}



// 检查一个 URL 是否有效的 RSS feed
function is_rss_feed($url)
{
    // 初始化curl
    $ch = curl_init();
    curl_setopt_array($ch, [
        CURLOPT_URL => $url,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_FOLLOWLOCATION => true,
        CURLOPT_TIMEOUT => 10,
        CURLOPT_CONNECTTIMEOUT => 5,
        CURLOPT_SSL_VERIFYPEER => false,
        CURLOPT_USERAGENT => 'Mozilla/5.0',
    ]);

    $content = curl_exec($ch);
    $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    // 请求失败或者状态码非200
    if ($http_code !== 200 || !$content) {
        return false;
    }

    // 用SimplePie解析内容前先去BOM防止乱码
    $content = ltrim(preg_replace('/^\xEF\xBB\xBF/', '', $content));

    $feed = new SimplePie();
    $feed->set_raw_data($content);
    $feed->set_useragent('Mozilla/5.0');
    $feed->enable_cache(false);
    $feed->init();

    // 解析无错,且至少有一条条目即判断为有效RSS
    return (!$feed->error() && $feed->get_item_quantity() > 0);
}


$data = get_transient($transient_key);
if ($data === false) {
    global $wpdb;
    $rss_sites = $wpdb->get_results("SELECT * FROM {$wpdb->prefix}links WHERE link_visible = 'Y'");
    $data = fetch_all_rss_items($rss_sites);
    usort($data, fn($a, $b) => $b->date <=> $a->date);
    set_transient($transient_key, $data, $cache_duration);

    // 更新上次更新时间
    update_option('rss_circle_last_updated', time());
}
?>


<style>
    .rss-grid {
        display: flex;
        flex-direction: column;
        align-items: center;
        gap: 10px;
        padding: 0 10px;
        box-sizing: border-box;
        width: 100%;
    }

    .rss-block {
        width: 100%;
        max-width: 1050px;
        border: 1px solid #ddd;
        padding: 20px;
        background: #fafafa;
        border-radius: 8px;
        display: flex;
        gap: 20px;
        box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
        font-size: 16px;
        box-sizing: border-box;
    }

    .rss-left {
        width: 100px;
        text-align: center;
        flex-shrink: 0;
        display: flex;
        flex-direction: column;
        justify-content: center;
    }


    .rss-left img {
        width: 80px;
        height: 80px;
        object-fit: cover;
        border-radius: 50%;
        display: block;
        margin: 0 auto 10px;
        background-color: #f0f0f0;
    }

    .rss-placeholder {
        width: 80px;
        height: 80px;
        line-height: 80px;
        border-radius: 50%;
        background: #eee;
        color: #999;
        font-size: 14px;
        margin: 0 auto 10px;
    }

    .rss-name a {
        display: block;
        text-align: center;
        font-weight: 600;
        font-size: 17px;
        color: #222;
        text-decoration: none;
        margin-top: 5px;
    }

    .rss-name a:hover {
        text-decoration: underline;
    }

    .rss-right {
        flex: 1;
    }

    .rss-posts {
        list-style: none;
        padding-left: 0;
        margin: 0;
    }

    .rss-posts li {
        margin-bottom: 10px;
        font-size: 15px;
    }

    .rss-posts a {
        text-decoration: none;
        color: #222;
    }

    .rss-posts a:hover {
        text-decoration: underline;
    }

    .rss-date {
        color: #999;
        font-size: 0.85em;
        margin-left: 6px;
    }

    /* 移动端适配 */
    @media (max-width: 768px) {
        .rss-block {
            flex-direction: column;
            padding: 15px;
        }

        .rss-left {
            width: 100%;
            text-align: center;
            margin-bottom: 10px;
        }

        .rss-left img,
        .rss-placeholder {
            width: 60px;
            height: 60px;
            line-height: 60px;
            margin-top: 10px;
        }

        .rss-name a {
            font-size: 16px;
        }

        .rss-posts li {
            font-size: 14px;
        }
    }
</style>

<?php
if (empty($data)) {
    echo '<p style="padding:20px; text-align:center; color:#999;">暂无RSS数据,请稍后刷新或稍候再试。</p>';
} else {
    // 分组
    $grouped = [];
    foreach ($data as $entry) {
        $grouped[$entry->source_link]['info'] = [
            'name' => $entry->source_name,
            'link' => $entry->source_link,
            'avatar' => $entry->source_avatar,
        ];
        $grouped[$entry->source_link]['items'][] = $entry;
    }

    echo '<div class="rss-grid">';
    foreach ($grouped as $source) {
        echo '<div class="rss-block">';
        echo '<div class="rss-left">';
        if (!empty($source['info']['avatar'])) {
            echo '<img src="' . esc_url($source['info']['avatar']) . '" alt="avatar">';
        } else {
            echo '<div class="rss-placeholder">No Avatar</div>';
        }
        echo '<div class="rss-name"><a href="' . esc_url($source['info']['link']) . '" target="_blank">' . esc_html($source['info']['name']) . '</a></div>';
        echo '</div>';
        echo '<div class="rss-right"><ul class="rss-posts">';
        foreach ($source['items'] as $item) {
            echo '<li>';
            echo '<a href="' . esc_url($item->link) . '" target="_blank">' . esc_html($item->title) . '</a>';
            echo '<span class="rss-date">(' . date('Y-m-d', $item->date) . ')</span>';
            echo '</li>';
        }
        echo '</ul></div>';
        echo '</div>';
    }
    echo '</div>';
}

get_footer();
?>