CMS integration
A Propeller storefront does not need a CMS: the commerce pages run entirely on the SDK. When you want editorial content (a home page, landing pages, a blog), you connect a headless CMS. The integration keeps rendering and fetching content apart, so you can change CMS without touching your shop.
Checked against propeller-v2-core-ui 0.13.0, propeller-v2-cms-react and propeller-v2-cms-vue 0.1.5 and the Next.js boilerplate of 18 October 2026.
Who owns the page
Decide first who owns the page URLs. In a commerce-led setup your storefront owns the URLs and pulls content in from the CMS, as the Accelerator boilerplates do. In a CMS-led setup the CMS owns the pages and editors place Propeller components in them as blocks. See Who owns the page for both shapes.
The layers
| Layer | Where it lives | Role |
|---|---|---|
| Contracts | @propeller-commerce/propeller-v2-core-ui | Framework-free types for pages, blocks, menus and globals, plus two provider interfaces: CmsAdapter and CmsProvider (see below). |
| Renderers | @propeller-commerce/propeller-v2-cms-react and @propeller-commerce/propeller-v2-cms-vue | Render a page's blocks and make a CmsAdapter available to your tree: CmsAdapterProvider, useCms, CmsPageRenderer and CmsBlock. They ship no blocks of their own. |
| Provider | Your code, or the Next.js boilerplate's lib/cms | Fetches content from one specific CMS and converts it to the contract types. |
| Blocks | Your components | Brand-styled components you register per block type. |
The renderer only renders and the provider only fetches. Swap the CMS by swapping the provider: your renderers and blocks stay the same.
The two interfaces
The core layer defines two provider interfaces. They are different interfaces, not a small and a large version of one:
CmsAdapterhas three methods:getPage(slug),getMenu(name)andgetGlobals(). Its pages have flat blocks (typeanddata). This is what the renderers'CmsAdapterProvideranduseCmstake.CmsProvideris the interface the Next.js boilerplate'slib/cmsproviders implement. ItsgetPagereturns a page with typed blocks (_typeplus the block's fields), and it addsgetAllPageSlugs,getGlobal,getCategoryBanner,getArticles,getArticle,getAllArticleSlugsandresolveImageUrl, withgetMenuoptional.
CmsPageRenderer renders a page of either shape: it reads the block type from _type, or from type when there is none.
How rendering works
The renderer passes each block to a render function you supply in a renderers map, keyed by block type:
// React
import { CmsPageRenderer } from '@propeller-commerce/propeller-v2-cms-react';
import { HeroBlock, RichTextBlock, ProductCarouselBlock } from './cms-blocks';
export function CmsPage({ page }) {
return (
<CmsPageRenderer
page={page}
renderers={{
hero: (block) => <HeroBlock data={block.data} />,
'rich-text': (block) => <RichTextBlock data={block.data} />,
'product-carousel': (block) => <ProductCarouselBlock data={block.data} />,
}}
/>
);
}
CmsBlock renders a single block the same way. Block types without a renderer render nothing, or a debug box when you pass debug. Mount CmsAdapterProvider once at the root so client components can reach the adapter with useCms(). Server-side data fetching calls the adapter directly and passes the page into the view.
Bridge blocks
A bridge block combines editorial content with commerce data. An editor sets a category and a banner image in the CMS, and the block fetches the category name and its products through the SDK, then renders both together. Bridge blocks are your own components, registered in the same renderers map. They are where CMS content and Propeller data meet.
Writing a provider for another CMS
Which interface you implement depends on where you plug in:
- Into the renderers (
CmsAdapterProvideranduseCms): implementCmsAdapter. - Into the Next.js boilerplate's
lib/cms: implementCmsProvider, add it to the provider switch inlib/cmsand select it withCMS_PROVIDER.
Either way your pages and blocks depend only on the contract types, never on the CMS's own response shape, so supporting another CMS means writing one provider.
With the Accelerator
The CLI sets up a CMS for Next.js shops. Scaffold with --cms=strapi, --cms=prepr or --cms=contentful, and the matching provider in lib/cms is selected at runtime by CMS_PROVIDER, with a catch-all route for content pages. The settings example lists each provider's settings, and the generated cms/README.md explains the CMS side. Without a CMS (--cms=none), the home page uses a built-in layout and content pages return 404. The commerce pages work the same either way. See Routing for how content routes resolve.
See also
- Choosing your approach for commerce-led and CMS-led setups
- Accelerator for choosing a CMS when you scaffold
- Routing for how content and commerce routes resolve
- API docs: cms-react, cms-vue, core-ui