profound-logoProfound CMS
⌘K
Admin
Theme
DocsTutorialBlogPhilosophy
DocsTutorialBlogPhilosophy

Tutorials

Build & Ship an Airport DirectoryDeploymentsBuild & Ship a Stripe Storefront

Feature

Documentation Site TemplateFeature Template BuilderTranslation ServiceOrganizations & Website HeirarchyConnect Profound CMS to your AI clientSettings Integrationssetări-chei APISettings UsageSettings Websites
All Systems Operational
Powered Byprofound-logo
Theme

Build & Ship a Stripe Storefront

Un ghid practic: construiește un magazin Stripe bazat pe conținut în Profound CMS — un catalog pe care comerciantul îl poate edita fără cod, două rute parametrice, un coș headless și un checkout găzduit de Stripe.

Magazinul finalizat în acțiune — navighează într-o categorie, deschide un produs, adaugă-l în coș și finalizează comanda.

Un ghid practic pentru construirea unui magazin bazat pe conținut în Profound CMS: un catalog de produse (categorii și articole) modelat în CMS, pagini de listare și detalii pornind de la un singur set de rute și un checkout găzduit de Stripe, livrat ca o componentă headless.

Structura principală este un proiect Next.js scris manual, împreună cu panoul de administrare Profound. Claude Code (prin Profound MCP) se ocupă de trei sarcini — popularea catalogului, conectarea sistemului de design și scrierea componentelor magazinului (inclusiv coșul headless). Trei părți: Configurare, Construire, Producție.

Plățile într-o singură frază. Folosim Checkout găzduit de Stripe: cumpărătorul plătește pe pagina Stripe, nu pe a ta. Aplicația ta face doar două lucruri pe server — creează o sesiune Checkout și verifică un webhook. Fără câmpuri de card, fără Stripe Elements, fără sarcina conformității PCI.

Ce vei construi

  • Un catalog mic și publicat în CMS — trei categorii și opt produse (demo-ul „Edison's Inventions”) — fiecare editabil de un comerciant fără cod.
  • Două rute parametrice (/products/{item_code}, /categories/{category_code}) și un /cart static, toate pornind de la același set de componente.
  • Un coș headless (useCart) și un checkout găzduit de Stripe, cu prețul rezolvat întotdeauna pe server folosind un Stripe Price ID.
  • Magazinul implementat în Vercel, cu previzualizare live și editare directă pentru echipă.

Cerințe preliminare

  • Bun ≥ 1.3 — curl -fsSL https://bun.sh/install | bash
  • Claude Code cu Profound MCP (Partea 1 îl instalează).
  • Un cont Profound CMS.
  • Un cont Stripe. Acest tutorial rulează în modul de test, astfel încât în timpul construirii nu se încasează bani reali — fluxul este însă identic cu cheile live, așa că poți folosi cheile contului real dacă preferi. (Modul de test nu necesită date despre firmă sau bancă.)
  • Stripe CLI (stripe login) pentru webhook-uri locale.
  • Pentru implementare: GitHub CLI (gh) și un cont Vercel conectat la GitHub.

Cum se potrivesc elementele

Profound separă conținutul de randare:

  • Componentele definesc structura conținutului. O Custom Component cu un câmp Route Slug poate fi accesată prin rută (category, item); una etichetată UI Element poate fi plasată pe o pagină (nav, product_grid etc.).
  • Documentele reprezintă conținutul (un produs, o categorie).
  • Elementele UI sunt secțiuni ale paginii; fiecare câmp scalar acceptă o valoare statică sau o expresie CEL, evaluată la randare.
  • Rutele parametrice asociază un URL cu un document și elemente UI, transmițând parametrii rutei (meta.params.* în CEL, routeParams în React).
  • Aplicația Next.js citește datele prin cms-renderer; Stripe este adăugat sub forma unor rute API obișnuite.

Regula care modelează construcția: CEL conectează doar câmpuri de tip string/number. Prin urmare, elementele scalare ale interfeței (brandul navigației, subsolul, titlurile) sunt conectate cu CEL, în timp ce orice conținut bogat sau colecție (o grilă de produse, o galerie de imagini, text îmbogățit) este preluat în componenta React folosind parametrul rutei. Iar Stripe este sursa adevărului pentru prețuri — price din CMS este doar pentru afișare; suma este întotdeauna rezolvată pe server dintr-un Stripe Price ID.

Partea 1 — Configurare

Rezultatul final: un catalog mic și publicat, aplicația conectată pentru citirea lui, Stripe instalat și designul pregătit — încă nu se randează nimic.

1. Creează contul și site-ul

Înregistrează-te la Profound (autentificare WorkOS). Creează un site numit store, apoi copiază ID-ul site-ului (UUID-ul din URL-ul panoului de administrare) și o cheie API de nivel read (Deployments → Create API key). Aplicația doar citește; popularea catalogului se va face ulterior prin MCP, care se autentifică separat.

2. Creează aplicația, conecteaz-o și adaugă Stripe

bunx create-profound-next store
cd store
bun add stripe

Scaffold-ul este un proiect Next.js App Router configurat pentru Profound (SDK-ul cms-renderer, o rută catch-all, un script generate-schemas și un <Refresher>). Nu include stilizare. bun add stripe instalează SDK-ul de server — singura dependență de plăți necesară pentru checkout-ul găzduit.

Adaugă valorile în .env.local:

# CMS
PROFOUND_API_KEY=<your read key>
NEXT_PUBLIC_PROFOUND_WEBSITE_ID=<your website id>
NEXT_PUBLIC_CMS_API_URL=https://cms.dev.tryprofound.com
NEXT_PUBLIC_BUNNY_CDN_URL=https://cms-profound.b-cdn.net   # serves CMS-hosted images

# Stripe
STRIPE_SECRET_KEY=sk_test_...            # test key here; swap to your live key when you go live
STRIPE_WEBHOOK_SECRET=whsec_...          # filled in Build step 4
NEXT_PUBLIC_SITE_URL=http://localhost:3000

Obține STRIPE_SECRET_KEY din Stripe → Developers → API keys. Folosim o cheie de test (sk_test_…), astfel încât construirea să nu transfere bani reali; treci la cheia live când ești gata să accepți plăți reale. Rulează bun dev și deschide localhost:3000 — aplicația inițială se va randa.

Checkout-ul găzduit redirecționează browserul către un URL Stripe, deci cheia secretă de server este tot ce are nevoie Stripe — fără cheie publicabilă și fără SDK Stripe pentru client.

3. Definește componentele category, product_image și item

Creează trei Custom Components (Components → Create new component) — acestea sunt sursa datelor, deci nu adăuga eticheta UI Element. Setează fiecare componentă ca Active.

CMS-ul nu are un câmp „array de imagini”, astfel că o galerie este o matrice de referințe către o componentă mică product_image. Creează category și product_image (și activează-le) înainte de item — un câmp de referință poate indica doar componente active.

  • 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 de referințe → product_image), price (Number, cenți — doar pentru afișare), currency (Select, usd), stripePriceId (Text), category (Reference → category), active (Boolean)

Lasă toate câmpurile opționale. Panoul de administrare transformă numele câmpurilor în snake_case cu litere mici („Stripe Price Id” → stripe_price_id) — pe acestea se bazează codul tău, așa că verifică numele reale după rularea generate-schemas. Numim identificatorul rutabil code (nu slug): este atât Route Slug, cât și cheia pentru un apel curat documents.getByCode ulterior.

Componenta item — code ca Route Slug, images ca referințe către product_image, plus stripePriceId și o referință category.

4. Generează tipurile locale

bun run generate-schemas

Scrie schemele și tipurile Zod în generated/cms-schemas.ts (categorySchema/Category, itemSchema/Item). Comanda verifică și conexiunea — acreditările greșite vor eșua aici.

5. Populează catalogul prin Profound MCP

Instalează și autentifică MCP o singură dată:

claude mcp add --transport http Profound http://107.21.107.99:8081/mcp

Rulează mcp__Profound__authenticate, finalizează fluxul WorkOS, apoi cere-i lui Claude:

Generează un catalog e-commerce mic pentru un magazin numit Edison's Inventions — trei categorii și produsele de mai jos, fiecare cu o description scurtă și corectă pentru epocă, un price în cenți, currency: "usd" și active: 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)

Fiecare categorie are nevoie de un name și de acel code cu litere mici; fiecare articol are nevoie de name, code cu litere mici, description, price (în cenți), currency și active. Salvează datele în data/catalog.json și validează-le conform componentelor category și item. Apoi folosește Profound MCP pentru a crea fiecare element ca document publicat: creează mai întâi categoriile, salvează ID-urile lor, apoi creează produsele cu category setată ca referință — { "_type": "reference", "_ref": "<category-id>", "_schema": "category" }. Lasă stripePriceId gol pentru moment. Creează produsele în paralel.

Claude scrie data/catalog.json, îl validează și lansează apeluri paralele create_document (status: "published"). Populează categoriile înaintea produselor, astfel încât referințele să indice ID-uri deja existente.

Cele trei categorii populate, publicate și Live.

Cele opt produse populate, fiecare asociat unei categorii.

6. Creează prețuri Stripe și conectează câteva produse

Catalogul se află în CMS; acum atribuie câtorva produse un preț Stripe real — sarcina comerciantului, realizată în două panouri de administrare, fără cod:

  1. Stripe Dashboard → Products → + Add product, setează un preț unic și copiază Price ID (price_…).
  2. Fă acest lucru pentru aproximativ trei produse principale (de exemplu Lightbulb, Phonograph și Kinetoscope).
  3. Profound admin → item → Documents → lipește fiecare Price ID în stripePriceId și salvează.

CMS-ul păstrează catalogul; Stripe păstrează prețul oficial; legătura este un singur șir de caractere introdus de comerciant. (Vrei să automatizezi? Stripe MCP oficial poate crea produsele și prețurile — lipește ID-urile returnate în același mod.)

7. Adaugă sistemul de design și conectează-l cu AI

Scaffold-ul nu are stilizare. Pune un DESIGN.md (un bloc Tailwind v4 @theme și tokenuri) în rădăcina proiectului — unul creat de tine sau descărcat de la refero.design. Apoi cere-i lui Claude, limitând solicitarea la stilizare:

Citește fișierul de design pe care tocmai l-am adăugat. Configurează Tailwind dacă este necesar, apoi conectează tema și fonturile astfel încât stilizarea să funcționeze. Folosește next/font pentru fonturi — nu le încărca de la Google în timpul rulării. Doar stilizarea — nu construi încă pagini sau componente.

Verifică dacă src/app/globals.css conține @import "tailwindcss"; și blocul @theme, iar localhost:3000 afișează tokenurile. Păstrează solicitarea restrânsă și încarcă fonturile prin next/font, niciodată printr-un import Google la runtime.

8. Adaugă imagini pentru produse (opțional)

Opțional — poți ajunge la un checkout funcțional și fără imagini. Pentru a le adăuga: creează câte un document product_image pentru fiecare imagine (încarcă imaginea în câmpul image), apoi creează referințe către acestea în matricea images a produsului. Folosește fotografii proprii sau generează un set coerent cu un model de imagini.

cms-renderer standalone nu are un helper pentru URL-ul imaginilor, așa că adu buildAssetUrl în src/lib/image.ts (aproximativ 40 de linii) — acesta adaugă prefixul NEXT_PUBLIC_BUNNY_CDN_URL și extensia. Componentele din pasul 3 al secțiunii Build îl folosesc.

Partea 2 — Construire

Construiește stratul de randare și checkout-ul, ajungând la o achiziție reală în modul de test.

1. Definește cele cinci componente UI element

Cinci componente, fiecare Active și etichetată UI Element (Settings → Tags), fără Route Slug:

  • nav → brand · product_grid → heading · product_detail → heading · cart_summary → heading · footer → text (toate Text)

Eticheta UI Element face componenta să apară în lista Add UI Element din Page Builder — faptul că este activă nu este suficient. Fiecare câmp este scalar (tipul pe care îl poate conecta CEL); datele catalogului nu sunt un câmp aici — ProductGrid/ProductDetail le preiau după parametrul rutei.

2. Regenerează tipurile

bun run generate-schemas

3. Generează cititorul catalogului, componentele și coșul headless

Un singur prompt poate construi helperul de citire, cele cinci componente, coșul și registrul:

Construiește magazinul nostru în src/, folosind SDK-ul Profound cms-renderer.

src/lib/catalog.ts — un cititor CMS pe server. Creează un client cu getCmsClient({ cmsUrl: process.env.NEXT_PUBLIC_CMS_API_URL!, apiKey: process.env.PROFOUND_API_KEY, websiteId: process.env.NEXT_PUBLIC_PROFOUND_WEBSITE_ID! }) din cms-renderer/lib/cms-api. Exportă getItemByCode(code) și returnează res.document.published_content. Exportă listItems(categoryCode?), filtrează produsele active și, dacă este transmisă o categorie, păstrează produsele asociate documentului categoriei. Exportă resolveImages(refs) pentru rezolvarea referințelor din item.images și transformarea câmpului imagine într-un URL cu buildAssetUrl.

src/components/ — cele cinci componente UI element înregistrate în ruta catch-all după numele componentei, snake_case pentru a corespunde panoului de administrare: { nav, product_grid, product_detail, cart_summary, footer }. Nav și Footer citesc câmpul scalar din proprietatea content. ProductGrid și ProductDetail sunt componente server asincrone, citesc routeParams și preiau datele din catalog.ts: routeParams.<param> este { value, … }, deci citește .value. ProductGrid apelează listItems(routeParams.category_code?.value), iar ProductDetail apelează getItemByCode(routeParams.item_code?.value). CartSummary afișează coșul folosind useCart și un buton de plată.

src/components/AddToCartButton.tsx — buton client ("use client") care primește { code, name, priceLabel } și apelează useCart().addItem({ code, name, priceLabel, quantity: 1 }).

src/lib/useCart.ts — un coș headless cu elemente { code, name, priceLabel, quantity }, păstrate în localStorage, care expune addItem/removeItem/updateQty/subtotal și un checkout() ce trimite { lines: [{ code, quantity }] } (doar coduri și cantități — niciodată prețuri) către /api/stripe/checkout, apoi redirecționează către URL-ul primit.

Stilizează totul cu sistemul nostru de design, ca propriile componente — nu copia aspectul site-ului sursă.

De reținut după rulare:

  • Elementele scalare ale interfeței vin în content ({ content }: BlockComponentProps<T>). Datele catalogului vin din routeParams și dintr-un fetch în catalog.ts, deoarece CEL nu poate conecta liste sau galerii. Coșul transportă coduri, niciodată prețuri.
  • routeParams.<param> este { value, schemaName, document } — citește .value. Citirile returnează published_content, nu .content. Cheile registrului sunt snake_case.
  • Actualizează @types/react/@types/react-dom la v19 — scaffold-ul include v18, ceea ce provoacă probleme pentru componentele server asincrone cu React 19.

4. Scrie codul Stripe pentru server

Trei fișiere scurte de server — singurul cod de plată al aplicației. Ele reutilizează getItemByCode, astfel încât suma este rezolvată pe server.

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 — rezolvă fiecare produs din CMS și folosește prețul Stripe:

import { NextResponse } from "next/server";
import { stripe } from "@/lib/stripe";
import { getItemByCode } from "@/lib/catalog";

export async function POST(req: Request) {
  const { lines } = await req.json();
  const line_items = await Promise.all(
    lines.map(async ({ code, quantity }: { code: string; quantity: number }) => {
      const item = await getItemByCode(code);
      return { price: item!.stripe_price_id, quantity };
    })
  );
  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 });
}

src/app/api/stripe/webhook/route.ts — semnalul de încredere pentru îndeplinirea comenzii:

import { stripe } from "@/lib/stripe";

export async function POST(req: Request) {
  const body = await req.text();
  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("Bad signature", { status: 400 });
  }
  if (event.type === "checkout.session.completed") {
    // finalizează comanda: înregistrează comanda / trimite chitanța.
  }
  return new Response(null, { status: 200 });
}

Corecție necesară pentru scaffold: src/proxy.ts redirecționează toate rutele /api/* către CMS, astfel încât rutele Stripe nu ar rula. Permite-le mai întâi să treacă local. Verifică folosind curl -X POST localhost:3000/api/stripe/webhook -d x, care trebuie să returneze Bad signature.

Rulează Stripe CLI pentru webhook-uri locale:

stripe login
stripe listen --forward-to localhost:3000/api/stripe/webhook
# copiază whsec_... în STRIPE_WEBHOOK_SECRET și repornește bun dev

whsec_… este valabil pentru fiecare sesiune. Două reguli asigură securitatea: checkout-ul recalculează prețul din CMS după code (un coș modificat nu poate schimba prețul), iar webhook-ul verifică semnătura folosind corpul brut al cererii.

5. Creează rutele

Admin → Pages → Create page, de trei ori. Asociază fiecare parametru cu componenta sa (câmpul slug code):

  1. /products/{item_code} → item
  2. /categories/{category_code} → category
  3. /cart — pagină statică (introdu literal /cart, nu /{cart})

6. Adaugă elementele UI, conectează CEL și publică

Pentru fiecare rută: Page Builder → Add UI Element → Custom → adaugă componentele în ordine, completează câmpurile scalare (valoare statică sau CEL) și apasă Publish.

  • /products/{item_code}: nav, product_detail, footer
  • /categories/{category_code}: nav, product_grid, footer
  • /cart: nav, cart_summary, footer

Setează nav.brand și footer.text ca șiruri statice; titlurile pot fi etichete statice.

Particularitate a Page Builder pentru rute parametrice: pe cele două rute parametrice, adăugarea elementelor UI nu se salvează corect (blocurile rămân fără asociere și pagina este goală). Până la remediere, conectează direct block_ids prin Profound MCP update_page, apoi publică. Pagina statică /cart se atașează normal.

7. Randează și cumpără

  • /categories/lighting → grila. Apasă pe un produs → detalii și Add to cart. /cart → Pay.
  • Plata redirecționează către checkout-ul găzduit de Stripe. Folosește cardul de test 4242 4242 4242 4242, cu orice dată de expirare/CVC viitoare. Revii la /cart?status=success, iar stripe listen afișează checkout.session.completed.

Doar produsele cu preț sunt cumpărabile — cumpără unul dintre cele aproximativ trei produse pentru care ai creat un preț la pasul 6.

Opțional — internaționalizare. Tradu fiecare componentă (toate cele 35 de limbi simultan), adaugă un segment /{language}/… asociat componentei de sistem language și schimbă câmpurile conectate prin CEL la documents.translated. Consultă tutorialul pentru directorul aeroportului, Partea 2, pasul 7.

Partea 3 — Producție

1. Publică: GitHub, apoi Vercel

git init && git add -A && git commit -m "Stripe storefront"
gh repo create store --private --source=. --push   # --public este de asemenea în regulă

Comanda de build: generated/cms-schemas.ts este ignorat de git, deci configurează build-ul pentru a-l regenera — adaugă vercel.json:

{ "$schema": "https://openapi.vercel.sh/vercel.json", "buildCommand": "bun run generate-schemas && next build" }

În Vercel: Add New → Project, importă store și adaugă variabilele de mediu — PROFOUND_API_KEY, NEXT_PUBLIC_PROFOUND_WEBSITE_ID, NEXT_PUBLIC_CMS_API_URL, NEXT_PUBLIC_BUNNY_CDN_URL, STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET (valoarea endpointului publicat) și NEXT_PUBLIC_SITE_URL (URL-ul de producție). Implementează aplicația.

Apoi configurează webhook-ul publicat: Stripe → Developers → Webhooks → + Add endpoint → https://<prod>/api/stripe/webhook, evenimentul checkout.session.completed. Copiază whsec_… în Vercel și implementează din nou.

Variabilele de mediu lipsă înseamnă „funcționează local, este gol în producție” — cea mai frecventă problemă la implementare. Aici folosim chei de test; schimbă STRIPE_SECRET_KEY și secretul webhook-ului cu valorile live când ești gata să accepți plăți reale.

2. Previzualizare live și editare directă

Ambele funcționalități sunt incluse în scaffold.

  • Previzualizare live: <Refresher> actualizează pagina previzualizată când un editor salvează în panoul de administrare — fără redeploy. Vizitatorii văd conținutul publicat la revalidarea normală.
  • Editare directă: adaugă ?edit_mode=true la orice URL pentru suprapunerile de editare. Vizitatorii publici văd pagina normală.

Adaugă ruta de previzualizare omisă de scaffold. Panoul admin încarcă iframe-ul de previzualizare la /cms-preview_<path>; fără această rută, toate previzualizările returnează 404. Adaugă ruta și pagina pentru rădăcina segmentului, conform exemplului din scaffold.

Aceasta este construcția

Un magazin Stripe bazat pe conținut: un catalog CMS, pagini de listare și detalii pornind de la un singur set de rute și un checkout găzduit funcțional. AI a populat catalogul, a conectat designul și a scris cititorul catalogului, componentele și coșul headless; tu ai creat componentele, legăturile către prețurile Stripe, cele trei rute, elementele CEL și cele trei fișiere Stripe scurte. CEL conectează elementele interfeței; componentele preiau catalogul. Iar Stripe a rămas simplu — un apel sessions.create și un webhook semnat, cumpărătorul plătind pe pagina proprie Stripe.

Continue Reading
Previous‹Deployments