ProductReadyProductReady

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_URLBusabase 工作区地址,例如 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-postsproductready-landing-pagesproductready-categoriesproductready-tags)。这些默认值可分别用 BUSABASE_CMS_POSTS_BASE_SLUGBUSABASE_CMS_PAGES_BASE_SLUGBUSABASE_CMS_CATEGORIES_BASE_SLUGBUSABASE_CMS_TAGS_BASE_SLUG 按部署覆盖。

CMS 读取结果会进入 Next 的 data cache,缓存 300 秒;缓存 key 按 app(以及 host/空间/Folder/Base slug)分命名空间,因此两个 app 指向同一个 Busabase 空间时也绝不会共用缓存条目。需要提前失效时, 可用 productready:cms-postsproductready:cms-pagesproductready:cms-categoriesproductready: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.tsresolveBlogPage);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.tssrc/domains/marketing/components/busabase-cms-page.tsx —— 解析并渲染 单个 CMS 落地页,在 dangerouslySetInnerHTML 之前对 HTML 进行消毒。

完整 SDK 参考

本文只覆盖 ProductReady 这一侧的集成范围。关于 busabase-cms 的完整 API——createBusabaseCms、 schema 自动建表、schemaProfile、缓存辅助函数,以及 Fumadocs 渲染辅助函数(SafeMarkdownsanitizeLandingPageHtml)——请参阅 monorepo 中的 packages/busabase-cms/README.md

On this page