Busabase CMS 集成
让 ProductReady 的 Blog 和落地页从 Busabase 工作区拉取内容,与本地 MDX 内容共存。
Busabase CMS 集成
busabase-cms 是一个 npm SDK 包,不是托管系统。 它是一个类型化的客户端库——在本 monorepo
内使用 "busabase-cms": "workspace:*",在外部则安装已发布到 npm 的 busabase-cms 包——由你自己的
服务端代码调用,读取你已有的 Busabase 工作区内容。它不是一个需要单独部署的服务。
ProductReady 的 Blog(/blog)和落地页默认使用 content/blog/ 下的本地 MDX 内容——无需任何配置、
零网络请求即可正常工作。如果你还希望编辑者通过 Busabase 工作区(及其
approval-first 的 ChangeRequest 审核流程)发布内容,而不用提交 Git PR,只需将 ProductReady 指向一
个 Busabase CMS Folder,它就会把这部分内容合并进来。
如何开启
四个环境变量控制这项集成,其中前三个全部设置后集成才会开启——没有单独的功能开关或后台 设置项。
| 变量 | 作用 |
|---|---|
BUSABASE_CMS_BASE_URL | Busabase 工作区地址,例如 https://busabase.com 或自托管地址。必填。 |
BUSABASE_CMS_API_KEY | 拥有目标空间读取权限(首次建表还需写权限)的 Busabase API key。必填。 |
BUSABASE_CMS_SPACE_ID | 目标 Busabase Cloud 空间 id,作为请求头发送。必填。 |
BUSABASE_CMS_FOLDER_ID | 可选但推荐:存放 Posts/Pages/Categories/Tags Base 的 Busabase Folder。 |
把它们加入 .env(具体格式参考 .env.example 中已注释的代码块)并重启开发服务器即可。三个必填
变量全部设置之前,集成始终保持关闭——配置不全时根本无法访问 Busabase,因此按「关闭」处理,
而不是每次请求都发起一次注定失败的读取。推荐使用 BUSABASE_CMS_FOLDER_ID:首次读取时会自动发现
Folder 下的子 Base,补齐缺失字段,并把稳定的 Base ID 存入 Folder 的 metadata,之后重命名 Base
也不会破坏集成。
未设置 Folder id 时,ProductReady 按 slug 读取四个带应用前缀的 Base(productready-blog-posts、
productready-landing-pages、productready-categories、productready-tags)。这些默认值可分别用
BUSABASE_CMS_POSTS_BASE_SLUG、BUSABASE_CMS_PAGES_BASE_SLUG、
BUSABASE_CMS_CATEGORIES_BASE_SLUG、BUSABASE_CMS_TAGS_BASE_SLUG 按部署覆盖。
CMS 读取结果会进入 Next 的 data cache,缓存 300 秒;缓存 key 按 app(以及 host/空间/Folder/Base
slug)分命名空间,因此两个 app 指向同一个 Busabase 空间时也绝不会共用缓存条目。需要提前失效时,
可用 productready:cms-posts、productready:cms-pages、productready:cms-categories、
productready:cms-tags 这几个 revalidateTag 目标。
除此之外无需改动任何东西。 只要这些变量都未设置(开箱即用的默认状态),ProductReady 就永远
不会尝试任何 CMS 网络请求——Blog 和落地页路由只读取 content/blog/,行为与集成上线前完全一致。
CMS 内容与本地 MDX 如何共存
两个来源在每次请求时都会被读取,并按规范化路径(/blog/my-post、/zh-CN/blog/my-post 等)合并:
- 如果同一路径同时存在于 CMS 和本地 MDX 中,CMS 版本优先——这样你可以把一篇文章迁移到 CMS, 而不必删除本地 MDX 作为兜底文件。
- 如果某个路径只存在于其中一个来源,就渲染那个来源的内容。
- 如果 CMS 集成处于关闭状态(环境变量未设置),合并时 CMS 一侧始终为空,所有路径都会解析到本地 MDX——与今天的行为完全一致。
这一逻辑同时适用于 Blog 列表/详情路由(src/app/[lang]/blog/[[...slug]]/page.tsx)和一个专门的落
地页 catch-all 路由(src/app/[lang]/(home)/[...cmsPath]/page.tsx,用于渲染 CMS 撰写的落地页)——
已有的静态路由(如 /pricing)始终优先于这个 catch-all。
Blog 文章的跨语言兜底
Blog 文章详情页(/blog/my-post、/zh-CN/blog/my-post 等)按以下顺序解析:先在请求语言下找
CMS 内容,再找本地 MDX,如果两者都不存在——才会兜底到英文版本(无论来源是 CMS 还是本地
MDX)。这意味着一篇还没翻译的文章会以英文渲染,而不是直接 404。发生兜底时,页面标题上方会显示
一个小的 LocaleFallbackNotice 提示条,用访问者自己的语言告知该页面暂无对应语言版本。这套解析
逻辑位于 src/domains/marketing/logic/blog-page-resolver.ts(resolveBlogPage);Blog 列表
页不需要这个逻辑,因为它本身就是直接展示请求语言下已存在的所有内容。
Category 与 Tag 归档页
另外还有两个按分类聚合 Blog 文章的路由:/blog/category/[slug] 和 /blog/tag/[slug]。
Category 与 Tag 是只存在于 CMS 的概念——本地 MDX 文章没有对应字段——所以这两个页面只会展示
CMS 来源的文章。当 CMS 集成关闭时,listBusabaseCategoriesOrFallback /
listBusabaseTagsOrFallback 都会解析为 [](不生成任何静态页面,所有请求都会 404),与本集成
其余部分的"关闭即关闭"保证一致。
代码位置
遵循 ProductReady 自身的领域驱动设计(DDD)布局,这项集成位于 src/domains/marketing/logic/:
busabase-cms-logic.ts—— 一层很薄的绑定:把 ProductReady 自己的语言配置、schema profile、 缓存命名空间以及带应用前缀的 Base slug(如productready-blog-posts)传给busabase-cms/integration里的createCmsIntegration。所有接入本 SDK 的 kapps 应用共用那个工厂函数,它持有开关逻辑(isBusabaseCmsEnabled)、带缓存的客户端,以及针对 Posts、Pages、Categories、Tags 的类型化读取函数,每个都带有...OrFallback版本,在任何错误 (网络、鉴权、Folder 不存在)下都会优雅降级为空结果,而不是让页面崩溃。blog-page-resolver.ts—— 上文所述 Blog 详情页的 CMS → 本地 MDX → 英文兜底解析逻辑。busabase-cms-page.ts与src/domains/marketing/components/busabase-cms-page.tsx—— 解析并渲染 单个 CMS 落地页,在dangerouslySetInnerHTML之前对 HTML 进行消毒。
完整 SDK 参考
本文只覆盖 ProductReady 这一侧的集成范围。关于 busabase-cms 的完整 API——createBusabaseCms、
schema 自动建表、schemaProfile、缓存辅助函数,以及 Fumadocs 渲染辅助函数(SafeMarkdown、
sanitizeLandingPageHtml)——请参阅 monorepo 中的
packages/busabase-cms/README.md。