分享一下本站“朋友圈”的制作过程

本来我很少会写建站相关的博文,因为这些在网上一翻一大堆,写了也是重复造轮子。

不过昨天有个博主问到了本站的“朋友圈”是如何实现的,正巧这个“朋友圈”的创建并非是完全参考某一个教程搭建的,中间也有不少我的魔改,所以就记录一下。

PS1:本站“朋友圈”其实是对订阅的其他网站的RSS的内容的一个集合展示。

PS2:实现原理:

  1. 部署 FreshRSS 用于订阅其他博主/网站的 RSS 订阅源;
  2. 使用宝塔做了两个定时任务:
    1. 一个定时任务让 FreshRSS 刷新订阅源的最新内容;
    2. 另一个定时任务是将最新的前 n 条内容覆写到一个指定的 json 文件中。
  3. 在网站后台(WordPress)魔改了说说模块文件,其目的是读取的这个 json 内容并展示;
  4. 最后就形成了我现在的 “朋友圈” 的效果。

PS3:博主不会php语言,所以下面的魔改部分都是我摸索着配出来的,本着能用就行的态度写的,所以会有很多写的不对的地方,多多包涵,欢迎指正。

一、配置清单

下面是我的配置清单,我只能保证最终效果在本配置下生效。

  • 服务器系统: CentOS 7.9
  • 博客平台: WordPress 6.8.0及之后
  • 主题模板: Argon 1.3.5
  • 配置托管平台: 宝塔Linux面板(没有配置托管平台的,其过程也可以手搓)
  • RSS订阅工具: Fresh RSS

二、安装配置FreshRSS

点击跳转☞:【FreshRSS官网】

点击跳转☞:【FreshRSS官方安装文档】

点击跳转☞:【FreshRSS官方下载页面(发文时版本)】

2.1 下载FreshRSS

打开下载页面,拉到最下面,根据自己的需要下载zip或者tar.gz版本。

2.2 部署FreshRSS

  1. 将下载的压缩包上传到centos的某目录下并解压;
  2. 然后使用宝塔,点击“网站” > “添加站点”;
    1. “域名”:可以提前对主域名进行解析出一个二级域名,可以加SSL证书做成HTTP的;
    2. “根目录”:选择刚才解压的目录;
    3. 其他的默认即可。
  3. 创建好站点之后,点击站点后的“配置”,你可以根据需要进行后续配置,比如配置SSL证书;
  4. 默认使用80和443端口,记得开启防火墙,做好二级域名的DNS解析。这样的话,你就可以通过前面的二级域名访问到自己搭建的FreshRSS网站了。

2.3 添加RSS订阅源

根据需要添加“分类”,或者添加“订阅源”,此处的订阅源就是其他站点的RSS地址,订阅源的可选择分类就是上面自己添加的分类。

添加完订阅源之后,返回首页刷新就会加载最新的订阅源的内容。

到这一步,你就成功的能在FreshRSS看到其他站点的文章了。

但是FreshRSS有一个问题,就是不能主动刷新订阅源,需要我们手动点击页面的【⟳】按钮才能刷新。

我们要的是定时自动刷新,所以我们还需要做相关配置,详情见下面第六节。

2.4 配置FreshRSS

  1. 打开你的FreshRSS的域名,并进行登陆。

  2. 点击 设置 > 管理 > 认证;

    1. 勾选“允许 API 访问”并提交。
  3. 点击 设置 > 账户 > API管理;

    1. 设置密码并提交保存,记住设置的api密码。
    2. 复制网址,将在下一步配置文件中用于${网址1}

  4. 在自己站点(WordPress)根目录下创建一个php文件,用于放FreshRSS api调用函数,例如:rss.php。
    例如:

    • 我的博客网站根目录为:/www/wwwroot/www.tqazy.com
    • 我的FreshRSS根目录为:/www/wwwroot/rss.tqazy.com
      那么我就在 /www/wwwroot/www.tqazy.com 下创建创建一个文件:rss.php,内容如下:
<?php
/**
 * 获取最新订阅文章并生成JSON文件
 */
function getAllSubscribedArticlesAndSaveToJson($user, $password)
{
$apiUrl = '${网址1}';
    $loginUrl = $apiUrl . '/accounts/ClientLogin?Email=' . urlencode($user) . '&Passwd=' . urlencode($password);
    $loginResponse = curlRequest($loginUrl);

    // 处理可能的cURL错误
    if (isset($loginResponse['error'])) {
        die('登录请求失败: ' . $loginResponse['error']);
    }

    if (strpos($loginResponse, 'Auth=') !== false) {
        $authToken = substr($loginResponse, strpos($loginResponse, 'Auth=') + 5);
        $articlesUrl = $apiUrl . '/reader/api/0/stream/contents/reading-list?&n=1000';
        $articlesResponse = curlRequest($articlesUrl, $authToken);

        // 处理可能的cURL错误
        if (isset($articlesResponse['error'])) {
            die('获取文章失败: ' . $articlesResponse['error']);
        }

        $articles = json_decode($articlesResponse, true);
        if (isset($articles['items'])) {
            usort($articles['items'], function ($a, $b) {
                return $b['published'] - $a['published'];
            });
            $subscriptionsUrl = $apiUrl . '/reader/api/0/subscription/list?output=json';
            $subscriptionsResponse = curlRequest($subscriptionsUrl, $authToken);

            // 处理可能的cURL错误
            if (isset($subscriptionsResponse['error'])) {
                die('获取订阅失败: ' . $subscriptionsResponse['error']);
            }

            $subscriptions = json_decode($subscriptionsResponse, true);
            if (isset($subscriptions['subscriptions'])) {
                $subscriptionMap = array();
                foreach ($subscriptions['subscriptions'] as $subscription) {
                    $subscriptionMap[$subscription['id']] = $subscription;
                }
                $formattedArticles = array();
                foreach ($articles['items'] as $article) {
                    // 去掉以 http 或 https 开头的链接
                    $contentWithoutLinks = preg_replace('/https?:\/\/[^\s?#]*?\.(jpg|jpeg|png|gif|mp4|mp3|webm|ogg|wav|flac|svg|bmp)\b/i', '', $article['summary']['content']);
                    $desc_length = mb_strlen(strip_tags(html_entity_decode($contentWithoutLinks, ENT_QUOTES, 'UTF-8')), 'UTF-8');
                    if ($desc_length > 20) {
                        $short_desc = mb_substr(strip_tags(html_entity_decode($contentWithoutLinks, ENT_QUOTES, 'UTF-8')), 0, 99, 'UTF-8') . '...';
                    } else {
                        $short_desc = strip_tags(html_entity_decode($contentWithoutLinks, ENT_QUOTES, 'UTF-8'));
                    }

                    $formattedArticle = array(
                        'site_name' => $article['origin']['title'],
                        'title' => $article['title'],
                        'link' => $article['alternate'][0]['href'],
                        'time' => date('Y-m-d H:i', $article['published']),
                        'description' => $short_desc,
                    );

                    $subscriptionId = $article['origin']['streamId'];
                    if (isset($subscriptionMap[$subscriptionId])) {
                        $subscription = $subscriptionMap[$subscriptionId];
                        $iconUrl = $subscription['iconUrl'];
                        $filename = '${网址2}/' . substr($iconUrl, strrpos($iconUrl, '/') + 1);
                        $formattedArticle['icon'] = $iconUrl;
                    }

                    $formattedArticles[] = $formattedArticle;
                }

                saveToJsonFile($formattedArticles);
                return $formattedArticles;
            } else {
                die('Error retrieving subscriptions.');
            }
        } else {
            die('Error retrieving articles.');
        }
    } else {
        die('Login failed: ' . $loginResponse);
    }
    return null;
}

function curlRequest($url, $authToken = null)
{
    $ch = curl_init($url);
    if ($ch === false) {
        return ['error' => 'cURL初始化失败'];
    }

    $headers = [];
    if ($authToken) {
        $headers[] = 'Authorization: GoogleLogin auth=' . $authToken;
    }

    curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_FAILONERROR, true); // 捕获HTTP错误

    $response = curl_exec($ch);
    if ($response === false) {
        $error = curl_error($ch);
        curl_close($ch);
        return ['error' => $error];
    }

    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    if ($httpCode >= 400) {
        return ['error' => "HTTP请求失败,状态码:$httpCode"];
    }

    return $response;
}

/**
 * 将数据保存到JSON文件中
 */
function saveToJsonFile($data)
{
    $json = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
    file_put_contents('friends_rss.json', $json);
    echo '数据已保存到JSON文件中';
}

// 调用函数并提供用户名和密码
getAllSubscribedArticlesAndSaveToJson('${账户}', '${密码}');

代码中的替换项:

  • ${网址1}:上一步复制的网址;
  • ${网址2}:替换成你FreshRSS的域名地址,比如:https://rss.xxx.com
  • ${账户}:FreshRSS 的账户
  • ${密码}:这里是第3步设置的api密码

调用这个rss.php文件,就能从FreshRSS中把最新的订阅文章复写到一个叫friends_rss.json的文件中。

三、配置“朋友圈”模板文件

我们拿到了需要展示的数据,那么我们就要有一个展示的模板。

我的朋友圈设计是仅展示订阅文章的“文章名”、“发布时间”、“发布网站”、“文章前n个字符”等,并不会显示全文,如图:

这个模板其实是由WordPress的说说魔改的,去掉了翻页(仅展示前n条最新文章)、去掉了评论、去掉了点赞、新增了作者栏、修改了标题名自动加书名号、修改了内容超出部分自动加…等。

在目录/www/wwwroot/www.tqazy.com/wp-content/themes/argon-theme-master下创建文件:friend_rss.php。
内容如下:

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

<?php get_header(); ?>

<div class="page-information-card-container">
    <div class="page-information-card card bg-gradient-secondary shadow-lg border-0">
        <div class="card-body">
            <h3 class="text-black"><?php _e('${页面名}', 'argon'); ?></h3>
            <?php if (the_archive_description() != '') { ?>
                <p class="text-black mt-3">
                    <?php the_archive_description(); ?>
                </p>
            <?php } ?>
            <p class="text-black mt-3 mb-0 opacity-8">
                <i class="fa fa-quote-left mr-1"></i>
                <?php _e('${页面提示语}', 'argon'); ?>
            </p>
        </div>
    </div>
</div>

<?php get_sidebar(); ?>

<div id="primary" class="content-area">
    <main id="main" class="site-main" role="main">
        <?php

    // 定义自定义模板标签函数
    function the_site_name() {
        global $post;
        echo esc_html($post->post_site_name);
    }

    function the_icon() {
        global $post;
        echo esc_url($post->post_icon);
    }

        // 获取JSON数据
        $jsonData = file_get_contents('${json所在目录地址}');
        // 检查文件读取是否成功
        if ($jsonData!== false) {
            // 将JSON数据解析为PHP数组
            $articles = json_decode($jsonData, true);
            if ($articles!== null) {
                // 对文章按时间排序(最新的排在前面)
                usort($articles, function ($a, $b) {
                    return strtotime($b['time']) - strtotime($a['time']);
                });
                // 设置每页显示的文章数量
                $itemsPerPage = ${文章数量};
                // 生成文章列表
                $displayArticles = array_slice($articles, 0, $itemsPerPage);
                if (!empty($displayArticles)) {
                    global $post;
                    foreach ($displayArticles as $article) {
                        // 模拟WordPress文章对象
                      // 将 $article['time'] 转换为 MySQL 时间戳格式
                      $formatted_time = date('Y-m-d H:i:s', strtotime($article['time']));
                      $new_article = "  作者:" . $article['site_name'];

                        $post = (object) [
                            'ID' => $article['id']?? uniqid(), // 如果JSON中没有id字段,使用唯一ID
                            'post_title' => "《" . $article['title'] . "》",
                            'post_date' => $formatted_time,
                            'post_content' => $article['description'],
                            'guid' => $article['link'],
                            'post_permalink' => $article['link'],
                            'post_site_name' => $new_article,
                            'post_icon' => $article['icon'],                         

                        ];
                        setup_postdata($post); // 设置当前文章数据

                        get_template_part('template-parts/content', 'friend');

                        wp_reset_postdata(); // 重置文章数据
                    }
                } else {
                    get_template_part('template-parts/content', 'none-tag');
                }
            } else {
                get_template_part('template-parts/content', 'none-tag');
            }
        } else {
            get_template_part('template-parts/content', 'none-tag');
        }
        ?>

<?php get_footer(); ?>

替换项:

  • ${页面名}:页面名称,比如:朋友圈;

  • ${页面提示语}:在页面名称下面显示的提示语,比如:这里是已交换友链的网站发布的内容,通过RSS方式订阅,每1小时更新一次内容。

  • ${json所在目录地址}:前面rss.php生成的json所在地址,比如:/www/wwwroot/www.tqazy.com/friends_rss.json

  • ${文章数量}:即你要展示的文章数量,比如:38。可以写50及以内,如果经常看朋友圈的话,设置展示太多了没意义。

四、配置“朋友圈”配套文件

在目录/www/wwwroot/www.tqazy.com/wp-content/themes/argon-theme-master/template-parts下创建文件:content-friend.php,上面的文件用得到,属于样式配置。

内容如下,这个文件没有替换项:

<div class="shuoshuo-container">
    <div class="shuoshuo-meta shadow-sm">
        <span>

            <i class="fa fa-calendar-o" aria-hidden="true"></i> 
             <span class="shuoshuo-date-month"><?php echo get_the_time('n')?></span> <?php _e('月', 'argon');?> 
            <span class="shuoshuo-date-date"><?php echo get_the_time('d')?></span> <?php _e('日', 'argon');?> , 
            <span class="shuoshuo-date-year"><?php echo get_the_time('Y')?></span>
            <div class="post-meta-devide">|</div>
            <i class="fa fa-clock-o" aria-hidden="true"></i> 
            <span class="shuoshuo-date-time"><?php echo get_the_time('G:i:s')?></span>
        </span>
        <?php if ( is_sticky() ) : ?>
            <div class="post-meta-devide">|</div>
            <div class="post-meta-detail post-meta-detail-words">
                <i class="fa fa-thumb-tack" aria-hidden="true"></i>
                <?php _ex('置顶', 'pinned', 'argon');?>
            </div>
        <?php endif; ?>
    </div>
    <article class="card shuoshuo-main shuoshuo-foldable bg-white shadow-sm border-0" id="post-<?php the_ID(); ?>" <?php post_class(); ?>>
        <?php if ( get_the_title() != '' ) : ?>
            <a class="shuoshuo-title" href="<?php the_guid(); ?>" target="_black"><?php the_title(); ?></a>
            <font style="font-weight:900;"><?php the_site_name(); ?></font>
        <?php endif; ?>

        <div class="shuoshuo-content">
            <?php the_content(); ?>
        </div>
        <?php
            global $withcomments;
            $withcomments = true;
//          comments_template( '/comments-shuoshuo-preview.php' );
        ?>
    </article>
</div>

此时,“朋友圈”的模板部分就编写完毕了。

在网站后台可以看到:外观 > 主题文件编辑器 > friend_rss.php

五、展示“朋友圈”页面

第一步:博客后台 > 页面 > 添加页面:

  1. 页面名称:朋友圈
  2. 正文不用填写任何内容
  3. 设置右边的页面属性:
    1. 父级:无
    2. 模板:朋友圈
    3. 发布

第二步:博客后台 > 外观 > 菜单 > 选择要编辑的菜单,然后在左侧的页面里找到“朋友圈”,添加到菜单,至于剩下的可以自行配置了。

比如我的朋友圈就是放在“友链”菜单下的:

到此,“朋友圈”功能已经成功的添加到了你的网站上了。

六、自动刷新朋友圈

最重要的一步来了,我们要想自动刷新朋友圈,而不是每次都去调用rss.php文件,那么我们就要做定时任务。

我也不知道为什么我单调rss.php文件是不能刷新FreshRSS的订阅源的,所以我做了两个定时任务。

我是使用宝塔的,如果您没有宝塔,可以通过写脚本方式实现。

打开宝塔面板 > 计划任务:

6.1 第一个定时任务

  1. 添加任务;
  2. 选择:Shell脚本;
  3. 任务名称:定时刷新FreshRSS;
  4. 执行周期:每小时 1分钟;
  5. 脚本内容:php /www/wwwroot/rss.tqazy.com/app/actualize_script.php > /tmp/FreshRSS.log 2>&1
  6. 确定

需要把这里的/www/wwwroot/rss.tqazy.com替换成你的rss解压文件所在的目录地址。

6.2 第二个定时任务

  1. 添加任务;
  2. 选择:访问URL-GET;
  3. 任务名称:朋友圈订阅数据更新;
  4. 执行周期:每小时 5分钟;
  5. URL地址:https://xxx.com/xxx/xxx/rss.php
  6. 确定

需要把这里的https://xxx.com/xxx/xxx/rss.php换成你的博客网站能访问到rss.php的域名地址。

你可以执行一下,看看日志,是否成功的写入了。

至此,朋友圈的功能全部完成。

效果:
该功能会将在FreshRSS中订阅的订阅源,以每小时1次的频率自动刷新订阅源并读取到friends_rss.json中,然后模板通过读取friends_rss.json文件将最新的n条文章展示到“朋友圈”的页面中。

注意:每次您的网站新增友链时,需要将对方的rss地址手动的添加到你的FreshRSS中。

版权声明:本文《分享一下本站“朋友圈”的制作过程》是由陶其原创撰写,首发于陶其的个人博客
转载声明:如需转载本文,请务必在转载处保留原文链接:https://www.tqazy.com/?p=1821,并明确注明文章来源。

评论

  1. Android Chrome
    1 天前
    2025-7-30 21:52:06

    感谢博主,不过博主的代码都不太健壮,比如rss.php文件,如果我的网站配置了ssl证书,强制转换为https,这个php文件好像不太兼容这种情况,我加载起来是有问题的,我用ai改了一下代码(俺不会php,也不知道ai怎么改的,哈哈哈),然后又让ai改了其它代码,功能就正常了。另外定时可以直接修改php加上定时功能,也是ai的,哈哈哈哈,好长一段,不过能跑,或者用第三方定时任务平台

    • Avatar photo
      博主
      George
      Windows Chrome
      18 小时前
      2025-7-31 8:31:55

      对的,代码里应该是有很多不合理的地方,主要是我也不会php,这些代码还都是早期的ai给我生成的,那时候的ai没有现在的这么智能,而且还无法一次性生成我想要的,所以经过ai多次反复的修改,就留下了很多暗疾。不过在我这里能成功跑起来之后,我就没再去动它了,哈哈。

发送评论 编辑评论

|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇