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.
| Variable | Purpose |
|---|---|
BUSABASE_CMS_BASE_URL | The Busabase workspace host, e.g. https://busabase.com or a self-hosted URL. Required. |
BUSABASE_CMS_API_KEY | A Busabase API key with read (and, for first-time schema setup, write) access to the target space. Required. |
BUSABASE_CMS_SPACE_ID | The target Busabase Cloud space id, sent as a request header. Required. |
BUSABASE_CMS_FOLDER_ID | Optional 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.) tocreateCmsIntegrationfrombusabase-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...OrFallbackvariant 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 beforedangerouslySetInnerHTML.
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.