En praktisk gjennomgang: bygg en innholdsdrevet Stripe-butikkfront pĂ„ Profound CMS â en katalog en kjĂžpmann redigerer uten kode, to parametriske ruter, en headless-handlekurv og Stripe-hostet utsjekk.
Den ferdige butikken i bevegelse â bla gjennom en kategori, Ă„pne et produkt, legg i handlekurven, gĂ„ til kassen.
En praktisk gjennomgang som bygger en innholdsdrevet butikk pÄ Profound CMS: en produkt- katalog (kategorier + varer) modellert i CMS-et, liste- og detaljsider fra ett sett med ruter, og Stripe-hostet utsjekk levert som en headless-komponent.
Ryggraden er hĂ„ndskrevet Next.js pluss Profound-admin. Claude Code (via Profound MCP) trekker det tyngste lasset pĂ„ tre oppgaver â fyller katalogen, kobler design-systemet, og skriver butikkkomponentene (inkludert den headless-handlekurven). Tre deler: Oppsett, Bygg, Produksjon.
Betalinger pĂ„ Ă©n linje. Vi bruker Stripe-hostet Checkout: kunden betaler pĂ„ Stripes side, ikke din. Appen din gjĂžr bare to ting pĂ„ serversiden â oppretter en Checkout Session og verifiserer Ă©n webhook. Ingen kortfelt, ingen Stripe Elements, ingen PCI-byrde.
/products/{item_code}, /categories/{category_code}) pluss en
statisk /cart, alt fra ett sett med komponenter.useCart) og Stripe-hostet utsjekk, med prisen alltid
avklart pÄ serveren fra en Stripe Price ID.curl -fsSL https://bun.sh/install | bashstripe login) for lokale webhooks.gh) og en Vercel-konto koblet til GitHub.Profound skiller innhold fra rendering:
category, item); én merket UI Element kan plasseres pÄ en side
(nav, product_grid, âŠ).meta.params.* i CEL, routeParams i React).cms-renderer; Stripe legges til som ordinĂŠre API-ruter.Den ene regelen som former byggingen: CEL binder kun string- og number-felt. SĂ„
skalar krom (nav-brand, bunntekst, overskrifter) bindes med CEL, mens alt som er rikt eller
samlinger (et produktgalleri, et bildegalleri, rik tekst) hentes inne i React-
komponenten via ruteparameter. Og Stripe er kilde til sannhet for prising â CMS-
price er kun for visning; belÞpet avklares alltid pÄ serversiden fra en Stripe Price ID.
Sluttresultat: en liten publisert katalog, appen tilkoblet for Ă„ lese den, Stripe installert, design pĂ„ plass â ingenting rendret ennĂ„.
Registrer deg hos Profound (WorkOS-autentisering). Opprett en nettside som heter store,
kopier deretter website ID-en (UUID-en i admin-URL-en) og en lese-nivÄ API-nÞkkel
(Deployments â Create API key). Appen leser bare; katalogseedingen skjer senere via MCP-en,
som autentiserer separat.
bunx create-profound-next store
cd store
bun add stripe
Skjelettet er et Next.js App Router-prosjekt forhÄndskablet for Profound (cms-renderer-SDK,
en catch-all-rute, et generate-schemas-skript, en <Refresher>). Det leveres uten
stiler. bun add stripe henter server-SDK-en â den eneste betalingsavhengigheten en
hostet utsjekk trenger.
Legg verdiene dine til .env.local:
# CMS
PROFOUND_API_KEY=<din lesekey>
NEXT_PUBLIC_PROFOUND_WEBSITE_ID=<din website-id>
NEXT_PUBLIC_CMS_API_URL=https://cms.dev.tryprofound.com
NEXT_PUBLIC_BUNNY_CDN_URL=https://cms-profound.b-cdn.net # leverer CMS-hostede bilder
# Stripe
STRIPE_SECRET_KEY=sk_test_... # testnÞkkel her; bytt til live-nÞkkelen nÄr du gÄr live
STRIPE_WEBHOOK_SECRET=whsec_... # fylles inn i Bygg, trinn 4
NEXT_PUBLIC_SITE_URL=http://localhost:3000
Hent STRIPE_SECRET_KEY fra Stripe â Developers â API keys. Vi bruker en testnĂžkkel
(sk_test_âŠ) slik at byggingen aldri flytter ekte penger; bytt til live-nĂžkkelen nĂ„r du er
klar for ekte betalinger. KjĂžr bun dev og Ă„pne localhost:3000 â startprosjektet rendres.
Hostet checkout videresender nettleseren til en Stripe-URL, sĂ„ serverens hemmelige nĂžkkel er alt Stripe trenger â ingen publiserbar nĂžkkel, ingen Stripe-klient-SDK.
category, product_image og itemOpprett tre Custom Components (Components â Create new component) â datakilden, sĂ„ ingen UI Element-tag. Sett hver som Active.
CMS-et har ikke et felt for «array av bilder», sÄ et galleri er en array av referanser
til en liten product_image-komponent. Opprett category og product_image (og sett dem
Active) fĂžr item â et referansefelt kan bare peke mot aktive komponenter.
category â name (Text), code (Text, Route Slug), description (Rich text), heroImage (Image)product_image â image (Image)item â name (Text), code (Text, Route Slug), description (Rich text), images (array av referanser â product_image), price (Number, Ăžre â kun visning), currency (Select, usd), stripePriceId (Text), category (Reference â category), active (Boolean)La alle felt vĂŠre valgfrie. Adminen bruker lavere snake_case pĂ„ feltnavn («Stripe Price Id» â
stripe_price_id) â det er det koden din benytter, sĂ„ les de faktiske navnene tilbake fra
generate-schemas senere. Vi kaller det rutbare hÄndtaket code (ikke slug): det er Route
Slug og nĂžkkelen for et ryddig documents.getByCode-oppslag senere.
item-komponenten â code som Route Slug, images som referanser til product_image, pluss stripePriceId og en category-referanse.
bun run generate-schemas
Skriver Zod-skjemaer + typer til generated/cms-schemas.ts (categorySchema/Category,
itemSchema/Item). Dobbeltkontroll som tilkobling â feil legitimasjon feiler her.
Installer og autentiser MCP-en én gang:
claude mcp add --transport http Profound http://107.21.107.99:8081/mcp
KjĂžr mcp__Profound__authenticate, fullfĂžr WorkOS-flyten, og be deretter Claude:
Generer en liten e-handelskatalog for en butikk kalt Edisonâs Inventions â tre kategorier og disse produktene, med en kort tidsriktig
descriptionfor hver, enpricei Ăžre,currency: "usd", ogactive: true:
- Lighting & Power (
code: lighting): Incandescent Lightbulb (incandescent-lightbulb, $24), Electric Dynamo (electric-dynamo, $890), Electric Pen (electric-pen, $49)- Sound Recording (
code: sound): Tinfoil Phonograph (tinfoil-phonograph, $249), Carbon Microphone (carbon-microphone, $59), Dictaphone (dictaphone, $179)- Motion Pictures (
code: motion): Kinetoscope (kinetoscope, $399), Kinetograph Camera (kinetograph, $549)Hver kategori trenger et
nameog den smĂ„ bokstavercode; hvert item trenger etname, den smĂ„ bokstavercode,description,price(i Ăžre),currencyogactive. Lagre det idata/catalog.jsonog valider det mot komponentene vĂ„recategoryogitem. Opprett deretter hver som et publisert dokument via Profound MCP: opprett kategoriene fĂžrst, fang ID-ene deres, opprett deretter varene medcategorysatt til en referanse â{ "_type": "reference", "_ref": "<category-id>", "_schema": "category" }. LastripePriceIdstĂ„ tom for nĂ„. GjĂžr varene parallelt.
Claude skriver data/catalog.json, validerer det, og sender parallelle create_document-
kall (status: "published"). Seed kategorier fÞr varer slik at referansene peker pÄ ID-er
som allerede finnes.
De tre seeded-kategoriene, publisert og Live.
De Ätte seeded-produktene, hver knyttet til en kategori.
Katalogen ligger i CMS-et; gi nĂ„ noen produkter en ekte Stripe-pris â kjĂžpmannens jobb, utfĂžrt i to admin-paneler, uten kode:
price_âŠ).stripePriceId, lagre.CMS-et holder katalogen; Stripe holder den autoritative prisen; forbindelsen er Ă©n streng som kjĂžpmannen limer inn. (Vil du automatisere det? Den offisielle Stripe MCP kan opprette Products/Prices for deg â lim inn ID-ene pĂ„ samme mĂ„te.)
Skjelettet leveres uten stil. Legg en DESIGN.md (en Tailwind v4 @theme-blokk + tokens) i
prosjektroten â din egen, eller last ned en fra refero.design.
Be deretter Claude, begrenset til styling:
Les designfilen jeg nettopp la til. Sett opp Tailwind hvis det trengs, og kople inn temaet og fontene slik at stylingen fungerer. Bruk
next/fontfor fonter â ikke last dem fra Google ved kjĂžretid. Bare styling â ikke bygg sider eller komponenter ennĂ„.
Kontroller at src/app/globals.css har @import "tailwindcss"; + @theme-blokken og at
localhost:3000 viser tokenene. Hold prompten stram (Äpen, sÄ skjeletter agenten hele
forsiden), og last fonter via next/font, aldri en Google-import ved kjĂžretid.
Valgfritt â du kan nĂ„ en fungerende utsjekk uten bilder. For Ă„ legge dem til: opprett et
product_image-dokument per bilde (last opp i image-feltet), og referer dem fra
produktets images-array. Bruk egne produktbilder, eller generer et helhetlig sett med en
bilde-modell (la Claude utlede merkevare-vennlige prompt fra DESIGN.md og lÄs én
Midjourney---sref slik at hvert bilde matcher).
Den frittstÄende
cms-rendererhar ingen helper for bilde-URL, sĂ„ legg innbuildAssetUrlisrc/lib/image.ts(~40 linjer) â den prefikserNEXT_PUBLIC_BUNNY_CDN_URLog legger til filendelsen. Komponentene i Bygg, trinn 3 bruker den.
Bygg rendereringslaget og utsjekken, og avslutt i et ekte kjĂžp i testmodus.
Fem komponenter, hver Active og merket UI Element (Settings â Tags), ingen Route Slug:
nav â brand · product_grid â heading · product_detail â heading ·
cart_summary â heading · footer â text (alle Text)UI Element-taggen er det som gjĂžr at en komponent vises i Page Builder-listen Add UI
Element â Active alene er ikke nok. Hvert felt er et skalarfelt (typen CEL binder);
selve katalogdataene er ikke et felt her â ProductGrid/ProductDetail henter dem via
ruteparameter (trinn 3).
bun run generate-schemas
Ăn prompt bygger lesehelperen, de fem komponentene, handlekurven og registeret:
Bygg butikken vÄr i
src/, ved Ă„ bruke Profound-cms-renderer-SDK-en.
src/lib/catalog.tsâ en server-side CMS-leser. Opprett en klient medgetCmsClient({ cmsUrl: process.env.NEXT_PUBLIC_CMS_API_URL!, apiKey: process.env.PROFOUND_API_KEY, websiteId: process.env.NEXT_PUBLIC_PROFOUND_WEBSITE_ID! })fracms-renderer/lib/cms-api. EksportergetItemByCode(code)âcms.documents.getByCode.query({ websiteId, schemaName: "item", code })som returnererres.document.published_content. EksporterlistItems(categoryCode?)âcms.documents.list.query({ websiteId, schemaName: "item", status: "published", limit: 100 }), mapres.documentstil.published_content, filtreractive !== false, og hviscategoryCodeer gitt, behold items dercategory._refer lik kategoriensdocument.id. EksporterresolveImages(refs)som lĂžser hveritem.images-referanse viacms.documents.get.query({ websiteId, id: ref._ref })og gjĂžr bilde- feltet om til en URL med den vendedebuildAssetUrl(Del 1 trinn 8).
src/components/â fem UI-element-komponenter registrert i catch-all-rutens register etter komponentnavn, snake_case for Ă„ matche admin:{ nav, product_grid, product_detail, cart_summary, footer }.NavogFooterleser skalarfeltet sitt fracontent-propen (typetBlockComponentProps<T>fracms-renderer/lib/types).ProductGridogProductDetailer asynkrone serverkomponenter som leserrouteParamsog henter fracatalog.ts:routeParams.<param>er{ value, ⊠}â les.value, sĂ„ProductGridkallerlistItems(routeParams.category_code?.value)(kortene lenker til/products/{code}) ogProductDetailkallergetItemByCode(routeParams.item_code?.value)(galleri viaresolveImages, rik-tekst-beskrivelse, pris, Legg i handlekurv).CartSummaryrendrer handlekurven frauseCartmed en Betal-knapp. HoldformatPricei en rensrc/lib/format.tsslik at klientkomponenter ikke importerer server-onlycatalog.ts.
src/components/AddToCartButton.tsxâ en"use client"-knapp som tar{ code, name, priceLabel }og kalleruseCart().addItem({ code, name, priceLabel, quantity: 1 }). Bruk den inne iProductDetail.
src/lib/useCart.tsâ en headless-handlekurv: linjeelementer{ code, name, priceLabel, quantity }i state, persistert tillocalStorage, som eksponereraddItem/removeItem/updateQty/subtotalog encheckout()som POST-er{ lines: [{ code, quantity }] }(kun koder og antall â aldri priser) til/api/stripe/checkout, og omdirigerer tilurl-en som returneres.Style alt med design-systemet vĂ„rt, som vĂ„re egne komponenter â ikke kopier kilde-nettstedets layout.
Tre ting du mÄ vite etter at det kjÞrer:
content ({ content }: BlockComponentProps<T>)
â destrukturer feltene som top-level-proper og blokken rendrer tom. Katalogdata kommer fra
routeParams + et catalog.ts-oppslag, fordi CEL ikke kan binde lister eller galleri. Handlekurven
bĂŠrer vare-koder, aldri priser.routeParams.<param> er { value, schemaName, document } â les .value. Les returnerer
published_content, ikke .content. RegisternĂžkler er snake_case for Ă„ matche admin.@types/react/@types/react-dom til v19 â skjelettet leveres med v18, som Ăždelegger
async serverkomponentblokker mot React 19.Tre korte serverfiler â den eneste betalingskoden i appen. De gjenbruker getItemByCode, sĂ„
belÞpet avklares pÄ serveren.
src/lib/stripe.ts:
import Stripe from "stripe";
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
src/app/api/stripe/checkout/route.ts â lĂžser hver vare fra CMS-et, avklarer Stripe-prisen:
import { NextResponse } from "next/server";
import { stripe } from "@/lib/stripe";
import { getItemByCode } from "@/lib/catalog"; // serverside, lesetiernĂžkkel
export async function POST(req: Request) {
const { lines } = await req.json(); // [{ code, quantity }] â ingen priser fra klienten
const line_items = await Promise.all(
lines.map(async ({ code, quantity }: { code: string; quantity: number }) => {
const item = await getItemByCode(code); // serveren lĂžser fra CMS-et
return { price: item!.stripe_price_id, quantity }; // pris fra CMS, aldri klient
})
);
const session = await stripe.checkout.sessions.create({
mode: "payment",
line_items,
success_url: `${process.env.NEXT_PUBLIC_SITE_URL}/cart?status=success`,
cancel_url: `${process.env.NEXT_PUBLIC_SITE_URL}/cart?status=cancelled`,
});
return NextResponse.json({ url: session.url }); // klienten omdirigerer hit
}
src/app/api/stripe/webhook/route.ts â det betrodde fullfĂžringssignalet:
import { stripe } from "@/lib/stripe";
export async function POST(req: Request) {
const body = await req.text(); // RĂ
kropp â nĂždvendig for signaturverifisering
const sig = req.headers.get("stripe-signature")!;
let event;
try {
event = stripe.webhooks.constructEvent(body, sig, process.env.STRIPE_WEBHOOK_SECRET!);
} catch {
return new Response("Ugyldig signatur", { status: 400 });
}
if (event.type === "checkout.session.completed") {
// fullfĂžr: registrer bestillingen / send kvittering.
}
return new Response(null, { status: 200 });
}
PÄkrevd skjelettfiks: skjelettets
src/proxy.tsvideresender alle/api/*til CMS-et, sÄ Stripe-rutene dine kjÞrer aldri. La dem passere fÞrst:import { createCmsProxy } from "cms-renderer/lib/proxy"; import { NextResponse, type NextRequest } from "next/server"; import { cmsConfig } from "@/lib/cms-config"; const cmsProxy = createCmsProxy({ upstream: cmsConfig.cmsUrl }); const LOCAL_API_PREFIXES = ["/api/stripe"]; export const proxy = async (request: NextRequest) => { if (LOCAL_API_PREFIXES.some((p) => request.nextUrl.pathname.startsWith(p))) { return NextResponse.next(); // hÄndter lokalt } return cmsProxy(request as unknown as Parameters<typeof cmsProxy>[0]); }; // behold skjelettets `export const config = { matcher: [...] }` uendretBekreft:
curl -X POST localhost:3000/api/stripe/webhook -d xreturnererUgyldig signatur.
KjĂžr Stripe CLI for lokale webhooks:
stripe login
stripe listen --forward-to localhost:3000/api/stripe/webhook
# kopier whsec_... inn i STRIPE_WEBHOOK_SECRET, start bun dev pÄ nytt
whsec_⊠er per Þkt. To regler bÊrer sikkerheten: Checkout gjenutleder prisen fra CMS-et via code
(en manipulert handlekurv kan ikke endre den), og webhooken verifiserer signaturen mot den rÄ kroppen.
Admin â Pages â Create page, tre ganger. Koble hver parameter til komponenten sin (slug-felt code):
/products/{item_code} â item/categories/{category_code} â category/cart â en statisk side (skriv inn bokstavelig /cart, ikke /{cart})For hver rute: Page Builder â Add UI Element â Custom â legg til komponenter i rekkefĂžlge, fyll skalarfeltene (statisk verdi eller CEL), Publish.
/products/{item_code}: nav, product_detail, footer/categories/{category_code}: nav, product_grid, footer/cart: nav, cart_summary, footerSett nav.brand og footer.text til statiske strenger; overskrifter til statiske etiketter.
Page Builder pĂ„ kategori-ruten â product_grid-elementet er valgt, heading bundet via CEL.
Page Builder pĂ„ produkt-ruten â product_detail-elementet pĂ„ en produktbinding.
Parametrisk Page Builder-fallgruve: pÄ de to parametriske rutene persisterer ikke UI-elementer nÄr du legger dem til (blokker blir foreldrelÞse og siden rendrer tom). Inntil det er fikset, kabler du disse sidenes
block_idsdirekte via Profound MCPupdate_page, og publiser. (Statisk/cartkobles normalt.) Av samme grunn henterProductGridheadingen sin fra kategorien den henter i stedet for via CEL.
/categories/lighting â galleriet. Klikk et produkt â detalj + Legg i handlekurv. /cart â Betal./cart?status=success, og stripe listen viser
checkout.session.completed.En rendret produktside â galleri, pris og Legg i handlekurv.
Handlekurven â linjeelementer og Ă©n Betal med Stripe-knapp.
Bare prisede varer kan kjĂžpes â kjĂžp en av de ~3 du satte pris pĂ„ i trinn 6.
Valgfritt â internasjonaliser. Oversett hver komponent (alle 35 sprĂ„k pĂ„ Ă©n gang), legg til et
/{language}/âŠ-segment kartlagt til den innebygdelanguage-System-komponenten, og bytt CEL-bundne felt tildocuments.translated. Se flyplasstutorialen, Del 2 trinn 7.
git init && git add -A && git commit -m "Stripe-butikkfront"
gh repo create store --private --source=. --push # --public er ogsÄ greit
Byggekommando:
generated/cms-schemas.tser ignorert av git, sĂ„ lĂ„s byggingen til Ă„ regenerere den â legg tilvercel.json:{ "$schema": "https://openapi.vercel.sh/vercel.json", "buildCommand": "bun run generate-schemas && next build" }
I Vercel: Add New â Project, importer store, og legg til miljĂžvariablene â PROFOUND_API_KEY,
NEXT_PUBLIC_PROFOUND_WEBSITE_ID, NEXT_PUBLIC_CMS_API_URL, NEXT_PUBLIC_BUNNY_CDN_URL,
STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET (verdien for distribusjon, under), og
NEXT_PUBLIC_SITE_URL (prod-URL-en din). Distribuer.
Koble deretter den distribuerte webhooken (den lokale stripe listen-hemmeligheten var kun lokal): Stripe
â Developers â Webhooks â + Add endpoint â https://<prod>/api/stripe/webhook, event
checkout.session.completed. Kopier whsec_⊠til Vercel og distribuer pÄ nytt.
Manglende miljĂžvariabler = «fungerer lokalt, tomt i prod» â den vanligste distribusjonsfellen. Vi
publiserer med testnĂžkler her; bytt STRIPE_SECRET_KEY og webhook-hemmeligheten til dine live-verdier
nÄr du er klar for ekte betalinger.
Begge leveres med skjelettet.
<Refresher> oppdaterer siden du forhÄndsviser nÄr en redaktÞr lagrer
i admin â ingen redistribusjon. (Det er en forhĂ„ndsvisning for redaktĂžren; besĂžkende ser publisert
innhold ved normal revalidasjon.)?edit_mode=true pÄ en URL for redigeringsoverlegg. Vanlige
besÞkende fÄr den rene siden.Legg til forhÄndsvisningsruten som skjelettet utelater. Admin laster forhÄndsvisnings-iframe-en sin pÄ
/cms-preview_<path>; uten denne ruten fÄr hver forhÄndsvisning 404. Legg den til:// src/app/cms-preview_/[...slug]/page.tsx import { ParametricRoutePreviewPage } from "cms-renderer/lib/renderer"; import { registry } from "../../registry"; // ekstraher registeret til en delt modul export default async function Page({ params, searchParams }) { const { slug } = await params; const PreviewPage = ParametricRoutePreviewPage as any; // async RSC; React 19-typer return <PreviewPage registry={registry} apiKey={process.env.PROFOUND_API_KEY ?? ""} websiteId={process.env.NEXT_PUBLIC_PROFOUND_WEBSITE_ID ?? ""} cmsUrl={process.env.NEXT_PUBLIC_CMS_API_URL ?? "https://cms.dev.tryprofound.com"} params={Promise.resolve({ slug })} searchParams={searchParams} />; }Legg ogsÄ til
src/app/cms-preview_/page.tsx(samme,slug: []) for segmentroten.
En innholdsdrevet Stripe-butikkfront: en CMS-katalog, liste- + detaljsider fra ett sett med
ruter, og en fungerende hostet utsjekk. KI seedet katalogen, koblet designet og skrev
katalogleser + komponenter + headless-handlekurv; du gjorde komponentene, Stripe-pris-koblingene,
de tre rutene, CEL-kromet og tre korte Stripe-filer. CEL binder kromet; komponenter henter
katalogen. Og Stripe forble liten â ett sessions.create-kall og Ă©n signert webhook, med
kunden som betaler pÄ Stripes egen side.