[{"data":1,"prerenderedAt":396},["ShallowReactive",2],{"content:\u002Fmemos-dynamic-page":3,"surround:\u002Fmemos-dynamic-page":387},{"id":4,"title":5,"body":6,"categories":359,"date":361,"description":362,"draft":363,"extension":364,"image":365,"meta":366,"navigation":368,"path":369,"permalink":369,"published":370,"readingTime":371,"recommend":370,"references":370,"seo":376,"sitemap":377,"stem":378,"tags":379,"type":385,"updated":361,"__hash__":386},"content\u002Fposts\u002F2026\u002Fmemos-dynamic-page.md","blog-v3 动态：接入 Memos",{"type":7,"value":8,"toc":347},"minimark",[9,18,22,55,63,67,70,80,83,89,92,100,107,110,113,156,159,163,170,176,191,211,214,217,235,238,241,247,250,263,266,269,276,279,282,317,320,328,331,337,340],[10,11,14],"alert",{"title":12,"type":13},"这篇文章做了什么","info",[15,16,17],"p",{},"把自建 Memos 变成博客的动态数据源：Memos 负责发布，博客负责展示。新增或修改公开动态后，动态页会通过服务端接口读取，不需要重新写 Markdown 文章。",[19,20,21],"h2",{"id":21},"目标结构",[23,24,25,28,36,39,46,49],"timeline",{},[15,26,27],{},"{Memos}",[15,29,30,31,35],{},"在 ",[32,33,34],"code",{"code":34},"memos.moodlog.cn"," 中发布一条动态，并把可见性设为公开。",[15,37,38],{},"{Nuxt 服务端接口}",[15,40,41,42,45],{},"博客通过 ",[32,43,44],{"code":44},"\u002Fapi\u002Fmoments"," 请求 Memos，只保留指定账号的公开、正常状态动态。",[15,47,48],{},"{动态页面}",[15,50,51,54],{},[32,52,53],{"code":53},"\u002Fmoments"," 负责展示时间、标签、Markdown、图片和附件，并提供加载更多。",[15,56,57,58,62],{},"这种结构的关键是：",[59,60,61],"strong",{},"令牌不进入浏览器，数据筛选放在服务端完成","。当前实例已经开启公开访问，因此博客不需要携带访问令牌。",[19,64,66],{"id":65},"memos-api-的基本接入","Memos API 的基本接入",[15,68,69],{},"Memos v0.31 的匿名列表接口是：",[71,72,78],"pre",{"className":73,"code":75,"language":76,"meta":77},[74],"language-text","GET https:\u002F\u002Fmemos.example.com\u002Fapi\u002Fv1\u002Fmemos\n","text","",[32,79,75],{"__ignoreMap":77},[15,81,82],{},"博客接口只接受一个分页参数：",[71,84,87],{"className":85,"code":86,"language":76,"meta":77},[74],"GET \u002Fapi\u002Fmoments?pageToken=下一页令牌\n",[32,88,86],{"__ignoreMap":77},[15,90,91],{},"不要把用户传入的 URL 当作代理地址。服务端应该固定 Memos 来源，并固定查询条件：",[71,93,98],{"className":94,"code":96,"language":97,"meta":77},[95],"language-ts","const response = await $fetch\u003CMemosResponse>(\n  'https:\u002F\u002Fmemos.example.com\u002Fapi\u002Fv1\u002Fmemos',\n  {\n    query: {\n      pageSize: 10,\n      pageToken,\n      state: 'NORMAL',\n      orderBy: 'pinned desc, create_time desc',\n      filter: 'creator == \"users\u002FZhang\" && visibility == \"PUBLIC\"',\n    },\n  },\n)\n","ts",[32,99,96],{"__ignoreMap":77},[10,101,104],{"title":102,"type":103},"不要把访问令牌写进前端","warning",[15,105,106],{},"如果实例保持私有，令牌也应该只放在服务端环境变量中，不能写进 Vue 组件、公开仓库或浏览器请求。当前公开模式下，匿名读取已经足够，就不需要额外保存令牌。",[19,108,109],{"id":109},"服务端再次过滤",[15,111,112],{},"上游过滤不是唯一防线。收到数据后，还要再次检查：",[114,115,116,123,132,140,146],"ul",{},[117,118,119,122],"li",{},[32,120,121],{"code":121},"creator"," 是否是指定账号",[117,124,125,128,129],{},[32,126,127],{"code":127},"visibility"," 是否为 ",[32,130,131],{"code":131},"PUBLIC",[117,133,134,128,137],{},[32,135,136],{"code":136},"state",[32,138,139],{"code":139},"NORMAL",[117,141,142,145],{},[32,143,144],{"code":144},"parent"," 是否为空，避免把回复当成独立动态",[117,147,148,151,152,155],{},[32,149,150],{"code":150},"name"," 是否符合 ",[32,153,154],{"code":154},"memos\u002F{id}"," 格式",[15,157,158],{},"接口只返回页面真正需要的字段，不把创建者、可见性等内部字段原样透传给前端。",[19,160,162],{"id":161},"markdown-和附件","Markdown 和附件",[15,164,165,166,169],{},"动态正文支持基础 Markdown，但不要直接渲染 Memos 返回的原始 HTML。当前实现使用 ",[32,167,168],{"code":168},"markdown-it","，并关闭原始 HTML：",[71,171,174],{"className":172,"code":173,"language":97,"meta":77},[95],"const markdown = new MarkdownIt({\n  html: false,\n  linkify: true,\n  breaks: true,\n})\n",[32,175,173],{"__ignoreMap":77},[15,177,178,179,182,183,186,187,190],{},"图片、链接只允许 ",[32,180,181],{"code":181},"http"," 和 ",[32,184,185],{"code":185},"https","，拒绝 ",[32,188,189],{"code":189},"javascript:"," 等危险协议。附件按类型分别展示：",[114,192,193,196,202,208],{},[117,194,195],{},"图片：图片网格",[117,197,198,199],{},"视频：原生 ",[32,200,201],{"code":201},"\u003Cvideo controls>",[117,203,204,205],{},"音频：原生 ",[32,206,207],{"code":207},"\u003Caudio controls>",[117,209,210],{},"其他文件：附件链接",[19,212,213],{"id":213},"为什么一张图片会显示两次",[15,215,216],{},"Memos 的一张图片可能同时出现在两个位置：",[218,219,220,226],"ol",{},[117,221,222,223],{},"Markdown 正文：",[32,224,225],{"code":225},"![图片](\u002Ffile\u002Fattachments\u002Fxxx)",[117,227,228,231,232],{},[32,229,230],{"code":230},"attachments"," 字段：",[32,233,234],{"code":234},"attachments\u002Fxxx",[15,236,237],{},"如果把正文和附件直接分别渲染，就会得到两张相同图片。",[15,239,240],{},"解决方式是先解析 Markdown 图片，取出资源标识，再过滤附件：",[71,242,245],{"className":243,"code":244,"language":97,"meta":77},[95],"function imageKey(value: string) {\n  \u002F\u002F Memos 的以下地址都指向同一个附件资源：\n  \u002F\u002F \u002Ffile\u002Fattachments\u002Fid\n  \u002F\u002F \u002Ffile\u002Fattachments\u002Fid\u002Fname.webp\n  \u002F\u002F \u002Ffile\u002Fattachments\u002Fid\u002Fname.webp?thumbnail=true\n  return resourceIdFromMemosFileUrl(value)\n}\n",[32,246,244],{"__ignoreMap":77},[15,248,249],{},"这里应该按附件 ID 判断，而不是按文件名判断，因为两个不同附件可能恰好使用同一个文件名。正文图片保留在原本位置，只有重复的附件图片被隐藏。",[15,251,252,256,257,256,260],{},[253,254,255],"badge",{},"正文顺序优先"," ",[253,258,259],{},"按资源 ID 去重",[253,261,262],{},"不猜文件名",[19,264,265],{"id":265},"动态页的缓存和分页",[15,267,268],{},"动态接口每页读取 10 条，并在服务端缓存 60 秒。缓存可以减少 Memos 请求，但也意味着刚发布的动态可能最多延迟约一分钟出现。",[15,270,271,272,275],{},"页面加载更多时只携带 ",[32,273,274],{"code":274},"nextPageToken","，不要把所有动态一次性加载。请求失败时要保留当前页内容，并按照原来的分页方向重试，避免“加载下一页失败后重新请求第一页”。",[19,277,278],{"id":278},"页面效果",[15,280,281],{},"动态卡片包含：",[283,284,285],"card-list",{},[114,286,287,293,299,305,311],{},[117,288,289,292],{},[59,290,291],{},"时间线"," 显示作者、发布时间和置顶状态。",[117,294,295,298],{},[59,296,297],{},"正文"," 支持 Markdown、代码块、引用和链接。",[117,300,301,304],{},[59,302,303],{},"媒体"," 支持图片、视频、音频和普通附件。",[117,306,307,310],{},[59,308,309],{},"移动端"," 卡片、标签和媒体自动适配窄屏。",[117,312,313,316],{},[59,314,315],{},"空状态"," 没有公开动态时显示友好的提示，而不是伪造示例内容。",[19,318,319],{"id":319},"发布和验证清单",[71,321,326],{"className":322,"code":324,"language":325,"meta":77},[323],"language-sh","# 本地回归测试\nnode scripts\u002Ftest-moments.mjs\n\n# 代码检查\npnpm exec eslint app\u002Fpages\u002Fmoments.vue server\u002Futils\u002Fmemos.ts server\u002Fapi\u002Fmoments.get.ts\n\n# 生产构建\npnpm build\n","sh",[32,327,324],{"__ignoreMap":77},[15,329,330],{},"最后访问：",[71,332,335],{"className":333,"code":334,"language":76,"meta":77},[74],"https:\u002F\u002Fyour-domain.example\u002Fmoments\n",[32,336,334],{"__ignoreMap":77},[15,338,339],{},"如果 Memos 中只有一张图片，动态页也应该只呈现一张；如果正文里没有引用附件图片，附件区才补充显示它。",[341,342,344],"quote",{"icon":343},"tabler:message-circle",[15,345,346],{},"动态页适合记录短内容，正文文章适合沉淀长内容。让 Memos 负责快速发布，让博客负责长期展示，两者各自做好擅长的事。",{"title":77,"searchDepth":348,"depth":348,"links":349},4,[350,352,353,354,355,356,357,358],{"id":21,"depth":351,"text":21},2,{"id":65,"depth":351,"text":66},{"id":109,"depth":351,"text":109},{"id":161,"depth":351,"text":162},{"id":213,"depth":351,"text":213},{"id":265,"depth":351,"text":265},{"id":278,"depth":351,"text":278},{"id":319,"depth":351,"text":319},[360],"博客折腾","2026-10-16","在 Nuxt 4 + Nuxt Content 3 博客中接入自建 Memos，只展示公开动态，并处理分页、Markdown、附件和重复图片。",false,"md","https:\u002F\u002Fimg.moodlog.cn\u002Fi\u002F1\u002Fd532dc9feada45d523229056fedbe26c\u002F6ad04cfad3063.webp",{"slots":367},{},true,"\u002Fmemos-dynamic-page",null,{"text":372,"minutes":373,"time":374,"words":375},"6 min read",5.415,324900,1083,{"title":5,"description":362},{"loc":369},"posts\u002F2026\u002Fmemos-dynamic-page",[380,381,382,383,384],"Nuxt","Memos","API","动态页","blog-v3","story","LQgxzOpfxauxklKJ-SK_-sp9qB0vFQoPqyw3PcxslFU",[388,392],{"title":389,"path":390,"stem":391,"date":361,"type":385,"children":-1},"blog-v3 友圈：聚合友链文章","\u002Ffriend-circle-page","posts\u002F2026\u002Ffriend-circle-page",{"title":393,"path":394,"stem":395,"date":361,"type":385,"children":-1},"blog-v3 统计：Umami 访问趋势","\u002Fumami-analytics-page","posts\u002F2026\u002Fumami-analytics-page",1792159297632]