在Hugo博客中优雅地添加轻量朋友圈

数据抓取

配置

项目文件放在 hugo-friend-circle 。接入 Hugo 时复制:

  • scripts/fetch_feeds.py
  • data/friends.yaml
  • data/friend_posts.json
  • layouts/page/friends-circle.html
  • .github/workflows/update.yml

朋友列表写在 data/friends.yaml。最小配置如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
settings:
  max_posts: 100
  max_posts_per_friend: 10
  timeout: 10
  retries: 2

friends:
  - name: Example Friend
    site: https://example.com/
    feed: https://example.com/index.xml
    avatar: https://example.com/avatar.png
    description: A friend with RSS.

  - name: HTML Only Friend
    site: https://example.org/
    avatar: https://example.org/avatar.png
    description: No RSS exposed; fallback will try dated links on homepage and archives.

省略 feed 时,脚本先在站点首页寻找 RSS / Atom 的 <link rel="alternate">;未找到时,再解析首页、/archives//posts//post/ 中的文章链接。

HTML 兜底适合归档页中带有明确日期和文章标题的站点,无法覆盖所有博客主题。结构特殊的站点仍应显式填写 RSS 或 Atom feed。

慢站点与失效站点

网络较慢的站点单独调大 timeoutretries

1
2
3
4
5
6
  - name: Slow Friend
    site: https://slow.example/
    feed: https://slow.example/rss.xml
    avatar: https://slow.example/avatar.png
    timeout: 25
    retries: 3

域名过期、站点关闭或没有可解析 feed 时,保留友链卡片并关闭朋友圈抓取:

1
2
3
4
5
6
  - name: Offline Friend
    site: https://old-domain.example/
    avatar: https://old-domain.example/avatar.png
    description: Keep the link card, but skip friend-circle crawling.
    circle: false
    circle_reason: domain unavailable

设置 circle: false 后,该站点不参与抓取,也不会拖慢整轮更新。

本地生成

独立项目中配置好 data/friends.yaml 后,在 Hugo 站点根目录运行:

1
python scripts\fetch_feeds.py

脚本会生成或更新 data/friend_posts.json。这个文件里包含朋友名称、头像、文章标题、文章链接、发布时间、来源站点,以及抓取失败时的 warning。

页面接入

独立页面

独立页面放在 content/friends-circle/index.md

1
2
3
4
5
6
7
---
title: "Friend Circle"
description: "Recent posts from friends."
layout: "friends-circle"
slug: "friends-circle"
comments: false
---

layouts/page/friends-circle.html 读取 site.Data.friend_posts 并渲染文章列表。

Stack 友链页

已有 Stack 友链页时,可以把 Friend Circle 放在普通友链卡片下方。

实际效果可参考 https://www.elainafan.one/friends/

友链页整体效果,普通友链卡片下方接入 Friend Circle

友链卡片由 layouts/partials/article/components/links.html 渲染。partial 末尾调用 Friend Circle:

1
2
3
{{ if or .Params.friendCircle (eq .Params.slug "friends") }}
    {{ partial "article/components/friend-circle" . }}
{{ end }}

朋友圈主体位于 layouts/partials/article/components/friend-circle.html,从 data/friend_posts.json 读取数据:

1
2
3
4
{{ $data := site.Data.friend_posts | default dict }}
{{ $posts := $data.posts | default slice }}
{{ $friends := $data.friends | default slice }}
{{ $warnings := $data.warnings | default slice }}

文章卡片只显示头像、标题与日期,不渲染摘要。

Friend Circle 卡片样式,显示头像、文章标题和日期

  • 搜索文章或朋友。
  • 按朋友筛选。
  • 默认只展示前几条,点击 Load More 后继续展开。

这些交互使用原生 JavaScript。数据在 Hugo 构建时已经写入页面,筛选与展开只切换现有 DOM 的显示状态。

定时更新

GitHub Actions 定时运行脚本,并把生成的 data/friend_posts.json 提交回仓库。

博客仓库中的 workflow 如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
name: update friend circle

on:
  schedule:
    - cron: "0 0 * * *"
  workflow_dispatch:
  push:
    paths:
      - "data/friends.yaml"
      - "scripts/fetch_feeds.py"

permissions:
  contents: write

jobs:
  update:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.x"
      - name: Fetch feeds
        run: python scripts/fetch_feeds.py
      - name: Commit generated data
        uses: stefanzweifel/git-auto-commit-action@v5
        with:
          commit_message: "chore: update friend circle data"
          file_pattern: data/friend_posts.json

部署平台监听仓库更新时,GitHub Actions 提交新 JSON 后会触发一次重新部署。

抓取边界

  • 模板仓库只保存示例 friends.yaml 与空的 friend_posts.json,个人友链数据留在自己的博客仓库。

  • 没有 RSS、feed 地址特殊或归档结构不规则的站点可能无法抓取。HTML 解析保持通用规则,不为单个站点写死选择器。

  • 失效站点显式设置 circle: false,友链卡片与朋友圈抓取互不影响。

  • 页面只显示头像、标题和日期。摘要、标签与封面不进入卡片,友链页保持轻量。