create-profound-next, with checkout as a headless UI component and a catalog modeled entirely in the Profound CMS admin panel.create-profound-next today scaffolds a generic Next.js + Profound CMS starter. This PRD proposes a second, opinionated ecommerce template that ships a complete, deployable storefront: a Stripe-backed checkout exposed as a headless UI component, plus a product catalog (categories and items) modeled as first-class components inside the Profound CMS admin panel.
The goal is to let a developer run a single command and get a working store where a non-technical merchant can create categories, add products, set prices, and publish — with payments handled by Stripe and the entire visual layer editable in the CMS.
Building an ecommerce site on a CMS usually means stitching together a payments provider, a product data model, a checkout UI, and a content-editing experience by hand. There is no turnkey path from "scaffold" to "merchant-editable store with real payments."
npx create-profound-next --template ecommerce).| Persona | Needs | Touchpoint |
|---|---|---|
| Developer | Fast scaffold, typed components, clear Stripe wiring, control over styling. | CLI + codebase |
| Merchant / Editor | Create categories & products, set prices and images, publish — no code. | Profound CMS admin panel |
| Shopper | Browse catalog, add to cart, pay securely. | Storefront |
Two new CMS components are registered in the catch-all route's registry (see [...slug]/page.tsx) and exposed in the admin panel for merchant editing.
| Field | Type | Notes |
|---|---|---|
name | text | Display name, e.g. "Apparel" |
slug | text | URL segment, unique |
description | rich text | Optional intro copy |
heroImage | image | Optional banner |
items | reference[] | Ordered list of Item components |
| Field | Type | Notes |
|---|---|---|
name | text | Product title |
slug | text | URL segment, unique |
description | rich text | Product detail copy |
images | image[] | Gallery |
price | number | Minor units (cents) |
currency | select | ISO 4217, default usd |
stripePriceId | text | Maps to a Stripe Price; source of truth for charging |
category | reference | Parent Category |
active | boolean | Hide/show without deleting |
price is for presentation; the actual charge is always derived server-side from stripePriceId. This prevents client-tampered amounts and keeps Stripe as the pricing source of truth.Checkout ships as a headless component: it owns cart state, line-item validation, and the call to create a Stripe Checkout Session, but renders no opinionated markup. The developer supplies the UI via render props / children, so it adapts to any design.
// headless: logic + state, zero styling
const {
items, // cart line items
addItem, removeItem, updateQty,
subtotal, // derived, display-only
checkout, // () => redirects to Stripe Checkout
status, // 'idle' | 'loading' | 'redirecting' | 'error'
error,
} = useCart();
<Checkout>
{({ checkout, status }) => (
<button onClick={checkout} disabled={status === 'loading'}>
Pay
</button>
)}
</Checkout>
stripePriceId from the CMS, ignoring any client-sent price./api/stripe/webhook handler verifies checkout.session.completed and is the trusted fulfillment signal.Built on the existing base template (ParametricRoutePage, Refresher, createCmsProxy). New files added under src/templates/ecommerce/:
src/templates/ecommerce/
src/
app/
[...slug]/page.tsx # registry += { Category, Item }
cart/page.tsx # cart + headless <Checkout>
api/stripe/
checkout/route.ts # create Checkout Session
webhook/route.ts # verify + fulfill
components/
Category.tsx # CMS component
Item.tsx # CMS component
ProductGrid.tsx
lib/
stripe.ts # server Stripe client
useCart.ts # headless cart hook
next.config.mjs
_gitignore
tsconfig.json
The CLI's generatePackageJson() adds stripe and @stripe/stripe-js alongside the existing cms-renderer dependency when the ecommerce template is selected.
| Variable | Scope | Purpose |
|---|---|---|
PROFOUND_API_KEY | server | Existing — CMS access |
NEXT_PUBLIC_PROFOUND_WEBSITE_ID | client | Existing — website id |
STRIPE_SECRET_KEY | server | Create sessions, verify webhooks |
STRIPE_WEBHOOK_SECRET | server | Verify webhook signatures |
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY | client | Stripe.js redirect |
NEXT_PUBLIC_SITE_URL | client | Checkout success/cancel URLs |
| # | Requirement | Priority |
|---|---|---|
| R1 | CLI scaffolds the ecommerce template via a flag | Must |
| R2 | Category & Item registered as editable CMS components | Must |
| R3 | Headless cart hook + Checkout component | Must |
| R4 | Server-side price resolution from stripePriceId | Must |
| R5 | Signed Stripe webhook handler for fulfillment | Must |
| R6 | Category & product listing/detail pages via CMS routing | Should |
| R7 | Cart persistence across reloads (localStorage) | Should |
| R8 | Discount/promo code support | Could |
| R9 | Inventory count + sold-out state | Could |
| Item | Mitigation / Question |
|---|---|
| CMS price vs. Stripe price drift | Treat Stripe as source of truth; optional sync script to push CMS price → Stripe Price. |
| Webhook reliability in local dev | Document stripe listen / Stripe CLI in template README. |
| Headless API surface scope | Open: how much UI (e.g. a default unstyled cart) should ship as an optional example? |
| Catalog vs. Stripe Product sync | Open: auto-create Stripe Products from CMS Items, or require manual mapping in v1? |
| Phase | Scope |
|---|---|
| M1 — Data model | Category + Item components, registry wiring, listing/detail pages |
| M2 — Payments | Headless cart, checkout session route, webhook handler |
| M3 — CLI integration | --template ecommerce flag, deps in generatePackageJson() |
| M4 — Polish | README, env docs, test-mode walkthrough, example styling |