用 GitHub 充当无头 CMS:highlight.io 博客内容管道的迁移实践

发布时间:2026/9/25 17:43:11
用 GitHub 充当无头 CMS:highlight.io 博客内容管道的迁移实践 可观测性后端【免费下载链接】highlighthighlight.io: The open source, full-stack monitoring platform. Error monitoring, session replay, logging, distributed tracing, and more.项目地址https://gitcode.com/gh_mirrors/hi/highlight点击查看免费下载本文基于 highlight.io 开源仓库的真实迁移记录完整还原了 highlight.io 如何从 Hygraph 切换到 GitHub 作为博客内容管理系统Headless CMS的决策过程与工程实现。文章将围绕构建性能瓶颈、开发者体验、Markdown 文件系统读取、gray-matter 元数据解析与 next-mdx-remote 渲染等核心环节展开并结合仓库源码逐层拆解这套以 Git 仓库为内容源的静态生成管道帮助你掌握一种零 API 依赖、近乎零成本的内容发布方案。为什么要把 CMS 迁移到 GitHub 上内容管理系统CMS是用于创建、管理和托管内容的软件。对于任何需要频繁更新的站点内容CMS 都至关重要因为它提供了三个核心价值便捷的内容创建与编辑非技术人员和工程师都能以统一方式撰写内容集中的内容管理所有内容在同一处维护便于检索、归档与复用协作与工作流管理审阅、反馈、发布流程可以标准化。highlight.io 此前使用 HygraphGraphCMS托管博客内容随后迁移到 GitHub用纯 Markdown 文件 Git 工作流取代了传统的托管式 CMS。仓库中的blog-content/using-github-as-a-headless-cms.md就是这一迁移的原始记录而整个blog-content/目录详见 blog-content如今正是博客内容的数据库。构建时间瓶颈48 次 API 调用与限流延迟迁移前Highlight 的博客页面使用 Next.js 的静态站点生成SSG。在文章撰写时博客共有 48 篇文章构建时每一篇的正文都需要从 Hygraph 拉取也就是48 次独立的 GraphQL API 调用。由于 API 存在速率限制每次请求之间必须人为插入延迟文章数量越多构建时间越长且这种增长是线性的——这是一个典型的内容数量增长导致构建时长失控的场景。迁移后所有文章都写成 Markdown 文件直接存放在仓库中构建服务器改为直接读取本地文件系统不再发起任何一次 API 调用。highlight.io/shared/blog.ts中的这一行直接点明了内容源的指向export const BLOG_CONTENT_PATH path.join(process.cwd(), ../blog-content)博客内容的构建时间因此下降到约 2 秒并且随着文章数量继续增加这一时间基本保持恒定——因为从磁盘读取 100 个文件与读取 48 个文件的时间差微乎其微完全绕开了网络 I/O 与限流问题。开发者体验PR 审阅、协作与 GitHub 生态除了性能迁移带来的更大收益是开发者体验Hygraph 免费/基础版本对协作者席位有限制并非所有团队成员都能在发布前参与审阅而在 GitHub 上任何工程师都可以直接发起 PR并在预览环境中看到博客文章按照最终样式渲染后的效果再决定是否合并发布团队免费获得了 GitHub 生态能力Issues内容选题与缺陷跟踪、ActionsCI/CD 自动化、Projects看板管理形成统一的内容协作与反馈平台。这实际上是内容管理从封闭平台回归开源协作的典型模式内容即代码Content as Code。迁移方案设计从富文本 AST 到 Markdown/MDX迁移前的内容管道包含两个步骤通过 GraphQL API 从 Hygraph 拉取原始 Markdown 内容使用 Hygraph 的Rich Text Renderer按既定样式渲染内容。迁移的目标很明确直接访问并渲染仓库内的 Markdown。实现路径分为两步第一步使用 Node.js 的fs模块从内容目录读取文件。这一步很快就能跑通第二步处理渲染问题。团队发现原来的 Rich Text Renderer 只能渲染AST抽象语法树表示的内容而 AST 与 Markdown 不是同一种数据形态。与其把 Markdown 转成 AST不如彻底重建渲染管道改用next-mdx-remoteNext.js 生态中把 MDX/Markdown 字符串序列化为可渲染组件的方案。重建后的渲染调用如下MDXRemote {...source} // Raw markdown source经 serialize 处理后的 MDX 源 components{components} // Record of styled components样式化组件映射表 /其中components是一份组件映射表把 Markdown 中的标准元素段落、标题、列表、代码块、表格等替换为项目自定义的样式化组件从而保证文章排版与站点设计系统完全一致。配套的元数据方案是使用gray-matter把标题、作者信息、标签等关联数据直接写在 Markdown 文件开头的 YAML frontmatter 中构建时解析并按需渲染。这也是GitHub 即 CMS模式的标准做法——内容与元数据同文件共存随 Git 一起版本化。源码级实现拆解内容目录与文件系统读取highlight.io/shared/blog.ts中的getBlogPaths是内容发现的核心函数它递归遍历blog-content目录export const getBlogPaths async (fs_api: any, base: string | undefined): PromiseBlogPath[] { base base ?? const full_path path.join(BLOG_CONTENT_PATH, base) const read await fs_api.readdir(full_path) let paths: BlogPath[] [] for (var i 0; i read.length; i) { const file_string read[i] let total_path path.join(full_path, file_string) const file_path await fs_api.stat(total_path) if (file_string.includes(README)) continue if (file_path.isDirectory()) { paths paths.concat(await getBlogPaths(fs_api, path.join(base, file_string))) } else { // ... 解析 frontmatter 与正文构建 BlogPath } } return paths }几个值得注意的设计细节通过fs_api参数注入文件系统 API便于测试或替换实现例如用内存文件系统做单元测试自动跳过README文件避免把说明文档当博客渲染支持目录递归允许内容按子目录组织removeOrderingPrefix定义于 highlight.io/shared/doc.ts会剥掉文件名中的_排序前缀例如05_tips.md渲染时路径变成tips.md这是一种用文件名前缀控制文章顺序的轻量做法。用 gray-matter 解析 frontmatterhighlight.io/shared/doc.ts中的parseMarkdown是元数据解析的核心export const parseMarkdown (fileContents: string) { const { content, data } matter(fileContents, { delimiters: [---, ---], engines: { yaml: (s: any) yaml.load(s, { schema: yaml.JSON_SCHEMA }) as Object, }, }) // 用正则抽取正文中的所有链接用于预构建站点地图与内链检查 const regex /(.)\[(.*?)\]\((.*?)\)/g const links new Setstring( [...content.matchAll(regex)] .filter((m) m[1] ! !) // 过滤图片链接 .map((m) m[3]), ) return { content, data, links } }它使用gray-matter解析---包裹的 YAML frontmatter并顺手用正则把正文中的 Markdown 链接抽取为links集合——这个链接集合会被BlogPath.embedded_links携带供站点层面的链接管理与校验使用。以本仓库blog-content/using-github-as-a-headless-cms.md实际文件为例一篇完整博客的 frontmatter 长这样--- title: Using Github as a Headless CMS createdAt: 2023-06-01T12:00:00.000Z readingTime: 12 authorFirstName: Abhishek authorLastName: More authorTitle: Software Engineer authorTwitter: authorLinkedIn: https://www.linkedin.com/in/abhishek-more-linked/ authorGithub: https://github.com/Abhishek-More authorWebsite: https://abhishekmore.com authorPFP: https://tamuhack.org/static/th-2022/headshots/webp/abhishek.webp tags: Engineering, Developer Experience metaTitle: Using Github as a Headless CMS ---结合markdownToPost定义于 highlight.io/shared/blog.ts可以看到frontmatter 与页面渲染字段一一对应frontmatter 字段渲染用途title页面标题与列表标题必需字段缺失会直接抛错createdAt发布日期用于排序与日期显示readingTime阅读时长未提供时按 200 词/分钟估算authorFirstName/authorLastName作者展示名authorTitle/authorTwitter/authorLinkedIn/authorGithub/authorWebsite/authorPFP作者卡片信息tags文章标签逗号分隔必须命中白名单description/metaDescription/metaTitleSEO 元信息缺省时回退到正文首句或标题image/youtubeVideoId头图与视频嵌入值得注意的是tags的强校验markdownToPost会把逗号分隔的标签逐一与VALID_TAGS白名单比对不匹配就抛出Invalid tag错误。当前仓库的白名单见 highlight.io/shared/blog.ts包含All、Engineering、Frontend、Backend、Observability、OpenTelemetry、Product Updates、Developer Experience、Company。这意味着新增标签必须同步修改代码从机制上保证了内容分类的整洁可控。用 next-mdx-remote 渲染 Markdownhighlight.io/pages/blog/[slug].tsx是博客详情页的完整实现。在getStaticProps中正文被序列化为 MDX 源const mdxSource await serialize(githubPost.richcontent.markdown, { mdxOptions: { remarkPlugins: [remarkGfm], }, })这里启用了remark-gfm插件从而支持 GitHub Flavored Markdown 的表格、删除线、任务列表等扩展语法——与内容托管在 GitHub的定位完全自洽。序列化后的mdxSource连同components组件映射表一起传给MDXRemote{source ( div className{classNames(styles.blogText)} MDXRemote {...source} components{components} / /div )}components映射表同文件 highlight.io/pages/blog/[slug].tsx是一个非常值得借鉴的模式它把p、h1–h5、ul、ol、code、table等 Markdown 元素逐一重定向到项目的样式化组件。例如段落被包装进styles.blogText统一正文字体与行高多行代码块交给HighlightCodeBlock项目自研的带语法高亮的代码块组件单行代码渲染为行内code语言标注为language-hint的代码块会被渲染成Callout提示框——这实际上给了作者一种用代码块语法写提示框的轻量能力。这种组件映射表是 MDX 渲染管道的精髓正文作者只需要写 Markdown排版完全由站点组件体系接管。静态路径与静态属性构建期全量抓取getStaticPaths通过getBlogPaths枚举全部文章路径并返回fallback: blocking新文章合并后首次访问会触发按需构建export const getStaticPaths: GetStaticPaths async () { let paths: GetStaticPathsResult[paths] [] let p await getBlogPaths(fsp, ) p.forEach((path) { paths.push({ params: { slug: path.simple_path } }) }) return { paths, fallback: blocking } }getStaticProps则调用loadPostsFromGithub一次性加载全部文章该方法在 highlight.io/shared/blog.ts 中把每个文件经readMarkdownmarkdownToPost转为Post对象再按 slug 命中当前文章并随机挑选 3 篇NUM_SUGGESTED_POSTS 3作为相关文章推荐。推荐列表甚至内置了降级逻辑文章没有头图时会用/api/og/blog/{slug}动态生成一张带标题和作者的 OG 社交卡片。博客首页的排序与定时发布博客列表页 highlight.io/pages/blog/index.tsx 同样在getStaticProps中调用loadPostsFromGithub随后按postedAt时间戳倒序排序过滤掉发布时间晚于当前时刻的文章——这意味着作者可以提前把文章合并进仓库系统会自动定时发布无需任何额外的发布平台逻辑。这再次印证了GitHub 即 CMS的优雅之处发布时间就是 frontmatter 里的一个日期字段发布动作就是一次 git 合并。构建性能与缓存策略整个管道在构建期的成本极低每次构建读取的是本地磁盘上的若干 Markdown 文件进行 frontmatter 解析与 MDX 序列化没有网络往返、没有第三方 API 限流、没有内容同步任务。如原始博客所述博客内容构建时间稳定在约 2 秒级别且不随文章数量线性增长。与之配套的还有增量构建的兜底策略详情页与列表页的getStaticProps均设置了revalidate: 30 * 24 * 60 * 60即静态响应缓存 30 天之后触发增量静态再生成ISR。这意味着日常访问直接命中缓存只有内容更新后的下一次请求才会重建页面进一步摊薄了构建开销。经验总结从 Hygraph 到 GitHub 的迁移本质上是把内容管理从外部托管服务收敛到仓库内部的文件性能用文件系统读取替代 API 调用消除了限流延迟构建时间从随文章数线性增长变为近似常量约 2 秒协作内容走 Git 分支与 PR 流程全员可审阅、可预览、可回滚版本历史天然完整生态复用 Issues、Actions、Projects 与 CI/CD内容工作流与工程工作流统一成本与理念方案免费且完全契合开源项目内容开放、可派生、可贡献的定位——文章以纯 Markdown 形式随仓库公开任何人都可以 fork 并复用这套内容管道。如果你想在自建站点中复刻这套方案核心依赖链就是blog-content目录Markdown 文件→gray-matterfrontmatter 解析→next-mdx-remoteremark-gfmMDX 渲染→ Next.jsgetStaticPaths/getStaticProps静态生成。对应依赖可参考 highlight.io/package.json 中的gray-matter、next-mdx-remote、remark-gfm等条目。这套管道的所有实现细节都可以在仓库的 highlight.io/shared/blog.ts、highlight.io/shared/doc.ts 与 highlight.io/pages/blog/[slug].tsx 中直接阅读。如今仓库中的blog-content目录已积累了远超原始 48 篇的文章百余篇规模构建管道依然稳定工作——这正是这套文件系统即 CMS方案生命力的最好证明。赞分享可观测性后端【免费下载链接】highlighthighlight.io: The open source, full-stack monitoring platform. Error monitoring, session replay, logging, distributed tracing, and more.项目地址https://gitcode.com/gh_mirrors/hi/highlight点击查看免费下载相关推荐hve-notes迁移学习博客知识重用的科普内容方案hve notes迁移学习博客知识重用的科普内容方案 你还在为更换博客平台时丢失文章格式、重新配置主题而烦恼吗本文将介绍如何利用hve notesGrid桌面应用CMS前端10分钟上手VesselRuby爬虫框架快速开始教程10分钟上手VesselRuby爬虫框架快速开始教程 Vessel是一款基于Ruby的高性能Web爬虫框架专为简化数据抓取流程设计。无论是新手开发者还是有经后端Web框架揭秘Qwopus3.5-9B-v3训练pipeline从数据清洗到LoRA微调的完整流程揭秘Qwopus3.5 9B v3训练pipeline从数据清洗到LoRA微调的完整流程 想要了解如何高效训练一个推理增强型大语言模型吗Qwopus3.5上一篇Wekan 管理面板域名统计Admin Panel / People / Domains全实例邮箱域名聚合表页的实现与使用下一篇WeKan 多选模式Multi-Selection设计解析看板与 All Boards 页的统一选择交互创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询