[{"data":1,"prerenderedAt":346},["ShallowReactive",2],{"content:\u002Fposts\u002Fmemos-dynamic":3,"surround:\u002Fposts\u002Fmemos-dynamic":336},{"id":4,"title":5,"body":6,"categories":308,"date":310,"description":311,"draft":312,"extension":313,"image":314,"meta":315,"navigation":317,"path":318,"permalink":319,"pinned":312,"published":319,"readingTime":320,"recommend":319,"references":319,"seo":325,"sitemap":326,"stem":327,"tags":328,"type":333,"updated":334,"__hash__":335},"content\u002Fposts\u002Fposts\u002Fmemos-dynamic.md","Firefly 魔改：集成 Memos API 作为动态数据源",{"type":7,"value":8,"toc":295},"minimark",[9,19,22,25,32,37,45,68,72,78,98,108,115,122,126,139,146,150,206,210,217,228,242,246,256,264,272,280,288,292],[10,11,15],"alert",{"title":12,"type":13,"icon":14},"AI 迁移提示","info","tabler:robot",[16,17,18],"p",{},"本文由 AI 协助从旧站迁移，尚未完成逐篇人工审校；内容如有疏漏，将在复核后修订。",[10,20],{"title":21,"type":13},"> 本文部分内容由 AI 辅助整理，已结合实际操作进行校验，请根据自身环境谨慎使用。",[16,23,24],{},"之前在博客中维护动态内容需要手动编辑本地 Markdown 文件，比较繁琐。如果能直接用自建的 Memos 服务作为数据源，发布动态后自动同步到博客，体验会好很多。",[16,26,27],{},[28,29],"img",{"alt":30,"src":31},"动态页完整效果","https:\u002F\u002Fimg.olinl.com\u002Ffile\u002Fpost-img\u002Fmemos-dynamic\u002F0001.webp",[33,34,36],"h2",{"id":35},"一设计思路","一、设计思路",[16,38,39,40,44],{},"用 Memos 替代本地 ",[41,42,43],"code",{"code":43},"content\u002Fdynamic\u002F"," 集合，通过 API 代理层获取数据：",[46,47,48,52,59,62,65],"ol",{},[49,50,51],"li",{},"配置 Memos 服务地址和 Token",[49,53,54,55,58],{},"服务端 API 代理请求 Memos 数据（",[41,56,57],{"code":57},"src\u002Fpages\u002Fapi\u002Fdynamic.json.ts","）",[49,60,61],{},"支持标签过滤（只显示包含特定标签的 memo）",[49,63,64],{},"支持可见性控制（仅公开\u002F全部）",[49,66,67],{},"降级处理：Memos 不可用时返回空数据",[33,69,71],{"id":70},"二配置","二、配置",[16,73,74,77],{},[41,75,76],{"code":76},"src\u002Fconfig\u002FdynamicConfig.ts"," 中配置 Memos 连接信息：",[79,80,95],"pre",{"className":81,"code":83,"highlights":84,"language":93,"meta":94},[82],"language-typescript","export const dynamicConfig: DynamicConfig = {\n    title: \"\",\n    description: \"\",\n    cover: {\n        enable: true,\n        image: \"https:\u002F\u002Fph.0824.uk\u002Ffile\u002F博客横屏封面\u002F1780635498430_mmexport1774845895097.jpg\",\n        greeting: \"Hello 顾拾柒\",\n    },\n    showComment: true,\n    itemsPerPage: 20,\n\n    \u002F\u002F Memos 集成（配置后将从 Memos 拉取动态，忽略本地 content\u002Fdynamic\u002F）\n    memos: {\n        serverUrl: \"https:\u002F\u002Fnote.olinl.com\",\n        accessToken: \"\",\n        pageSize: 100,\n        visibility: \"public\",\n        tags: [\"blog\"],\n    },\n};\n",[85,86,87,88,89,90,91,92],12,13,14,15,16,17,18,19,"typescript","title=\"src\u002Fconfig\u002FdynamicConfig.ts\" ins=",[41,96,83],{"__ignoreMap":97},"",[16,99,100,103,104,107],{},[41,101,102],{"code":102},"accessToken"," 可留空（仅拉取公开 memo），",[41,105,106],{"code":106},"tags"," 控制只显示带特定标签的动态。",[16,109,110,111,114],{},"类型定义在 ",[41,112,113],{"code":113},"src\u002Ftypes\u002FdynamicConfig.ts","：",[79,116,120],{"className":117,"code":118,"language":93,"meta":119},[82],"export type DynamicConfig = {\n    title?: string;\n    description?: string;\n    showComment?: boolean;\n    itemsPerPage?: number;\n    cover?: { enable: boolean; image: string; greeting?: string };\n    memos?: {\n        serverUrl: string;\n        accessToken: string;\n        pageSize?: number;\n        visibility?: \"all\" | \"public\";\n        tags?: string[];\n    };\n};\n","title=\"src\u002Ftypes\u002FdynamicConfig.ts\"",[41,121,118],{"__ignoreMap":97},[33,123,125],{"id":124},"三api-代理层","三、API 代理层",[16,127,128,130,131,134,135,138],{},[41,129,57],{"code":57}," 是核心代理端点。文件放在 ",[41,132,133],{"code":133},"src\u002Fpages\u002Fapi\u002F"," 下，Astro 自动将其注册为 ",[41,136,137],{"code":137},"\u002Fapi\u002Fdynamic.json"," 路由，无需额外配置。",[79,140,144],{"className":141,"code":142,"language":93,"meta":143},[82],"import { createMarkdownProcessor } from \"@astrojs\u002Fmarkdown-remark\";\nimport { dynamicConfig } from \"@\u002Fconfig\u002FdynamicConfig\";\n\nlet processor: Awaited\u003CReturnType\u003Ctypeof createMarkdownProcessor>> | null = null;\nasync function getProcessor() {\n    if (!processor) processor = await createMarkdownProcessor();\n    return processor;\n}\n\nconst markdownImagePattern = \u002F!\\[([^\\]]*)\\]\\((\\S+?)(?:\\s+[\"']([^\"']*)[\"'])?\\)\u002Fg;\n\ninterface MemoAttachment {\n    name: string;\n    filename: string;\n    type: string;\n    externalLink?: string;\n}\n\ninterface Memo {\n    name: string;\n    content: string;\n    createTime: string;\n    displayTime?: string;\n    visibility: string;\n    tags?: string[];\n    attachments?: MemoAttachment[];\n}\n\ninterface MemosResponse {\n    memos: Memo[];\n}\n\nasync function fetchFromMemos(): Promise\u003C\n    {\n        id: string;\n        published: number;\n        html: string;\n        images: { alt: string; src: string }[];\n        searchText: string;\n    }[]\n> {\n    if (!dynamicConfig.memos) throw new Error(\"Memos config not found\");\n    const { serverUrl, accessToken, pageSize = 100, visibility = \"public\", tags } = dynamicConfig.memos;\n    const baseUrl = serverUrl.replace(\u002F\\\u002F+$\u002F, \"\");\n    const url = `${baseUrl}\u002Fapi\u002Fv1\u002Fmemos?pageSize=${pageSize}`;\n    const headers: Record\u003Cstring, string> = { \"Content-Type\": \"application\u002Fjson\" };\n    if (accessToken) headers.Authorization = `Bearer ${accessToken}`;\n\n    const response = await fetch(url, { headers });\n    if (!response.ok) throw new Error(`Memos API error: HTTP ${response.status}`);\n    const data: MemosResponse = await response.json();\n    const mdProcessor = await getProcessor();\n\n    const promises = (data.memos || [])\n        .filter((memo) => visibility !== \"public\" || memo.visibility === \"PUBLIC\")\n        .filter((memo) => !tags?.length || tags.some((tag) => memo.tags?.includes(tag)))\n        .map(async (memo) => {\n            const images: { alt: string; src: string }[] = [];\n            const contentWithoutImages = memo.content.replace(\n                markdownImagePattern,\n                (_match: string, alt: string, src: string) => {\n                    images.push({ alt, src });\n                    return \"\";\n                },\n            );\n\n            if (memo.attachments) {\n                for (const att of memo.attachments) {\n                    if (att.type?.startsWith(\"image\u002F\")) {\n                        if (att.externalLink) {\n                            images.push({ alt: att.filename, src: att.externalLink });\n                        } else {\n                            images.push({\n                                alt: att.filename,\n                                src: `${baseUrl}\u002Ffile\u002F${att.name}\u002F${encodeURIComponent(att.filename)}`,\n                            });\n                        }\n                    }\n                }\n            }\n\n            const rendered = await mdProcessor.render(contentWithoutImages);\n            const id = memo.name.split(\"\u002F\").pop() || memo.name;\n\n            return {\n                id,\n                published: new Date(memo.displayTime || memo.createTime).getTime(),\n                html: rendered.code,\n                images,\n                searchText: (memo.content || \"\").replace(\u002F[#*`[\\]]\u002Fg, \"\").trim(),\n            };\n        });\n\n    return Promise.all(promises);\n}\n\nexport async function GET() {\n    try {\n        if (dynamicConfig.memos?.serverUrl) {\n            const data = await fetchFromMemos();\n            return new Response(JSON.stringify(data), {\n                headers: { \"Content-Type\": \"application\u002Fjson; charset=utf-8\" },\n            });\n        }\n    } catch (error) {\n        console.error(\"Failed to fetch from Memos:\", error);\n    }\n    return new Response(JSON.stringify([]), {\n        headers: { \"Content-Type\": \"application\u002Fjson; charset=utf-8\" },\n    });\n}\n","title=\"src\u002Fpages\u002Fapi\u002Fdynamic.json.ts\"",[41,145,142],{"__ignoreMap":97},[147,148,149],"h3",{"id":149},"关键逻辑",[151,152,153,167,177,187,197],"ul",{},[49,154,155,162,163,166],{},[156,157,158,161],"strong",{},[41,159,160],{"code":160},"GET()"," 导出"," — Astro 文件路由约定，",[41,164,165],{"code":165},"export async function GET()"," 即为 API 端点",[49,168,169,172,173,176],{},[156,170,171],{},"baseUrl 清理"," — ",[41,174,175],{"code":175},"serverUrl.replace(\u002F\\\u002F+$\u002F, \"\")"," 去除尾部斜杠，避免双斜杠",[49,178,179,182,183,186],{},[156,180,181],{},"Markdown 渲染"," — 使用 ",[41,184,185],{"code":185},"@astrojs\u002Fmarkdown-remark"," 服务端渲染",[49,188,189,192,193,196],{},[156,190,191],{},"图片提取"," — 同时处理 Markdown 行内图片和附件图片，附件图片构造 ",[41,194,195],{"code":195},"${baseUrl}\u002Ffile\u002F${name}\u002F${filename}"," URL",[49,198,199,202,203],{},[156,200,201],{},"降级"," — 任何异常捕获后返回空数组 ",[41,204,205],{"code":205},"[]",[33,207,209],{"id":208},"四动态页面集成","四、动态页面集成",[16,211,212,213,216],{},"在 ",[41,214,215],{"code":215},"src\u002Fpages\u002Fdynamic\u002Findex.astro"," 的 frontmatter 中，一行本地集合读取被替换为 Memos 优先的完整逻辑：",[79,218,226],{"className":219,"code":221,"highlights":222,"language":224,"meta":225},[220],"language-astro","---\nconst umamiCfg = analyticsConfig.umamiAnalytics;\nconst showUmamiViews = !!umamiCfg?.shareId;\nconst umamiShareId = umamiCfg?.shareId || \"\";\nconst umamiApiBase =\n\tumamiCfg?.shareApiBase ||\n\t(umamiCfg?.scriptUrl ? new URL(umamiCfg.scriptUrl).origin : \"\");\n\nconst dynamicCount = (await getCollection(\"dynamic\")).length;\n\u002F\u002F 优先从 Memos 统计动态数，否则回退到本地集合\nlet dynamicCount = 0;\nif (dynamicConfig.memos?.serverUrl) {\n    try {\n        const svr = dynamicConfig.memos.serverUrl.replace(\u002F\\\u002F+$\u002F, \"\");\n        const tok = dynamicConfig.memos.accessToken;\n        const headers: Record\u003Cstring, string> = {};\n        if (tok) headers.Authorization = `Bearer ${tok}`;\n        const r = await fetch(`${svr}\u002Fapi\u002Fv1\u002Fmemos?pageSize=1000`, { headers });\n        if (r.ok) {\n            const d = await r.json();\n            const tags = dynamicConfig.memos?.tags;\n            dynamicCount = (d.memos || []).filter(\n                (m: { visibility: string; tags?: string[] }) =>\n                    (dynamicConfig.memos?.visibility !== \"public\" ||\n                        m.visibility === \"PUBLIC\") &&\n                    (!tags?.length || tags.some((t) => m.tags?.includes(t))),\n            ).length;\n        }\n    } catch {}\n} else {\n    dynamicCount = (await getCollection(\"dynamic\")).length;\n}\n---\n",[223],9,"astro","title=\"src\u002Fpages\u002Fdynamic\u002Findex.astro\" del= ins={10-32}",[41,227,221],{"__ignoreMap":97},[16,229,230,233,234,237,238,241],{},[41,231,232],{"code":232},"del"," 为原来的代码，",[41,235,236],{"code":236},"ins"," 为 Memos 集成后的替换代码。若未配置 Memos 则走 ",[41,239,240],{"code":240},"else"," 分支回退到本地集合。",[33,243,245],{"id":244},"五相关文件","五、相关文件",[16,247,248,249],{},"配置：",[250,251,255],"a",{"href":252,"rel":253},"https:\u002F\u002Fgithub.com\u002Folinll\u002Ffirefly-blog\u002Fblob\u002Fmaster\u002Fsrc\u002Fconfig\u002FdynamicConfig.ts",[254],"nofollow","\u002Fsrc\u002Fconfig\u002FdynamicConfig.ts",[16,257,258,259],{},"类型：",[250,260,263],{"href":261,"rel":262},"https:\u002F\u002Fgithub.com\u002Folinll\u002Ffirefly-blog\u002Fblob\u002Fmaster\u002Fsrc\u002Ftypes\u002FdynamicConfig.ts",[254],"\u002Fsrc\u002Ftypes\u002FdynamicConfig.ts",[16,265,266,267],{},"API 代理：",[250,268,271],{"href":269,"rel":270},"https:\u002F\u002Fgithub.com\u002Folinll\u002Ffirefly-blog\u002Fblob\u002Fmaster\u002Fsrc\u002Fpages\u002Fapi\u002Fdynamic.json.ts",[254],"\u002Fsrc\u002Fpages\u002Fapi\u002Fdynamic.json.ts",[16,273,274,275],{},"动态页：",[250,276,279],{"href":277,"rel":278},"https:\u002F\u002Fgithub.com\u002Folinll\u002Ffirefly-blog\u002Fblob\u002Fmaster\u002Fsrc\u002Fpages\u002Fdynamic\u002Findex.astro",[254],"\u002Fsrc\u002Fpages\u002Fdynamic\u002Findex.astro",[16,281,282,283],{},"相关源码：",[250,284,287],{"href":285,"rel":286},"https:\u002F\u002Fgithub.com\u002Folinll\u002Ffirefly-blog",[254],"olinll\u002Ffirefly-blog",[33,289,291],{"id":290},"最后","🔗 最后",[16,293,294],{},"集成 Memos 后，发布动态的流程变成了：打开 Memos → 写内容 → 打标签 → 发布，博客自动同步。不再需要手动编辑文件再部署，极大提升了发布效率。",{"title":97,"searchDepth":296,"depth":296,"links":297},4,[298,300,301,305,306,307],{"id":35,"depth":299,"text":36},2,{"id":70,"depth":299,"text":71},{"id":124,"depth":299,"text":125,"children":302},[303],{"id":149,"depth":304,"text":149},3,{"id":208,"depth":299,"text":209},{"id":244,"depth":299,"text":245},{"id":290,"depth":299,"text":291},[309],"Firefly","2026-07-19 15:03:19","将 Memos 作为动态数据源，支持标签过滤和可见性控制，替代本地 content\u002Fdynamic\u002F。",false,"md","https:\u002F\u002Fimg.olinl.com\u002Ffile\u002Fpost-img\u002Fmemos-dynamic\u002Fcover.webp",{"slots":316},{},true,"\u002Fposts\u002Fmemos-dynamic",null,{"text":321,"minutes":322,"time":323,"words":324},"6 min read",5.34,320400,1068,{"title":5,"description":311},{"loc":318},"posts\u002Fposts\u002Fmemos-dynamic",[309,329,330,331,332],"博客","二开","Memos","动态","tech","2026-07-22 19:30:00","VcyRi7Sldvry1gBKGW2RqRxKwyq6rJQIPpXGaVa8IRA",[337,341],{"title":338,"path":339,"stem":340,"date":310,"type":333,"children":-1},"Firefly 魔改：FooterConfig 页脚模板定制","\u002Fposts\u002Ffooter-config","posts\u002Fposts\u002Ffooter-config",{"title":342,"path":343,"stem":344,"date":345,"type":333,"children":-1},"Jenkins 部署 Vue+Java 完整指南","\u002Fposts\u002Fjenkins-devops","posts\u002Fposts\u002Fjenkins-devops","2026-07-19 18:02:26",1788712221314]