Практически наръчник: изградете витрина на Stripe, управлявана от съдържание, върху Profound CMS — каталог, който търговецът редактира без код, два параметрични маршрута, headless количка и финализиране на покупката, хоствано от Stripe.
Готовият магазин в действие — разгледайте категория, отворете продукт, добавете в количката, финализирайте покупката.
Практически наръчник, който изгражда магазин, ориентиран към съдържанието, върху Profound CMS: каталог с продукти (категории + артикули), моделирани в CMS, страници за списъци и детайли от един набор маршрути, и финализиране на покупките, хоствано от Stripe, доставено като headless компонент.
Основата е ръчно написан Next.js плюс администраторският панел на Profound. Claude Code (чрез Profound MCP) върши тежката работа по три задачи — пълни каталога, свързва дизайн системата и пише компонентите на витрината (включително headless количката). Три части: Настройка, Изграждане, Продукция.
Плащания в един ред. Използваме финализиране на покупка, хоствано от Stripe: клиентът плаща на страницата на Stripe, не на вашата. Вашето приложение прави само две сървърни действия — създава сесия за финализиране на покупката и проверява един webhook. Няма полета за карта, няма Stripe Elements, няма тежест за PCI.
/products/{item_code}, /categories/{category_code}) плюс статичен /cart, всички от един набор компоненти.useCart) и финализиране на покупка, хоствано от Stripe, като цената винаги се изчислява на сървъра от Stripe Price ID.curl -fsSL https://bun.sh/install | bashstripe login) за локални уебхукове.gh) и акаунт във Vercel, свързан с GitHub.Profound разделя съдържанието от рендерирането:
category, item); такъв, маркиран като UI Element, може да се постави на страница (nav, product_grid, …).meta.params.* в CEL, routeParams в React).cms-renderer; Stripe се добавя като обикновени API маршрути.Единственото правило, което оформя изграждането: CEL свързва само string/number полета. Така че скаларният хром (надпис на навигацията, футър, заглавия) се свързва с CEL, докато всичко богато или колекция (мрежа от продукти, галерия от изображения, богато форматиран текст) се зарежда вътре в React компонента по параметър на маршрута. И Stripe е източникът на истина за ценообразуването — price в CMS е само за показване; начислената сума винаги се изчислява на сървъра от Stripe Price ID.
Краен резултат: малък публикуван каталог, приложението свързано да го чете, Stripe инсталиран, дизайн поставен — още нищо не се рендерира.
Регистрирайте се в Profound (WorkOS удостоверяване). Създайте сайт с име store, след това копирайте неговото ID на сайта (UUID в URL адреса на администрацията) и API ключ от ниво read (Deployments → Create API key). Приложението само чете; по-късната сеитба на каталога минава през MCP, който се удостоверява отделно.
bunx create-profound-next store
cd store
bun add stripe
Скелетът е проект на Next.js App Router, предварително свързан за Profound (cms-renderer SDK, route catch-all, скрипт generate-schemas, <Refresher>). Не доставя стилове. bun add stripe добавя сървърния SDK — единствената зависимост за плащания, нужна за хостваното финализиране.
Добавете стойностите си в .env.local:
# CMS
PROFOUND_API_KEY=<вашият read ключ>
NEXT_PUBLIC_PROFOUND_WEBSITE_ID=<ID на вашия сайт>
NEXT_PUBLIC_CMS_API_URL=https://cms.dev.tryprofound.com
NEXT_PUBLIC_BUNNY_CDN_URL=https://cms-profound.b-cdn.net # сервира изображения, хоствани в CMS
# Stripe
STRIPE_SECRET_KEY=sk_test_... # тестов ключ тук; сменете с живия ключ, когато излизате онлайн
STRIPE_WEBHOOK_SECRET=whsec_... # попълва се в стъпка 4 от Изграждане
NEXT_PUBLIC_SITE_URL=http://localhost:3000
Вземете STRIPE_SECRET_KEY от Stripe → Developers → API keys. Използваме тестов ключ (sk_test_…), така че изграждането да не движи реални средства; сменете с живия, когато сте готови да приемате реални плащания. Стартирайте bun dev и отворете localhost:3000 — стартерът се визуализира.
Хостваното финализиране пренасочва браузъра към URL на Stripe, така че сървърният таен ключ е всичко, което Stripe изисква — няма публикуем ключ, няма клиентски Stripe SDK.
category, product_image и itemСъздайте три Custom Components (Components → Create new component) — източникът на данни, затова без етикет UI Element. Задайте всеки като Active.
CMS няма поле „масив от изображение“, затова галерията е масив от препратки към малък компонент product_image. Създайте category и product_image (и ги задайте като Active) преди item — поле за препратка може да сочи само към 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 (масив от препратки → product_image), price (Number, в центове — само за показване), currency (Select, usd), stripePriceId (Text), category (Reference → category), active (Boolean)Оставете всички полета незадължителни. Администраторът преобразува имената на полетата в lower-snake-case („Stripe Price Id“ → stripe_price_id) — на това ще разчитате в кода, така че прочетете реалните имена чрез generate-schemas по-долу. Наричаме маршрутизируемия дескриптор code (не slug): това е Route Slug и ключът за чисто извличане чрез documents.getByCode по-късно.
Компонентът item — code като Route Slug, images като препратки към product_image, плюс stripePriceId и препратка category.
bun run generate-schemas
Записва Zod схеми + типове в generated/cms-schemas.ts (categorySchema/Category, itemSchema/Item). Служи и като проверка на връзката — грешни идентификационни данни ще се провалят тук.
Инсталирайте и удостоверете MCP веднъж:
claude mcp add --transport http Profound http://107.21.107.99:8081/mcp
Стартирайте mcp__Profound__authenticate, завършете потока на WorkOS, след което подскажете на Claude:
Генерирай малък каталог за електронна търговия за магазин, наречен Edison's Inventions — три категории и тези продукти, със съкратено описание, точно за периода,
priceв центове,currency: "usd"и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)Всяка категория се нуждае от
nameи този малъкcode; всеки артикул се нуждае отname, същияcode,description,price(в центове),currencyиactive. Запази го вdata/catalog.jsonи валидирай спрямо нашите компонентиcategoryиitem. След това използвай Profound MCP, за да създадеш всеки като публикуван документ: създай категориите първо, запази техните ID, после създай артикулите сcategory, зададено като препратка —{ "_type": "reference", "_ref": "<category-id>", "_schema": "category" }. ОставиstripePriceIdпразно за момента. Направи артикулите паралелно.
Claude създава data/catalog.json, валидира го и стартира паралелни извиквания create_document (status: "published"). Насейте категориите преди артикулите, за да сочат препратките към вече съществуващи ID.
Трите насети категории, публикувани и Live.
Осемте насети продукта, всеки свързан с категория.
Каталогът е в CMS; сега дайте реална Stripe цена на няколко продукта — работа на търговеца, изпълнявана в две администраторски панели, без код:
price_…).stripePriceId, запазете.CMS държи каталога; Stripe държи цената; връзката е един низ, който търговецът поставя. (Предпочитате да автоматизирате? Официалният Stripe MCP може да създаде Products/Prices вместо вас — поставете върнатите ID по същия начин.)
Скелетът идва без стил. Поставете DESIGN.md (Tailwind v4 @theme блок + токени) в корена на проекта — собствен или изтеглен от refero.design. После подскажете на Claude, ограничен само до стилизиране:
Прочети дизайна, който току-що добавих. Ако трябва — настрои Tailwind, после свържи темата и шрифтовете, за да проработи стилизирането. Използвай
next/fontза шрифтове — не ги зареждай от Google в runtime. Само стилизирането — не изграждай страници или компоненти.
Уверете се, че src/app/globals.css съдържа @import "tailwindcss"; + @theme блока и че localhost:3000 показва токените. Дръжте подсказката стегната (отворена — агентът може да изгради цяла начална страница) и зареждайте шрифтовете чрез next/font, никога чрез runtime импортиране от Google.
По избор — може да достигнете работещо финализиране без изображения. За да ги добавите: създайте по един документ product_image за всяко изображение (качете файла в полето image), после свържете тези документи от масива images на продукта. Донесете свои продуктови снимки или генерирайте координиран набор с модел за изображения (накарайте Claude да изведе прецизни за марката подсказки от DESIGN.md и заключете един Midjourney --sref, за да съответства всяко изображение).
Самостоятелният
cms-rendererняма помощник за URL на изображение, затова добаветеbuildAssetUrlвsrc/lib/image.ts(~40 реда) — той добавя префиксNEXT_PUBLIC_BUNNY_CDN_URLи разширението. Компонентите в стъпка 3 от Изграждане го използват.
Изградете слоя за рендериране и финализирането на покупката, завършвайки с реална покупка в тестов режим.
Пет компонента, всеки Active и маркиран с UI Element (Settings → Tags), без Route Slug:
nav → brand · product_grid → heading · product_detail → heading · cart_summary → heading · footer → text (всички Text)Етикетът UI Element прави компонента видим в списъка Add UI Element в Page Builder — Active само по себе си не е достатъчно. Всяко поле е скаларно (тип, който CEL свързва); реалните данни за каталога не са поле тук — ProductGrid/ProductDetail ги извличат по параметър на маршрута (стъпка 3).
bun run generate-schemas
Един prompt изгражда помощника за четене, петте компонента, количката и регистъра:
Построй нашата витрина в
src/, използвайки SDKcms-rendererна Profound.
src/lib/catalog.ts— сървърен четец на CMS. Създай клиент сgetCmsClient({ cmsUrl: process.env.NEXT_PUBLIC_CMS_API_URL!, apiKey: process.env.PROFOUND_API_KEY, websiteId: process.env.NEXT_PUBLIC_PROFOUND_WEBSITE_ID! })отcms-renderer/lib/cms-api. ЕкспортирайgetItemByCode(code)→cms.documents.getByCode.query({ websiteId, schemaName: "item", code }), който връщаres.document.published_content. ЕкспортирайlistItems(categoryCode?)→cms.documents.list.query({ websiteId, schemaName: "item", status: "published", limit: 100 }), картографирайres.documentsкъм.published_content, филтрирайactive !== false, и ако е подаденcategoryCode, задръж елементите, чийтоcategory._refсъвпада сdocument.idна категорията. ЕкспортирайresolveImages(refs), който разрешава всяка препратка отitem.imagesчрезcms.documents.get.query({ websiteId, id: ref._ref })и превръща полето image в URL с въведенатаbuildAssetUrl(част 1, стъпка 8).
src/components/— пет UI елемент компонента, регистрирани в регистъра на catch-all маршрута по име на компонента, snake_case, за да съвпада с администратора:{ nav, product_grid, product_detail, cart_summary, footer }.NavиFooterчетат своето скаларно поле от пропсаcontent(типBlockComponentProps<T>отcms-renderer/lib/types).ProductGridиProductDetailса асинхронни сървърни компоненти, които четатrouteParamsи извличат данни отcatalog.ts:routeParams.<param>е{ value, … }— чети.value, така чеProductGridизвикваlistItems(routeParams.category_code?.value)(картичките водят към/products/{code}), аProductDetailизвикваgetItemByCode(routeParams.item_code?.value)(галерия чрезresolveImages, богат текст за описанието, цена, бутон Add-to-cart).CartSummaryвизуализира количката отuseCartс бутон „Плати“. ДръжformatPriceв чист файлsrc/lib/format.ts, за да не импортират клиентските компоненти сървърнияcatalog.ts.
src/components/AddToCartButton.tsx—"use client"бутон, приемащ{ code, name, priceLabel }и извикващuseCart().addItem({ code, name, priceLabel, quantity: 1 }). Използвай го вProductDetail.
src/lib/useCart.ts— headless количка: редове{ code, name, priceLabel, quantity }в състояние, запазвани вlocalStorage, предоставяaddItem/removeItem/updateQty/subtotalиcheckout(), който изпраща POST{ lines: [{ code, quantity }] }(само кодове и количества — никога цени) към/api/stripe/checkout, след което пренасочва към върнатияurl.Стилизирай всичко с нашата дизайн система, като наши собствени компоненти — не копирай оформлението на изходния сайт.
Три неща, които трябва да знаете след изпълнението:
content ({ content }: BlockComponentProps<T>) — деструктурирайте полетата като пропсове на най-горно ниво и блокът ще остане празен. Данните за каталога идват от routeParams + извличане в catalog.ts, защото CEL не може да свързва списъци или галерии. Количката носи кодове на артикули, никога цени.routeParams.<param> е { value, schemaName, document } — чете се .value. Четенето връща published_content, не .content. Ключовете в регистъра са snake_case, за да съвпадат с администратора.@types/react/@types/react-dom до v19 — скелетът идва с v18, което чупи асинхронни сървърни компоненти срещу React 19.Три кратки сървърни файла — единственият платежен код в приложението. Те използват getItemByCode, така че начислената сума се изчислява на сървъра.
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 — резолвира всеки артикул от CMS, начислява Stripe цената:
import { NextResponse } from "next/server";
import { stripe } from "@/lib/stripe";
import { getItemByCode } from "@/lib/catalog"; // сървърно, read-tier ключ
export async function POST(req: Request) {
const { lines } = await req.json(); // [{ code, quantity }] — няма цени от клиента
const line_items = await Promise.all(
lines.map(async ({ code, quantity }: { code: string; quantity: number }) => {
const item = await getItemByCode(code); // сървърът извлича от CMS
return { price: item!.stripe_price_id, quantity }; // цена от CMS, никога от клиента
})
);
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 — довереният сигнал за изпълнение:
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") {
// изпълнение: запишете поръчката / изпратете разписка.
}
return new Response(null, { status: 200 });
}
Необходима поправка на скелета:
src/proxy.tsв скелета препраща всеки/api/*към CMS, така че вашите Stripe маршрути никога не се изпълняват. Оставете ги да преминат първо: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(); // обработи локално } return cmsProxy(request as unknown as Parameters<typeof cmsProxy>[0]); } // оставете `export const config = { matcher: [...] }` от скелета непромененоПроверка:
curl -X POST localhost:3000/api/stripe/webhook -d xвръщаBad signature.
Стартирайте Stripe CLI за локални уебхукове:
stripe login
stripe listen --forward-to localhost:3000/api/stripe/webhook
# копирайте whsec_... в STRIPE_WEBHOOK_SECRET, рестартирайте bun dev
whsec_… е за всяка сесия. Две правила осигуряват сигурността: финализирането преразчита цената от CMS чрез code (манипулирана количка не може да я промени) и webhook-ът проверява подписа срещу суровото тяло.
Администратор → Pages → Create page, три пъти. Свържете всеки параметър с неговия компонент (поле за слъг code):
/products/{item_code} → item/categories/{category_code} → category/cart — статична страница (въведете буквално /cart, не /{cart})За всеки маршрут: Page Builder → Add UI Element → Custom → добавете компонентите последователно, попълнете скаларните полета (статична стойност или CEL), Publish.
/products/{item_code}: nav, product_detail, footer/categories/{category_code}: nav, product_grid, footer/cart: nav, cart_summary, footerЗадайте nav.brand и footer.text като статични низове; заглавията — като статични етикети.
Page Builder на маршрута за категории — избран елемент product_grid, заглавието му е свързано чрез CEL.
Page Builder на маршрута за продукт — елементът product_detail върху обвързан продукт.
Особеност на параметричния Page Builder: на двата параметрични маршрута добавянето на UI елементи не се запазва (блоковете остават без връзка и страницата се рендерира празна). Докато това се поправи, свържете
block_idsна тези страници директно чрез Profound MCPupdate_page, после публикувайте. (Статичният/cartсе присъединява нормално.) По същата причинаProductGridизвлича заглавието си от категорията, която зарежда, вместо чрез CEL.
/categories/lighting → мрежата. Щракнете върху продукт → детайли + Add to cart. /cart → Плати./cart?status=success, а stripe listen показва checkout.session.completed.Визуализирана продуктова страница — галерия, цена и бутон Add to cart.
Количката — редове и един бутон „Плати със Stripe“.
Само продукти с цена могат да се купуват — купете един от ~3-те, които сте оценили в стъпка 6.
По избор — интернационализирайте. Преведете всеки компонент (всички 35 езика наведнъж), добавете сегмент
/{language}/…, картографиран към вградения системен компонентlanguage, и превключете CEL-свързаните полета къмdocuments.translated. Вижте урока за директорията на летището, част 2, стъпка 7.
git init && git add -A && git commit -m "Stripe storefront"
gh repo create store --private --source=. --push # --public също е възможно
Команда за build:
generated/cms-schemas.tsе игнориран от git, така че фиксирайте build-а да го регенерира — добаветеvercel.json:{ "$schema": "https://openapi.vercel.sh/vercel.json", "buildCommand": "bun run generate-schemas && next build" }
Във Vercel: Add New → Project, импортирайте store и добавете променливите на средата — PROFOUND_API_KEY, NEXT_PUBLIC_PROFOUND_WEBSITE_ID, NEXT_PUBLIC_CMS_API_URL, NEXT_PUBLIC_BUNNY_CDN_URL, STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET (стойността за внедрената крайна точка, по-долу) и NEXT_PUBLIC_SITE_URL (вашия продакшън URL). Внедрете.
След това свържете внедрения webhook (тайната от stripe listen беше само за локално): Stripe → Developers → Webhooks → + Add endpoint → https://<prod>/api/stripe/webhook, събитие checkout.session.completed. Копирайте whsec_… във Vercel и внедрете отново.
Липсващи променливи на средата = „работи локално, празно в прод“ — номер 1 сред проблемите при внедряване. Тук внедряваме с тестови ключове; сменете STRIPE_SECRET_KEY и тайния ключ за webhook с живи стойности, когато сте готови да приемате реални плащания.
И двете идват със скелета.
<Refresher> обновява страницата, която редакторът преглежда, когато запази в администратора — без повторно внедряване. (Това е преглед за редактора; посетителите виждат публикуваното съдържание при нормална ревалидация.)?edit_mode=true към всеки URL за редакторски наслагвания. Обикновените посетители виждат чистата страница.Добавете маршрута за преглед, който скелетът пропуска. Администраторът зарежда iframe за преглед на
/cms-preview_<path>; без този маршрут всеки преглед ще е 404. Добавете го:// src/app/cms-preview_/[...slug]/page.tsx import { ParametricRoutePreviewPage } from "cms-renderer/lib/renderer"; import { registry } from "../../registry"; // изнесете регистъра в споделен модул export default async function Page({ params, searchParams }) { const { slug } = await params; const PreviewPage = ParametricRoutePreviewPage as any; // async RSC; 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} />; }Добавете и
src/app/cms-preview_/page.tsx(същото,slug: []) за корена на сегмента.
Витрина на Stripe, управлявана от съдържание: CMS каталог, страници за списъци + детайли от един набор маршрути и работещо хоствано финализиране на покупката. AI насее каталога, свърже дизайна и напише четеца на каталога + компонентите + headless количката; вие изграждате компонентите, връзките към цените в Stripe, трите маршрута, CEL хромирането и трите кратки Stripe файла. CEL свързва хрома; компонентите извличат каталога. И Stripe остана малък — едно извикване sessions.create и един подписан webhook, като клиентът плаща на страницата на Stripe.