Skip to main content

CMS

Marketing content — rich pages at /{slug}, the blog, the homepage blocks, header/footer globals, and category banners — is powered by a CMS provider selected at runtime from the CMS_PROVIDER env var.

CMS is available on Next and Vue. The Next boilerplate ships a CMS engine in lib/cms (Strapi, Prepr, Contentful); the Vue boilerplate ships one in src/lib/cms (Strapi, Prepr). The Nuxt boilerplate has no CMS subsystem, so --cms=<x> on a Nuxt scaffold prints a notice and proceeds with no CMS.

There is no adapter package to install. Every provider already ships in the boilerplate's lib/cms/providers/ (Vue: src/lib/cms/providers/). Selecting a CMS is just setting CMS_PROVIDER (the scaffolder pre-sets it) plus the provider's credentials. No code to wire, nothing to npm install.

Built-in providers​

ProviderWhat it is
strapiSelf-hosted Strapi v5 REST provider (Next, Vue). The scaffold puts the boilerplate's Strapi app, with its content types and seed script, in my-shop/cms/. Full contract — pages (typed blocks), articles, category banners, a typed global, static-slug enumeration, image-URL resolution. Configured with STRAPI_API_URL (+ optional STRAPI_API_TOKEN).
preprHosted Prepr GraphQL provider (Next, Vue). Same full contract. Introspects the Prepr environment's schema at runtime and builds its queries dynamically, so it adapts to rich vs simple content models. Authenticates with a GraphQL token embedded in the URL (PREPR_ACCESS_TOKEN) — no separate CMS URL.
contentfulHosted Contentful GraphQL provider (Next only). Same full contract. Reads via the Content Delivery API (or the Preview API for drafts); Rich Text fields are rendered to HTML. Authenticates with a Space ID + Delivery API token (CONTENTFUL_SPACE_ID + CONTENTFUL_CDA_TOKEN). SaaS-only — there is no self-hosted Contentful; the free tier suffices for a shop's marketing content.
noneNo CMS (default). Marketing slugs return 404 and the homepage falls back to its built-in static structure.

All providers implement the same CmsProvider contract (pages with typed blocks, blog articles, category banners, a typed global, static-slug enumeration, and image-URL resolution), so the shop's components are identical regardless of which backend is selected — only the CMS_PROVIDER env var and its credentials change.

How a scaffolded shop wires CMS​

create-propeller-shop --cms=<provider> does three things — and none of them install a package or touch the shop's code:

  1. Sets propeller.json -> cms.adapter to 'strapi' / 'prepr' / 'contentful' / null.
  2. In the shop's env example (.env.local.example on Next, .env.example on Vue), flips CMS_PROVIDER=none → CMS_PROVIDER=<provider> (Vue: plus the VITE_CMS_PROVIDER mirror) and uncomments that provider's credential lines so you can see exactly what to fill in.
  3. Fills my-shop/cms/: for strapi, the boilerplate's Strapi app; otherwise a README with the connect steps.

Older shops may still carry the removed cms adapter in propeller.json; doctor flags it.

To finish: copy .env.local.example → .env.local and fill in the provider credentials. To switch provider later, change CMS_PROVIDER (and keep propeller.json -> cms.adapter in sync so npm run doctor checks the right one). No re-scaffold or reinstall is needed.

Credentials per provider​

ProviderEnv vars uncommented
strapiSTRAPI_API_URL, NEXT_PUBLIC_STRAPI_API_URL (Next), STRAPI_API_TOKEN
preprPREPR_ACCESS_TOKEN
contentfulCONTENTFUL_SPACE_ID, CONTENTFUL_ENVIRONMENT, CONTENTFUL_CDA_TOKEN (optional: CONTENTFUL_CPA_TOKEN + CONTENTFUL_PREVIEW=true for drafts)

Homepage fallback​

The Propeller convention for a CMS-driven homepage: if the provider returns null for getPage('home') (or the page has no blocks), the shop renders a built-in <HomeFallback> instead. This lets a shop launch without a CMS and add one later without touching the homepage route.

Non-homepage CMS slugs return 404 when the CMS is missing or the slug isn't found — there's no generic "page-not-found" fallback.

Adding a new provider​

Providers live in the Next boilerplate at lib/cms/providers/ and are registered in the runtime switch in lib/cms/index.ts. Each implements the CmsProvider contract:

interface CmsProvider {
getPage(slug: string, options?: CmsPageOptions): Promise<CmsPage | null>;
getAllPageSlugs(): Promise<string[]>;
getGlobal(): Promise<CmsGlobal | null>;
getCategoryBanner(categoryId: string): Promise<CmsCategoryBanner | null>;
getArticles(): Promise<CmsArticle[]>;
getArticle(slug: string): Promise<CmsArticle | null>;
getAllArticleSlugs(): Promise<string[]>;
resolveImageUrl(path: string): string;
}

The bulk of the work is mapping: translating the CMS's response shape into the normalized CmsPage / CmsBlock / CmsGlobal / CmsArticle types in lib/cms/types.ts. Use lib/cms/providers/strapi.ts (REST), prepr.ts (GraphQL + runtime schema introspection), or contentful.ts (GraphQL + Rich Text → HTML) as starting points.

To surface a new provider through the scaffolder:

  1. Add it to the boilerplate's lib/cms/index.ts switch and its .env.local.example credential block.
  2. In the accelerator, extend the CmsAdapter enum in packages/cli/src/schema/propellerJson.ts, add a prompt choice and --cms help entry, add its credential lines to activateCmsProviderEnv in packages/cli/src/commands/scaffold.ts, and add a cms/README.md case in packages/cli/src/template/cmsReadme.ts.