blog-v3 友圈:聚合友链文章

blog-v3 友圈:聚合友链文章

在 blog-v3 中接入 Friend-Circle 的 all.json 和 link.json,展示友链状态、最新文章,并使用 Clarity 卡片完成排序、分页与手机端适配。
本篇对应博客的 /circle 页面

友链页回答“我的朋友是谁”,友圈页回答“朋友们最近写了什么”。这篇记录如何把 Friend-Circle 的公开 JSON 接入 Nuxt 4 + Clarity,展示友链状态和朋友的新文章。

先看清楚数据从哪里来

这个页面不在浏览器里逐个抓取朋友的博客,而是读取后端已经汇总好的数据。

Friend-Circle 后端

负责采集文章、检查站点状态,并生成供前端读取的公开 JSON。

博客友圈页

打开 /circle 时,浏览器并行请求 all.json 和 link.json,然后渲染状态卡片与文章列表。

读者访问原文

点击文章卡片跳转到朋友的站点。友圈只提供发现入口,不复制和重新发布完整文章。

公开 JSON 每页 6 篇 按发布时间排序

本博客使用的两个地址是:

text
https://fc.moodlog.cn/all.json
https://fc.moodlog.cn/link.json
接入前提

本文讲的是博客前端接入,不包含 Friend-Circle 服务端从零部署。你需要先准备能返回下述字段的后端。如果使用其他版本或其他聚合服务,应以实际响应为准调整字段,不能只把域名换掉就默认兼容。

一、检查接口和跨域

先确认服务能够返回 JSON,而不是登录页、验证页或反向代理错误页面:

sh
curl -fsS https://fc.moodlog.cn/all.json
curl -fsS https://fc.moodlog.cn/link.json

all.json 中,页面主要用到:

字段用途
statistical_data.article_num展示已收录文章总数
article_data[].title文章标题
article_data[].created发布时间和排序依据
article_data[].link原文地址与列表 key
article_data[].author作者名称
article_data[].avatar文章卡片图标

link.json 中,状态卡片读取:

字段含义
link_total_num友链总数
link_reachable_num可正常访问的站点数
link_unreachable_num暂时无法访问的站点数
crawl_allowed_num允许抓取的站点数
link_last_checked_time后端最后检查时间

这些状态字段都位于 statistical_data 中。后端还可以返回单个站点的详细信息,但当前友圈页展示的是汇总状态,并没有逐站状态表。

终端能访问,浏览器却失败怎么办

博客和数据源不在同一个源:blog.moodlog.cn 与 fc.moodlog.cn 是不同源。即使同属一个主域名,也需要正确的跨域响应头。

可以先检查:

sh
curl -sI \
  -H 'Origin: https://blog.moodlog.cn' \
  https://fc.moodlog.cn/all.json

如果数据服务只供自己的博客读取,可以按服务端实际配置限定允许来源:

http
Access-Control-Allow-Origin: https://blog.moodlog.cn
Vary: Origin

这不是让前端添加请求头,而是要求数据服务或反向代理返回正确的响应头。本地预览来源也应单独考虑。不要用 mode: 'no-cors' 绕过问题,那样拿到的不透明响应无法读取 JSON。

二、新建 Nuxt 页面

在项目中创建:

text
app/pages/circle.vue

Nuxt 会自动生成 /circle 路由,无需再创建同名 Markdown 页面。下面是一份用于理解接入流程的精简实现,包含四项状态、最新文章、六篇分页和失败提示。现有项目已经完成接入时,不必覆盖原文件。

这段代码依赖本项目的 Clarity Alert、LinkCard 与主题变量,并不是任意 Nuxt 空项目都自带的组件。换用其他主题时,要相应替换它们。生产环境也应根据数据来源补充运行时结构校验和请求超时;TypeScript 类型断言本身并不验证接口数据。

三、排序和分页为什么放在前端

all.json 返回的是已收录文章集合。当前实现先按 created 从新到旧排序,再通过 slice() 选择当前页,每页展示六篇。

这里有两个小细节:

  1. 先复制数组再排序,不直接修改接口返回的原数组。
  2. 相同时间使用原文链接作次级排序,避免同一批文章反复交换位置。
这是展示分页,不是后端分页

点击下一页不会重新请求接口,也不会降低首次下载的 JSON 大小。如果后端将来收录成千上万篇文章,应评估服务端分页或限制导出数量,而不是只调整每页条数。

时间格式也需要约定。当前页面会把 2026-10-16 12:00:00 中的空格换为 T 后交给 Date 解析;若接入不同数据源,优先使用带时区的 ISO 时间。缺少时区或混合格式可能导致排序与显示产生偏差。

四、添加导航入口

在 app/app.config.ts 的现有导航数组中加入一项,不要覆盖整个配置:

text
{ icon: 'tabler:heart-handshake', text: '友圈', url: '/circle' },

上面是一条数组项片段,实际应放入项目已有的导航数组中。本博客已经配置好这项入口,无需重复添加。

现有 /circle 页面还包含移动端 BlogHeader、按页码跳转按钮,以及底部 PostComment 评论区。它们属于主题页面整合,和 Friend-Circle 数据采集本身是分开的。

五、手机端和主题保持一致

友链状态在桌面端为四列,手机端改成两列。文章列表保持一列,复用 Clarity 的 LinkCard,避免文章标题和作者信息被挤在过窄的卡片中。

样式上优先使用主题变量:

  • --c-border:边框
  • --c-text-2:辅助文字
  • --c-primary:主色
  • --c-success:正常状态
  • --c-warning:异常提醒

颜色不应成为唯一提示。例如“暂时异常”应同时有文字和图标,不能只把数字改成红色。

六、理解“实时”的边界

页面使用时间戳查询参数和 cache: 'no-store',进入页面时读取接口当前返回的版本。这不代表页面会触发后台立即检查所有友链,也不代表浏览器会定时轮询。

后端什么时候采集,页面就只能展示到什么时候。 判断数据是否陈旧,应看 link_last_checked_time 和采集日志,而不是只看页面刚刚有没有刷新。

同样,友链“暂时异常”只是某次检查的结果,可能来自网络、证书、防护或超时;“允许抓取”也不等于已经收录文章。不要根据单次异常就自动删除友链。

七、排错与验证

现象优先检查
JSON 直接打开正常,页面读取失败CORS、HTTPS、浏览器网络面板
统计数字为空实际字段是否和 statistical_data 定义一致
新文章迟迟不出现后端采集时间、订阅源和导出结果
卡片顺序不正确created 格式、时区、无效日期
下一页内容没有网络请求正常,当前使用前端分页

在项目目录执行:

sh
pnpm exec eslint app/pages/circle.vue
pnpm build

本地调试可以使用:

sh
pnpm dev

打开终端提示的本地地址并访问 /circle,检查四项状态、文章链接、前后翻页和窄屏布局。文章标题使用 Vue 文本插值,而不是把上游内容直接作为 HTML 注入。

三个页面,各司其职

友圈用来发现朋友的更新,动态用来记录自己的碎片,统计用来观察站点的访问和更新节奏。它们都能使用 API 数据,但并不需要采用完全相同的请求方式:友圈读取公开 JSON,动态通过博客服务端筛选内容,统计由服务端聚合数据。

同系列的另外两篇:

把朋友们认真写下的文章放到一个容易发现的地方,让友链不只停留在名单里,也能在阅读中继续产生连接。

blog-v3 动态:接入 Memos
写给未来的第一封信

评论区

评论加载中...