Astro引入FreshRss订阅的方法

最近,我开始使用Astro,在构建links页面的时候,发现如果每条信息都去添加,实在麻烦,就琢磨怎么才可以省心省力。刚巧,逛1900博客的时候,发现他的liks页面就采用了 关于友情链接和SSG在博客中展示Flux订阅 这种方式来实现。

图片来源于1900博客截图@bosir

不过,阅读完文章后,发现他的实现方式比较有难度,而且我对于miniflux、11ty这些都没有上手经验,所以我就想着实现的思路既然一样,我可不可以换一个程序就能够实现呢?而我一直使用的FreshRss,正好可以满足我的需求。那么我的思路也就很明确了,Docker安装Freshrss实现后端服务,Astro进行前端展示,我只需要引入FreshRss的RSS订阅信息,就可以实现我的需求了。

Docker安装Freshrss实现后端服务

我在Typecho就有使用过Freshrss,所以安装起来比较简单,直接使用Docker安装即可。而且现在宝塔面板自带Freshrss,安装起来更加简单。

docker run -d --restart unless-stopped --log-opt max-size=10m \
  -p 8080:80 \
  -e TZ=Europe/Paris \
  -e 'CRON_MIN=1,31' \
  -v freshrss_data:/var/www/FreshRSS/data \
  -v freshrss_extensions:/var/www/FreshRSS/extensions \
  --name freshrss \
  freshrss/freshrss

后续就是放行8080端口,然后访问http://你的ip:8080,进行安装即可。接着在添加反代,将8080端口转发到80端口,这样就可以使用http://你的ip访问了。具体可以参考呆哥的给博客添加一个输出友链 RSS 的页面,整个部署过程比较简单,这里就不再赘述了。我们只要记住开启api和api密码即可。

Artalk与FreshRss的实时更新机制

Astro的文档中,关于RSS订阅的文档比较少,而且官方的RSS订阅插件,只能订阅RSS源,不能订阅RSS订阅信息。所以,我只能自己动手,丰衣足食了。在这里我其实花了差不多两三天时间,踩了不少坑。因为我的思路是本地部署后生成dist文件,上传到宝塔面板。但是因为要更新Freshrss的订阅信息,所以需要重新生成dist文件,但是dist文件生成后,宝塔面板不会自动更新,所以需要手动更新。这就是个坑。

其实要实现,最简单的办法就是整个文件部署到服务器,给他定时任务,让他去渲染,这样就可以。但是这样太麻烦了,而且我还要在服务器上安装nodejs,所以我就放弃了。另一种方式就是通过Github Action实现,不过我觉得这个思路和我最开始出现了偏差,所以也放弃了。我就只想保持一个思路,我所有文件都在本地构建,但是liks页面却可以单独实现更新。

在这里,我就想到了一个方法,Astro引入Artalk的话,不管你是不是在本地构建的,只要用户评论了,页面不就可以实时显示评论吗?那么我如果我的Freshrss订阅信息,也通过和Artalk的评论功能一样的方式,不就可以实现实时更新了吗?

FreshRSS 与 Artalk 实时更新机制对比分析

特性Artalk (评论系统)FreshRSS (自托管 RSS 阅读器)
数据来源用户在你的网站上的交互行为 (发表评论)外部源 (订阅的博客/新闻等网站的 RSS Feed)
数据流向双向 (用户提交 → 你的数据库)单向 (外部源 → FreshRSS 数据库)
触发更新用户驱动 (点击提交按钮)定时任务驱动 (后台定期抓取)
前端角色主动交互 (提交/获取评论)主要被动展示 (获取聚合后的文章列表)

具体分析

也就是说,Freshrss和Artalk一样,都是后端服务内部,不依赖我们的Astro静态站点。那么我们是不是也就可以和artalk一样,通过客户端 JavaScript来按需或者定时获取呢?再简单一点的说法就是,我的所有freshrss订阅信息,都放在一个json文件中,然后通过客户端JavaScript定时去获取,然后渲染到页面上。因为我的这个页面本身已经渲染完毕,只要我把订阅信息包裹成一个组件,组件发生信息变更,这个组件也可以变更,这样也不需要重新渲染整个页面,就可以实现了订阅信息的变更了。

实现这个方法有两个,一个是使用纯js,第二种就是使用 React 组件 (更结构化)。我选用的就是第二种。

图片来源于本博客links页面截图@bosir

实现步骤

  1. 在 Astro 页面/组件中创建容器:

在你的 Astro 页面 (.astro) 或组件 (.astro / .jsx / .vue / .svelte) 中,创建一个 HTML 元素作为订阅列表的容器。

<Layout>
  <!-- 你的原代码 -->
  <!-- 组件加载前输出信息 -->
  <script>
    console.log('即将加载 FreshRSSListReact 组件');
  </script>
  <FreshRSSListReact lang={lang} client:load />
  <!-- 组件加载后输出信息 -->
  <script>
    document.addEventListener('astro:load', () => {
      console.log('FreshRSSListReact 组件加载完成');
    });
  </script>
</Layout>
  1. 编写 React 组件:

使用 React 编写一个组件,用于渲染订阅列表。组件接收订阅数据作为输入,并根据数据动态生成订阅列表。

// src/components/FreshRSSList.jsx
import { useState, useEffect } from 'react';

const FRESHRSS_API_URL = 'https://your-freshrss-domain.tld/api/greader.php'; // 或 /api/
const FRESHRSS_USER_ID = 'your_freshrss_username';
const FRESHRSS_API_PASSWORD = 'your_api_password_here'; // 重要:注意安全!

export default function FreshRSSList() {
  const [items, setItems] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    async function fetchFreshRSS() {
      try {
        const authString = `${FRESHRSS_USER_ID}:${FRESHRSS_API_PASSWORD}`;
        const encodedAuth = btoa(authString);
        const headers = { 'Authorization': `Basic ${encodedAuth}` };

        const apiEndpoint = `${FRESHRSS_API_URL}/reader/api/0/stream/contents?output=json&xt=user/-/state/com.google/read&n=20`;

        const response = await fetch(apiEndpoint, { headers });
        if (!response.ok) throw new Error(`API error: ${response.status}`);

        const data = await response.json();
        setItems(data.items || []);
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    }

    fetchFreshRSS();
  }, []); // 空依赖数组表示只在组件挂载时运行一次

  if (loading) return <p>Loading FreshRSS subscriptions...</p>;
  if (error) return <p className="error">Error: {error}</p>;
  if (items.length === 0) return <p>No unread subscriptions found.</p>;

  return (
    <ul className="freshrss-list">
      {items.map((item) => (
        <li key={item.id}>
          <a
            href={item.canonical?.[0]?.href || item.alternate?.[0]?.href || '#'}
            target="_blank"
            rel="noopener noreferrer"
          >
            {item.title}
          </a>
          {item.summary?.content && (
            <p className="summary" dangerouslySetInnerHTML={{ __html: item.summary.content }} />
          )}
          <span className="source">{item.origin?.title || 'Unknown Source'}</span>
        </li>
      ))}
    </ul>
  );
}
  1. 引入 React 组件:在 Astro 页面中引入 React 组件。可以使用 React 提供的方法,如 ReactDOM.render 或 ReactDOM.createRoot 来渲染组件。
---
import FreshRSSListReact from '../components/FreshRSSListReact.jsx'; // 引入组件
import Layout from '@/layouts/Layout.astro';
---
<Layout>
  <!-- 你的原代码 -->
  <!-- 组件加载前输出信息 -->
  <script>
    console.log('即将加载 FreshRSSListReact 组件');
  </script>
  <FreshRSSListReact lang={lang} client:load />
  <!-- 组件加载后输出信息 -->
  <script>
    document.addEventListener('astro:load', () => {
      console.log('FreshRSSListReact 组件加载完成');
    });
  </script>
</Layout>

因为Astro部署的方法不一样,还有就是主题各不相同,所以具体的实现方法,还是需要不断进行调整的。通过这样一个思路,后面会慢慢去逐步摸索和实现其它的一些功能,比如 Mastodon 、Artalk 之类的。