Praktický průvodce: postavte obsahově řízený Stripe storefront na Profound CMS — katalog, který obchodník upraví bez kódu, dvě parametrické routy, headless košík a pokladnu hostovanou na Stripe.
Hotový obchod v pohybu — procházejte kategorii, otevřete produkt, přidejte do košíku, zaplaťte.
Praktický průvodce, který na Profound CMS postaví obsahově řízený obchod: katalog produktů (kategorie + položky) namodelovaný v CMS, seznamové a detailní stránky z jedné sady rout a pokladna hostovaná na Stripe dodaná jako headless komponenta.
Základem je ručně psané Next.js plus administrace Profound. Claude Code (přes Profound MCP) odvede těžkou práci na třech úkolech — seedování katalogu, napojení design systému a napsání komponent výlohy (včetně headless košíku). Tři části: Příprava, Stavba, Produkce.
Platby jedním řádkem. Používáme Stripe-hosted Checkout: nakupující platí na stránce Stripe, ne na vaší. Vaše aplikace na serveru dělá jen dvě věci — vytvoří Checkout Session a ověří jeden webhook. Žádná pole pro kartu, žádné Stripe Elements, žádná PCI zátěž.
/products/{item_code}, /categories/{category_code}) plus statická /cart, vše z jedné sady komponent.useCart) a pokladna hostovaná na Stripe, přičemž cena se vždy vyhodnocuje na serveru ze Stripe Price ID.curl -fsSL https://bun.sh/install | bashstripe login) pro lokální webhooks.gh) a účet Vercel propojený s GitHubem.Profound odděluje obsah od renderingu:
category, item); komponenta označená jako UI Element je umístitelná na stránku (nav, product_grid, …).meta.params.* v CEL, routeParams v Reactu).cms-renderer; Stripe přidáte jako běžné API routy.Jediné pravidlo, které stavbu formuje: CEL svazuje pouze pole typu string/number. Takže skalární chrome (název v navigaci, patička, nadpisy) se váže přes CEL, zatímco cokoli bohatého nebo kolekce (maticový výpis produktů, galerie obrázků, rich text) se načítá uvnitř React komponenty podle parametru routy. A Stripe je zdroj pravdy pro ceny — pole price v CMS slouží jen k zobrazení; samotná platba se vždy určuje na serveru ze Stripe Price ID.
Cílový stav: malý publikovaný katalog, aplikace napojená na čtení, Stripe nainstalovaný, design připravený — zatím nic nerenderuje.
Zaregistrujte se v Profound (WorkOS přihlášení). Vytvořte web pojmenovaný store, poté zkopírujte jeho ID webu (UUID v URL administrace) a API klíč s read tier (Deployments → Create API key). Aplikace pouze čte; seed katalogu později proběhne přes MCP, které se autentizuje zvlášť.
bunx create-profound-next store
cd store
bun add stripe
Scaffold je projekt Next.js App Router předem připravený pro Profound (cms-renderer SDK, catch-all routa, skript generate-schemas, komponenta <Refresher>). Nedodává žádné styly. bun add stripe přidá serverové SDK — jedinou platební závislost, kterou hostovaná pokladna potřebuje.
Přidejte své hodnoty do .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 # poskytuje obrázky hostované v CMS
# Stripe
STRIPE_SECRET_KEY=sk_test_... # zde testovací klíč; při ostrém provozu jej vyměňte za live klíč
STRIPE_WEBHOOK_SECRET=whsec_... # doplníte ve 4. kroku části Build
NEXT_PUBLIC_SITE_URL=http://localhost:3000
STRIPE_SECRET_KEY získáte ve Stripe → Developers → API keys. Používáme testovací klíč (sk_test_…), aby stavba nepracovala s reálnými penězi; až budete připraveni přijímat reálné platby, přepněte na live klíč. Spusťte bun dev a otevřete localhost:3000 — starter se vykreslí.
Hostovaná pokladna přesměruje prohlížeč na URL Stripe, takže serverový tajný klíč je vše, co Stripe potřebuje — žádný publishable key, žádné klientské Stripe SDK.
category, product_image a itemVytvořte tři Custom Components (Components → Create new component) — zdroj dat, takže bez tagu UI Element. Nastavte každou jako Active.
CMS nemá pole „array of image“, proto je galerie pole referencí na malou komponentu product_image. Vytvořte category a product_image (a nastavte je na Active) předtím, než vytvoříte item — referenční pole může směřovat jen na komponenty, které jsou 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 (pole referencí → product_image), price (Number, centy — pouze pro zobrazení), currency (Select, usd), stripePriceId (Text), category (Reference → category), active (Boolean)Nechte všechna pole volitelná. Administrace převádí názvy polí na lower_snake_case („Stripe Price Id“ → stripe_price_id) — na tom staví váš kód, takže si skutečné názvy přečtěte z generate-schemas v dalším kroku. Routovací identifikátor pojmenujeme code (ne slug): je to Route Slug a klíč pro čisté vyhledání documents.getByCode později.
Komponenta item — code jako Route Slug, images jako reference na product_image, plus stripePriceId a reference category.
bun run generate-schemas
Zapíše Zod schémata + typy do generated/cms-schemas.ts (categorySchema/Category, itemSchema/Item). Zároveň funguje jako kontrola připojení — nesprávné přihlašovací údaje zde selžou.
MCP nainstalujte a autentizujte jednou:
claude mcp add --transport http Profound http://107.21.107.99:8081/mcp
Spusťte mcp__Profound__authenticate, dokončete WorkOS flow a potom instruujte Claude:
Vygeneruj malý e-commerce katalog pro obchod Edison's Inventions — tři kategorie a tyto produkty, pro každý krátký dobově přesný
description,pricev centech,currency: "usd"aactive: 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 $)Každá kategorie potřebuje
namea uvedený lowercasecode; každá položkaname, tentýž lowercasecode,description,price(v centech),currencyaactive. Ulož to dodata/catalog.jsona validuj vůči našim komponentámcategoryaitem. Poté použij Profound MCP k vytvoření každého dokumentu jako publikovaného: nejprve vytvoř kategorie, zachyť jejich ID a pak vytvoř položky scategorynastavenou na referenci —{ "_type": "reference", "_ref": "<category-id>", "_schema": "category" }.stripePriceIdzatím nech prázdné. Položky vytvoř paralelně.
Claude vytvoří data/catalog.json, zvaliduje jej a spustí paralelní volání create_document (status: "published"). Nejprve seedujte kategorie a až poté položky, aby reference mířily na existující ID.
Tři nasazené kategorie, publikované a Live.
Osm nasazených produktů, každé propojeno s kategorií.
Katalog je v CMS; nyní několika produktům přiřaďte reálnou cenu ve Stripe — úkol pro obchodníka, hotový ve dvou administracích bez kódu:
price_…).stripePriceId, uložte.CMS drží katalog; Stripe drží rozhodné ceny; spojení je jeden řetězec, který obchodník vloží. (Chcete to automatizovat? Oficiální Stripe MCP vám může vytvořit Products/Prices — vrácená ID vložte stejným způsobem.)
Scaffold je bez stylů. Vložte DESIGN.md (blok @theme z Tailwind v4 + tokeny) do kořene projektu — vlastní, nebo si jeden stáhněte z refero.design. Pak požádejte Claude, zaměřený výhradně na styly:
Přečti si designový soubor, který jsem právě přidal. Nastav Tailwind, pokud je to potřeba, a zapoj téma a fonty, aby styling fungoval. Fonty načti pomocí
next/font— ne z Google za běhu. Jen styling — zatím nestav žádné stránky ani komponenty.
Ověřte, že src/app/globals.css obsahuje @import "tailwindcss"; + blok @theme a že localhost:3000 ukazuje tokeny. Držte prompt úzký (otevřený prompt by agenta vedl ke scaffoldingu celé homepage) a fonty načítejte přes next/font, nikdy runtime importem z Google.
Volitelné — ke funkční pokladně se dostanete i bez nich. Chcete-li je přidat: vytvořte dokument product_image pro každý obrázek (nahrajte do jeho pole image), poté je referencujte z pole images u produktu. Použijte vlastní produktové snímky nebo si nechte vygenerovat konzistentní sadu modelem (Claude může z DESIGN.md odvodit prompt a uzamknout jeden Midjourney --sref, aby každý snímek ladil).
Samostatný
cms-renderernemá helper pro URL obrázků, takže si dosrc/lib/image.tsvendorujtebuildAssetUrl(~40 řádků) — přidá prefixNEXT_PUBLIC_BUNNY_CDN_URLa příponu. Komponenty v kroku 3 části Build jej používají.
Vybudujte renderovací vrstvu a pokladnu, zakončete reálným testovacím nákupem.
Pět komponent, každá Active a označená tagem UI Element (Settings → Tags), bez Route Slug:
nav → brand · product_grid → heading · product_detail → heading · cart_summary → heading · footer → text (všechno Text)Tag UI Element je to, co způsobí, že se komponenta objeví v rozbalovači Page Builderu Add UI Element — samotné Active nestačí. Každé pole je skalár (typ, který CEL svazuje); vlastní katalogová data nejsou polem zde — ProductGrid/ProductDetail je načítají podle parametru routy (krok 3).
bun run generate-schemas
Jedno zadání vytvoří pomocníka pro čtení, pět komponent, košík i registr:
Postav naši výlohu ve složce
src/s využitím SDKcms-rendererz Profound.
src/lib/catalog.ts— serverová čtečka z CMS. Vytvoř klientagetCmsClient({ cmsUrl: process.env.NEXT_PUBLIC_CMS_API_URL!, apiKey: process.env.PROFOUND_API_KEY, websiteId: process.env.NEXT_PUBLIC_PROFOUND_WEBSITE_ID! })zcms-renderer/lib/cms-api. ExportujgetItemByCode(code)→cms.documents.getByCode.query({ websiteId, schemaName: "item", code })vracejícíres.document.published_content. ExportujlistItems(categoryCode?)→cms.documents.list.query({ websiteId, schemaName: "item", status: "published", limit: 100 }), mapujres.documentsna.published_content, filtrujactive !== falsea pokud jecategoryCode, ponech položky, jejichžcategory._refse rovnádocument.iddané kategorie. ExportujresolveImages(refs), který pro každou referenciitem.imageszavolácms.documents.get.query({ websiteId, id: ref._ref })a z pole obrázku vytvoří URL pomocí vendorem přidanéhobuildAssetUrl(část 1, krok 8).
src/components/— pět komponent UI elementů registrovaných v registru catch-all routy pod názvy komponent, snake_case odpovídající administraci:{ nav, product_grid, product_detail, cart_summary, footer }.NavaFooterčtou skalární pole z propcontent(typBlockComponentProps<T>zcms-renderer/lib/types).ProductGridaProductDetailjsou asynchronní serverové komponenty, které čtourouteParamsa načítají zcatalog.ts:routeParams.<param>je{ value, … }— čti.value, takžeProductGridvolálistItems(routeParams.category_code?.value)(kartičky odkazují na/products/{code}) aProductDetailvolágetItemByCode(routeParams.item_code?.value)(galerie přesresolveImages, rich-text popis, cena, tlačítko Přidat do košíku).CartSummaryvykreslí košík zuseCarts tlačítkem Zaplatit. FunkciformatPriceponech v čistémsrc/lib/format.ts, aby klientské komponenty neimportovaly serverovýcatalog.ts.
src/components/AddToCartButton.tsx— komponenta"use client"přijímající{ code, name, priceLabel }a volajícíuseCart().addItem({ code, name, priceLabel, quantity: 1 }). Použij ji uvnitřProductDetail.
src/lib/useCart.ts— headless košík: položky{ code, name, priceLabel, quantity }ve stavu, persistované dolocalStorage, vystavujícíaddItem/removeItem/updateQty/subtotalacheckout(), který POSTne{ lines: [{ code, quantity }] }(jen kódy a množství — nikdy ceny) na/api/stripe/checkouta poté přesměruje na vrácenéurl.Vše na stylujte naším design systémem, jako vlastní komponenty — nekopírujte layout zdrojového webu.
Tři věci, které vědět po vygenerování:
content ({ content }: BlockComponentProps<T>) — pokud byste pole destrukturovali jako top-level props, blok nic nevykreslí. Katalogová data pocházejí z routeParams + fetch z catalog.ts, protože CEL neumí svázat seznamy ani galerie. Košík nese kódy položek, nikdy ceny.routeParams.<param> je { value, schemaName, document } — čtěte .value. Čtení vrací published_content, ne .content. Klíče registru jsou snake_case, aby odpovídaly administraci.@types/react/@types/react-dom na v19 — scaffold dodává v18, což rozbije async server components v Reactu 19.Tři krátké serverové soubory — jediný platební kód v aplikaci. Znovu používají getItemByCode, takže cena se určuje na serveru.
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 — načte každou položku z CMS, účtuje cenu ze Stripe:
import { NextResponse } from "next/server";
import { stripe } from "@/lib/stripe";
import { getItemByCode } from "@/lib/catalog"; // serverová čtečka, read-tier klíč
export async function POST(req: Request) {
const { lines } = await req.json(); // [{ code, quantity }] — z klienta nepřichází žádné ceny
const line_items = await Promise.all(
lines.map(async ({ code, quantity }: { code: string; quantity: number }) => {
const item = await getItemByCode(code); // server načte z CMS
return { price: item!.stripe_price_id, quantity }; // cena z CMS, nikdy z klienta
})
);
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 }); // klient se přesměruje sem
}
src/app/api/stripe/webhook/route.ts — důvěryhodný signál pro splnění objednávky:
import { stripe } from "@/lib/stripe";
export async function POST(req: Request) {
const body = await req.text(); // RAW tělo — nezbytné pro ověření podpisu
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") {
// splňte objednávku: uložte ji / odešlete účtenku.
}
return new Response(null, { status: 200 });
}
Nutná oprava scaffoldu: soubor
src/proxy.tspřesměrovává každou/api/*na CMS, takže vaše Stripe routy nedostanou žádný request. Nechte je projít nejdřív: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(); // obsluž lokálně } return cmsProxy(request as unknown as Parameters<typeof cmsProxy>[0]); }; // ponechte beze změny scaffoldem dodané `export const config = { matcher: [...] }`Ověření:
curl -X POST localhost:3000/api/stripe/webhook -d xvrátíBad signature.
Spusťte Stripe CLI pro lokální webhooks:
stripe login
stripe listen --forward-to localhost:3000/api/stripe/webhook
# zkopírujte whsec_... do STRIPE_WEBHOOK_SECRET, restartujte bun dev
whsec_… je pro každou relaci jiný. Bezpečnost stojí na dvou pravidlech: pokladna znovu odvozuje cenu z CMS podle code (manipulovaný košík ji nezmění) a webhook ověřuje podpis proti raw tělu.
Administrace → Pages → Create page, třikrát. Každý parametr mapujte na svoji komponentu (pole slug code):
/products/{item_code} → item/categories/{category_code} → category/cart — statická stránka (zadejte doslova /cart, ne /{cart})Pro každou routu: Page Builder → Add UI Element → Custom → přidejte komponenty v pořadí, vyplňte skalární pole (statickou hodnotou nebo CEL), Publish.
/products/{item_code}: nav, product_detail, footer/categories/{category_code}: nav, product_grid, footer/cart: nav, cart_summary, footerNastavte nav.brand a footer.text na statické řetězce; nadpisy na statické popisky.
Page Builder na kategorické routě — vybraný product_grid, jeho nadpis svázaný přes CEL.
Page Builder na produktové routě — product_detail napojený na produkt.
Úskalí parametrického Page Builderu: na dvou parametrických routách se přidané UI elementy neuloží (bloky osiří a stránka se vykreslí prázdná). Než to bude opravené, napojte
block_idstěchto stránek přímo přes Profound MCPupdate_page, poté publikujte. (Statická/cartse připojí standardně.) Ze stejného důvoduProductGridodvozuje nadpis z kategorie, kterou načítá, spíše než přes CEL.
/categories/lighting → mřížka. Klikněte na produkt → detail + Přidat do košíku. /cart → Zaplatit./cart?status=success a stripe listen ukáže checkout.session.completed.Vykreslená produktová stránka — galerie, cena a tlačítko Přidat do košíku.
Košík — položky a jediné tlačítko Zaplatit přes Stripe.
Zakoupit lze pouze položky s cenou — kupte jednu z ~3, které jste ocenili v kroku 6.
Volitelné — internacionalizace. Přeložte každou komponentu (všech 35 jazyků najednou), přidejte segment
/{language}/…mapovaný na vestavěnou systémovou komponentulanguagea přepněte pole svázaná přes CEL nadocuments.translated. Viz tutoriál letištního adresáře, část 2 krok 7.
git init && git add -A && git commit -m "Stripe storefront"
gh repo create store --private --source=. --push # --public je také v pořádku
Build command:
generated/cms-schemas.tsje v.gitignore, proto build připoutejte k opětovnému vygenerování — přidejtevercel.json:{ "$schema": "https://openapi.vercel.sh/vercel.json", "buildCommand": "bun run generate-schemas && next build" }
Ve Vercelu: Add New → Project, importujte store a přidejte env proměnné — PROFOUND_API_KEY, NEXT_PUBLIC_PROFOUND_WEBSITE_ID, NEXT_PUBLIC_CMS_API_URL, NEXT_PUBLIC_BUNNY_CDN_URL, STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET (hodnota pro nasazený endpoint, viz níže) a NEXT_PUBLIC_SITE_URL (vaše produkční URL). Nasazujte.
Poté napojte nasazený webhook (tajemství z stripe listen bylo jen lokální): Stripe → Developers → Webhooks → + Add endpoint → https://<prod>/api/stripe/webhook, událost checkout.session.completed. Zkopírujte whsec_… do Vercelu a redeploy.
Chybějící env proměnné = „funguje lokálně, v produkci prázdné“ — nejčastější nástraha. V tomto návodu nasazujeme s testovacími klíči; až budete připraveni na reálné platby, přepněte STRIPE_SECRET_KEY a webhook secret na live hodnoty.
Obojí je součástí scaffoldu.
<Refresher> aktualizuje stránku v náhledu, kdykoli editor v administraci uloží — bez redeploye. (Je to náhled pro editora; návštěvníci vidí publikovaný obsah podle běžné revalidace.)?edit_mode=true k libovolné URL pro editační overlay. Veřejní návštěvníci vidí čistou stránku.Přidejte náhledovou routu, kterou scaffold vynechal. Administrace načítá náhledové iframe na
/cms-preview_<path>; bez této routy každý náhled končí 404. Přidejte ji:// src/app/cms-preview_/[...slug]/page.tsx import { ParametricRoutePreviewPage } from "cms-renderer/lib/renderer"; import { registry } from "../../registry"; // vytáhněte registry do sdíleného modulu export default async function Page({ params, searchParams }) { const { slug } = await params; const PreviewPage = ParametricRoutePreviewPage as any; // async RSC; typy pro React 19 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} />; }Přidejte také
src/app/cms-preview_/page.tsx(stejné,slug: []) pro kořen segmentu.
Obsahově řízený Stripe storefront: katalog v CMS, seznamové i detailní stránky z jedné sady rout a fungující hostovaná pokladna. AI naseedovala katalog, zapojila design a napsala čtečku katalogu + komponenty + headless košík; vy jste zvládli komponenty, propojení Stripe cen, tři routy, CEL chrome a tři krátké Stripe soubory. CEL váže chrome; komponenty načítají katalog. A Stripe zůstal drobný — jedno sessions.create a jeden podepsaný webhook, přičemž nakupující platí na stránce Stripe.