ProductReadyProductReady

Busabase CMS Integration

Pull Blog and landing page content from a Busabase workspace into ProductReady's Blog and marketing pages, alongside local MDX.

Busabase CMS Integration

busabase-cms is an npm SDK package, not a hosted system. It's a typed client library — "busabase-cms": "workspace:*" inside this monorepo, or the published busabase-cms package from npm outside it — that your own server code calls to read content from a Busabase workspace you already run or have access to. It is not a service you deploy separately.

ProductReady's Blog (/blog) and landing pages ship with local MDX content under content/blog/ by default — that keeps working with zero setup, zero config, and zero network calls. If you also want editors to publish content from a Busabase workspace — with its approval-first ChangeRequest review flow — without touching a Git PR, point ProductReady at a Busabase CMS Folder and it merges that content in.

Turning it on

Four environment variables control the integration, and setting the first three of them is what turns it on — there is no separate feature flag or admin toggle to flip.

VariablePurpose
BUSABASE_CMS_BASE_URLThe Busabase workspace host, e.g. https://busabase.com or a self-hosted URL. Required.
BUSABASE_CMS_API_KEYA Busabase API key with read (and, for first-time schema setup, write) access to the target space. Required.
BUSABASE_CMS_SPACE_IDThe target Busabase Cloud space id, sent as a request header. Required.
BUSABASE_CMS_FOLDER_IDOptional but preferred: the Busabase Folder that holds your Posts/Pages/Categories/Tags Bases.

Add these to .env (see .env.example for the exact commented-out block) and restart the dev server. The integration stays off until all three required variables are set — a partial configuration cannot reach Busabase at all, so it is treated as "off" rather than as a guaranteed-failing read on every request. BUSABASE_CMS_FOLDER_ID is the preferred setup: the first read discovers the Folder's child Bases, provisions any missing fields, and stores stable Base IDs in the Folder's metadata, so renaming a Base afterwards doesn't break anything.

Without a Folder id, ProductReady reads four app-prefixed Bases by slug (productready-blog-posts, productready-landing-pages, productready-categories, productready-tags). Each of those defaults can be overridden per deploy with BUSABASE_CMS_POSTS_BASE_SLUG, BUSABASE_CMS_PAGES_BASE_SLUG, BUSABASE_CMS_CATEGORIES_BASE_SLUG and BUSABASE_CMS_TAGS_BASE_SLUG.

CMS reads are cached in Next's data cache for 300 seconds, under a cache key namespaced per app (and per host/space/Folder/Base slug), so two apps pointed at the same Busabase space never share cache entries. Individual collections can be busted early with the productready:cms-posts, productready:cms-pages, productready:cms-categories and productready:cms-tags revalidateTag targets.

Nothing else needs to change. With every one of these variables unset (the out-of-the-box state), ProductReady never attempts a CMS network call — the Blog and landing routes read only content/blog/ and behave exactly as they did before this integration existed.

How CMS content and local MDX coexist

Both sources are read on every request and merged by canonical path (/blog/my-post, /zh-CN/blog/my-post, …):

  • If a path exists in both the CMS and local MDX, the CMS version wins — this lets you migrate a post to the CMS without deleting the MDX fallback file.
  • If a path exists in only one source, that source's content renders.
  • If the CMS integration is off (env vars unset), the CMS side of the merge is always empty, so every path resolves to local MDX — identical to today's behavior.

This applies to both the Blog list/detail routes (src/app/[lang]/blog/[[...slug]]/page.tsx) and a dedicated landing-page catch-all route (src/app/[lang]/(home)/[...cmsPath]/page.tsx) that renders CMS-authored landing pages — existing static routes (like /pricing) always take priority over that catch-all.

Cross-locale fallback for Blog posts

A Blog post detail page (/blog/my-post, /zh-CN/blog/my-post, …) resolves in this order: CMS content at the requested locale, then local MDX at the requested locale, then — only if neither exists — the English version of either source. This means a post that hasn't been translated yet still renders (in English) instead of 404ing. When that fallback happens, the page shows a small LocaleFallbackNotice banner above the title, translated for the visitor's own locale, saying the page isn't available in their language yet. This resolution lives in src/domains/marketing/logic/blog-page-resolver.ts (resolveBlogPage); the Blog index page doesn't need this, since it already lists whatever content exists at the requested locale directly.

Category and Tag archive pages

Two extra routes list Blog posts by taxonomy: /blog/category/[slug] and /blog/tag/[slug]. Categories and Tags are a CMS-only concept — local MDX posts have no equivalent — so these pages only ever show CMS-sourced posts, and listBusabaseCategoriesOrFallback / listBusabaseTagsOrFallback resolve to [] (no static pages, every request 404s) whenever the CMS integration is off, the same off-state guarantee as the rest of this integration.

Where the code lives

Following ProductReady's own domain-driven layout, the integration lives in src/domains/marketing/logic/:

  • busabase-cms-logic.ts — a thin binding that hands ProductReady's locale config, schema profile, cache namespace, and app-prefixed Base slugs (productready-blog-posts, etc.) to createCmsIntegration from busabase-cms/integration. That shared SDK factory — used by every kapps app on this SDK — owns the on/off gate (isBusabaseCmsEnabled), the cached client, and typed reads for Posts, Pages, Categories, and Tags, each with an ...OrFallback variant that degrades to an empty result on any error (network, auth, missing Folder) instead of breaking the page.
  • blog-page-resolver.ts — the Blog detail page's CMS → local MDX → English-fallback resolution described above.
  • busabase-cms-page.ts + src/domains/marketing/components/busabase-cms-page.tsx — resolve and render a single CMS landing page, sanitizing its HTML before dangerouslySetInnerHTML.

Full SDK reference

This page covers ProductReady's integration surface only. For the complete busabase-cms API — createBusabaseCms, schema provisioning, schemaProfile, caching helpers, and the Fumadocs rendering helpers (SafeMarkdown, sanitizeLandingPageHtml) — see packages/busabase-cms/README.md in the monorepo.

On this page