blog-v3 动态:接入 Memos

blog-v3 动态:接入 Memos

在 Nuxt 4 + Nuxt Content 3 博客中接入自建 Memos,只展示公开动态,并处理分页、Markdown、附件和重复图片。
这篇文章做了什么

把自建 Memos 变成博客的动态数据源:Memos 负责发布,博客负责展示。新增或修改公开动态后,动态页会通过服务端接口读取,不需要重新写 Markdown 文章。

目标结构

Memos

在 memos.moodlog.cn 中发布一条动态,并把可见性设为公开。

Nuxt 服务端接口

博客通过 /api/moments 请求 Memos,只保留指定账号的公开、正常状态动态。

动态页面

/moments 负责展示时间、标签、Markdown、图片和附件,并提供加载更多。

这种结构的关键是:令牌不进入浏览器,数据筛选放在服务端完成。当前实例已经开启公开访问,因此博客不需要携带访问令牌。

Memos API 的基本接入

Memos v0.31 的匿名列表接口是:

text
GET https://memos.example.com/api/v1/memos

博客接口只接受一个分页参数:

text
GET /api/moments?pageToken=下一页令牌

不要把用户传入的 URL 当作代理地址。服务端应该固定 Memos 来源,并固定查询条件:

ts
const response = await $fetch<MemosResponse>(
  'https://memos.example.com/api/v1/memos',
  {
    query: {
      pageSize: 10,
      pageToken,
      state: 'NORMAL',
      orderBy: 'pinned desc, create_time desc',
      filter: 'creator == "users/Zhang" && visibility == "PUBLIC"',
    },
  },
)
不要把访问令牌写进前端

如果实例保持私有,令牌也应该只放在服务端环境变量中,不能写进 Vue 组件、公开仓库或浏览器请求。当前公开模式下,匿名读取已经足够,就不需要额外保存令牌。

服务端再次过滤

上游过滤不是唯一防线。收到数据后,还要再次检查:

  • creator 是否是指定账号
  • visibility 是否为 PUBLIC
  • state 是否为 NORMAL
  • parent 是否为空,避免把回复当成独立动态
  • name 是否符合 memos/{id} 格式

接口只返回页面真正需要的字段,不把创建者、可见性等内部字段原样透传给前端。

Markdown 和附件

动态正文支持基础 Markdown,但不要直接渲染 Memos 返回的原始 HTML。当前实现使用 markdown-it,并关闭原始 HTML:

ts
const markdown = new MarkdownIt({
  html: false,
  linkify: true,
  breaks: true,
})

图片、链接只允许 http 和 https,拒绝 javascript: 等危险协议。附件按类型分别展示:

  • 图片:图片网格
  • 视频:原生 <video controls>
  • 音频:原生 <audio controls>
  • 其他文件:附件链接

为什么一张图片会显示两次

Memos 的一张图片可能同时出现在两个位置:

  1. Markdown 正文:![图片](/file/attachments/xxx)
  2. attachments 字段:attachments/xxx

如果把正文和附件直接分别渲染,就会得到两张相同图片。

解决方式是先解析 Markdown 图片,取出资源标识,再过滤附件:

ts
function imageKey(value: string) {
  // Memos 的以下地址都指向同一个附件资源:
  // /file/attachments/id
  // /file/attachments/id/name.webp
  // /file/attachments/id/name.webp?thumbnail=true
  return resourceIdFromMemosFileUrl(value)
}

这里应该按附件 ID 判断,而不是按文件名判断,因为两个不同附件可能恰好使用同一个文件名。正文图片保留在原本位置,只有重复的附件图片被隐藏。

正文顺序优先 按资源 ID 去重 不猜文件名

动态页的缓存和分页

动态接口每页读取 10 条,并在服务端缓存 60 秒。缓存可以减少 Memos 请求,但也意味着刚发布的动态可能最多延迟约一分钟出现。

页面加载更多时只携带 nextPageToken,不要把所有动态一次性加载。请求失败时要保留当前页内容,并按照原来的分页方向重试,避免“加载下一页失败后重新请求第一页”。

页面效果

动态卡片包含:

  • 时间线 显示作者、发布时间和置顶状态。
  • 正文 支持 Markdown、代码块、引用和链接。
  • 媒体 支持图片、视频、音频和普通附件。
  • 移动端 卡片、标签和媒体自动适配窄屏。
  • 空状态 没有公开动态时显示友好的提示,而不是伪造示例内容。

发布和验证清单

sh
# 本地回归测试
node scripts/test-moments.mjs

# 代码检查
pnpm exec eslint app/pages/moments.vue server/utils/memos.ts server/api/moments.get.ts

# 生产构建
pnpm build

最后访问:

text
https://your-domain.example/moments

如果 Memos 中只有一张图片,动态页也应该只呈现一张;如果正文里没有引用附件图片,附件区才补充显示它。

动态页适合记录短内容,正文文章适合沉淀长内容。让 Memos 负责快速发布,让博客负责长期展示,两者各自做好擅长的事。

blog-v3 统计:Umami 访问趋势
blog-v3 友圈:聚合友链文章

评论区

评论加载中...