Um passo a passo prático: construa uma vitrine orientada a conteúdo com Stripe no Profound CMS — um catálogo que o lojista edita sem código, duas rotas paramétricas, um carrinho headless e checkout hospedado pela Stripe.
A loja finalizada em ação — navegue por uma categoria, abra um produto, adicione ao carrinho, finalize a compra.
Um passo a passo prático que constrói uma loja orientada a conteúdo no Profound CMS: um catálogo de produtos (categorias + itens) modelado no CMS, páginas de listagem e de detalhes a partir de um único conjunto de rotas e checkout hospedado pela Stripe entregue como componente headless.
A espinha dorsal é Next.js escrito à mão mais o painel do Profound. Claude Code (via o Profound MCP) cuida do trabalho pesado em três tarefas — semear o catálogo, conectar o sistema de design e escrever os componentes da vitrine (incluindo o carrinho headless). Três partes: Configuração, Construção, Produção.
Pagamentos em uma linha. Usamos o Checkout hospedado pela Stripe: o comprador paga na página da Stripe, não na sua. Seu app faz apenas duas coisas no servidor — cria uma sessão de Checkout e verifica um webhook. Sem campos de cartão, sem Stripe Elements, sem carga de PCI.
/products/{item_code}, /categories/{category_code}) mais um /cart estático, tudo a partir de um único conjunto de componentes.useCart) e checkout hospedado pela Stripe, com o preço sempre resolvido no servidor a partir de um Stripe Price ID.curl -fsSL https://bun.sh/install | bashstripe login) para webhooks locais.gh) e uma conta Vercel conectada ao GitHub.O Profound separa conteúdo de renderização:
category, item); um marcado como UI Element pode ser posicionado em uma página (nav, product_grid, …).meta.params.* em CEL, routeParams em React).cms-renderer; a Stripe é adicionada como rotas de API comuns.A única regra que molda a construção: CEL vincula apenas campos string/number. Portanto, o cromo escalar (marca do nav, rodapé, headings) é vinculado com CEL, enquanto qualquer coisa rica ou uma coleção (um grid de produtos, uma galeria de imagens, rich text) é buscada dentro do componente React por parâmetro de rota. E a Stripe é a fonte da verdade dos preços — o price no CMS é apenas exibido; a cobrança sempre é resolvida no servidor a partir de um Stripe Price ID.
Estado final: um pequeno catálogo publicado, o app conectado para lê-lo, Stripe instalada, design no lugar — nada renderizado ainda.
Cadastre-se no Profound (autenticação WorkOS). Crie um site chamado store, depois copie o ID do site (o UUID na URL do admin) e uma API key de leitura (Deployments → Create API key). O app apenas lê; a semente do catálogo mais adiante passa pelo MCP, que autentica separadamente.
bunx create-profound-next store
cd store
bun add stripe
O scaffold é um projeto Next.js App Router pré-conectado ao Profound (o SDK cms-renderer, uma rota catch-all, um script generate-schemas, um <Refresher>). Ele não traz estilos.
bun add stripe puxa o SDK de servidor — a única dependência de pagamentos que o checkout hospedado precisa.
Adicione seus valores ao .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 # serve as imagens hospedadas no CMS
# Stripe
STRIPE_SECRET_KEY=sk_test_... # chave de teste aqui; troque pela chave live quando for ao ar
STRIPE_WEBHOOK_SECRET=whsec_... # preenchido na etapa 4 da Construção
NEXT_PUBLIC_SITE_URL=http://localhost:3000
Pegue a STRIPE_SECRET_KEY em Stripe → Developers → API keys. Usamos uma chave de teste
(sk_test_…) para que a construção nunca mova dinheiro real; troque pela chave live quando estiver pronto para receber pagamentos reais. Execute bun dev e abra localhost:3000 — o starter renderiza.
O checkout hospedado redireciona o navegador para uma URL da Stripe, então a chave secreta do servidor é tudo o que a Stripe precisa — sem chave publicável, sem SDK cliente da Stripe.
category, product_image e itemCrie três Custom Components (Components → Create new component) — a fonte de dados, então sem tag de UI Element. Defina cada um como Active.
O CMS não tem campo "array de imagem", então uma galeria é um array de referências para um pequeno componente product_image. Crie category e product_image (e os marque como Active) antes de item — um campo de referência só aponta para componentes 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 referências → product_image), price (Number, centavos — apenas exibição), currency (Select, usd), stripePriceId (Text), category (Reference → category), active (Boolean)Deixe todos os campos opcionais. O admin converte os nomes dos campos para snake_case minúsculo ("Stripe Price Id" → stripe_price_id) — é isso que o seu código utiliza, então leia os nomes reais de volta pelo generate-schemas na sequência. Nomeamos o identificador roteável de code (não slug): é o Route Slug e a chave para um documents.getByCode limpo mais adiante.
O componente item — code como Route Slug, images como referências a product_image, além de stripePriceId e uma referência category.
bun run generate-schemas
Gera schemas Zod + tipos em generated/cms-schemas.ts (categorySchema/Category,
itemSchema/Item). Também serve como teste de conexão — credenciais erradas falham aqui.
Instale e autentique o MCP uma vez:
claude mcp add --transport http Profound http://107.21.107.99:8081/mcp
Execute mcp__Profound__authenticate, complete o fluxo WorkOS e então peça ao Claude:
Gere um pequeno catálogo de ecommerce para uma loja chamada Edison's Inventions — três categorias e estes produtos, com uma
descriptioncurta e historicamente coerente para cada um, umpriceem centavos,currency: "usd"eactive: true:
- Lighting & Power (
code: lighting): Incandescent Lightbulb (incandescent-lightbulb, US$ 24), Electric Dynamo (electric-dynamo, US$ 890), Electric Pen (electric-pen, US$ 49)- Sound Recording (
code: sound): Tinfoil Phonograph (tinfoil-phonograph, US$ 249), Carbon Microphone (carbon-microphone, US$ 59), Dictaphone (dictaphone, US$ 179)- Motion Pictures (
code: motion): Kinetoscope (kinetoscope, US$ 399), Kinetograph Camera (kinetograph, US$ 549)Cada categoria precisa de
namee daquelecodeem minúsculas; cada item precisa dename, daquelecodeem minúsculas,description,price(em centavos),currencyeactive. Salve emdata/catalog.jsone valide contra nossos componentescategoryeitem. Depois use o Profound MCP para criar cada um como documento publicado: crie as categorias primeiro, capture seus IDs e então crie os itens comcategoryconfigurado como referência —{ "_type": "reference", "_ref": "<category-id>", "_schema": "category" }. DeixestripePriceIdvazio por enquanto. Faça os itens em paralelo.
Claude escreve data/catalog.json, valida e dispara chamadas create_document paralelas (status: "published"). Semeie categorias antes dos itens para que as referências apontem para IDs já existentes.
As três categorias semeadas, publicadas e Live.
Os oito produtos semeados, cada um vinculado a uma categoria.
O catálogo está no CMS; agora dê a alguns produtos um preço real na Stripe — o trabalho do lojista, feito em dois painéis admin, sem código:
price_…).stripePriceId, salve.O CMS mantém o catálogo; a Stripe mantém o preço oficial; o vínculo é uma string que o lojista cola. (Prefere automatizar? O Stripe MCP oficial pode criar os Products/Prices para você — cole os IDs retornados da mesma forma.)
O scaffold vem sem estilos. Coloque um DESIGN.md (um bloco @theme Tailwind v4 + tokens) na raiz do projeto — pode ser seu ou baixar de refero.design.
Depois peça ao Claude, limitado a estilização:
Leia o arquivo de design que acabei de adicionar. Configure o Tailwind se necessário, depois conecte o tema e as fontes para que a estilização funcione. Use
next/fontpara as fontes — não as carregue do Google em tempo de execução. Apenas estilização — não construa páginas nem componentes ainda.
Verifique se src/app/globals.css tem @import "tailwindcss"; + o bloco @theme e se localhost:3000 mostra os tokens. Mantenha o prompt enxuto (se for aberto, um agente faz o scaffolding de uma página inteira) e carregue fontes via next/font, nunca com import do Google em runtime.
Opcional — você chega a um checkout funcional sem imagens. Para adicioná-las: crie um documento product_image por imagem (faça upload no campo image), depois referencie-os no array images do produto. Traga suas próprias fotos ou gere um conjunto coeso com um modelo de imagem (peça ao Claude para derivar prompts alinhados ao DESIGN.md e fixe um --sref no Midjourney para que cada foto combine).
O
cms-rendererstandalone não possui helper de URL de imagem, então faça vendor debuildAssetUrlemsrc/lib/image.ts(~40 linhas) — ele prefixaNEXT_PUBLIC_BUNNY_CDN_URLe adiciona a extensão. Os componentes na etapa 3 da Construção o utilizam.
Construa a camada de renderização e o checkout, terminando com uma compra real em modo de teste.
Cinco componentes, cada um Active e marcado com a tag UI Element (Settings → Tags), sem Route Slug:
nav → brand · product_grid → heading · product_detail → heading ·
cart_summary → heading · footer → text (todos Text)A tag UI Element é o que faz um componente aparecer na lista Add UI Element do Page Builder — ser Active não basta. Cada campo é escalar (o tipo que CEL vincula); os dados reais do catálogo não são um campo aqui — ProductGrid/ProductDetail os buscam por parâmetro de rota (etapa 3).
bun run generate-schemas
Um único prompt constrói o helper de leitura, os cinco componentes, o carrinho e o registry:
Construa nossa vitrine em
src/, usando o SDKcms-rendererdo Profound.
src/lib/catalog.ts— um leitor do CMS no servidor. Crie um client comgetCmsClient({ cmsUrl: process.env.NEXT_PUBLIC_CMS_API_URL!, apiKey: process.env.PROFOUND_API_KEY, websiteId: process.env.NEXT_PUBLIC_PROFOUND_WEBSITE_ID! })decms-renderer/lib/cms-api. ExportegetItemByCode(code)→cms.documents.getByCode.query({ websiteId, schemaName: "item", code })retornandores.document.published_content. ExportelistItems(categoryCode?)→cms.documents.list.query({ websiteId, schemaName: "item", status: "published", limit: 100 }), mapeieres.documentspara.published_content, filtreactive !== falsee, secategoryCodefor informado, retenha itens cujocategory._refiguale odocument.idda categoria. ExporteresolveImages(refs)que resolve cada referência deitem.imagesviacms.documents.get.query({ websiteId, id: ref._ref })e transforma o campo de imagem em uma URL com obuildAssetUrlvendorizado (Parte 1 etapa 8).
src/components/— cinco componentes de UI element registrados no registry da rota catch-all pelo nome do componente, snake_case para coincidir com o admin:{ nav, product_grid, product_detail, cart_summary, footer }.NaveFooterleem seus campos escalares da propcontent(tipadaBlockComponentProps<T>decms-renderer/lib/types).ProductGrideProductDetailsão componentes de servidor assíncronos que leemrouteParamse buscam emcatalog.ts:routeParams.<param>é{ value, … }— leia.value, portantoProductGridchamalistItems(routeParams.category_code?.value)(cards apontam para/products/{code}) eProductDetailchamagetItemByCode(routeParams.item_code?.value)(galeria viaresolveImages, descrição rich text, preço, Adicionar ao carrinho).CartSummaryrenderiza o carrinho deuseCartcom um botão Pagar. MantenhaformatPriceem um módulo purosrc/lib/format.tspara que componentes cliente não importem ocatalog.tsexclusivo do servidor.
src/components/AddToCartButton.tsx— um botão"use client"que recebe{ code, name, priceLabel }e chamauseCart().addItem({ code, name, priceLabel, quantity: 1 }). Use-o dentro deProductDetail.
src/lib/useCart.ts— um carrinho headless: itens{ code, name, priceLabel, quantity }em state, persistidos emlocalStorage, expondoaddItem/removeItem/updateQty/subtotale umcheckout()que faz POST de{ lines: [{ code, quantity }] }(apenas códigos e quantidades — nunca preços) para/api/stripe/checkout, depois redireciona para aurlretornada.Estilize tudo com nosso sistema de design, como componentes próprios — não copie o layout do site de origem.
Três coisas a saber depois que rodar:
content ({ content }: BlockComponentProps<T>) — destruture campos como props de topo e o bloco renderiza vazio. Dados do catálogo vêm de routeParams + uma busca em catalog.ts, porque CEL não vincula listas nem galerias. O carrinho carrega códigos dos itens, nunca preços.routeParams.<param> é { value, schemaName, document } — leia .value. Leituras retornam
published_content, não .content. As chaves do registry são em snake_case para coincidir com o admin.@types/react/@types/react-dom para v19 — o scaffold traz v18, o que quebra blocos de componentes de servidor assíncronos no React 19.Três arquivos curtos de servidor — o único código de pagamento no app. Eles reutilizam getItemByCode, então a cobrança é resolvida no servidor.
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 — resolve cada item no CMS, cobra o preço da Stripe:
import { NextResponse } from "next/server";
import { stripe } from "@/lib/stripe";
import { getItemByCode } from "@/lib/catalog"; // servidor, chave de leitura
export async function POST(req: Request) {
const { lines } = await req.json(); // [{ code, quantity }] — nenhum preço vindo do cliente
const line_items = await Promise.all(
lines.map(async ({ code, quantity }: { code: string; quantity: number }) => {
const item = await getItemByCode(code); // servidor resolve a partir do CMS
return { price: item!.stripe_price_id, quantity }; // preço do CMS, nunca do cliente
})
);
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 }); // cliente redireciona para cá
}
src/app/api/stripe/webhook/route.ts — o sinal de confiança para fulfillment:
import { stripe } from "@/lib/stripe";
export async function POST(req: Request) {
const body = await req.text(); // Corpo BRUTO — necessário para verificar a assinatura
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") {
// fulfillment: registre o pedido / envie um recibo.
}
return new Response(null, { status: 200 });
}
Correção obrigatória do scaffold:
src/proxy.tsencaminha todo/api/*para o CMS, então suas rotas Stripe nunca rodam. Deixe-as passar primeiro: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(); // trata localmente } return cmsProxy(request as unknown as Parameters<typeof cmsProxy>[0]); }; // mantenha inalterado o `export const config = { matcher: [...] }` do scaffoldVerifique:
curl -X POST localhost:3000/api/stripe/webhook -d xretornaBad signature.
Execute o Stripe CLI para webhooks locais:
stripe login
stripe listen --forward-to localhost:3000/api/stripe/webhook
# copie o whsec_... para STRIPE_WEBHOOK_SECRET, reinicie o bun dev
O whsec_… é por sessão. Duas regras sustentam a segurança: o checkout rederiva o preço a partir do CMS pelo code (um carrinho adulterado não consegue mudá-lo) e o webhook verifica a assinatura contra o corpo cru.
Admin → Pages → Create page, três vezes. Mapeie cada parâmetro para seu componente (campo slug code):
/products/{item_code} → item/categories/{category_code} → category/cart — uma página estática (digite literalmente /cart, não /{cart})Para cada rota: Page Builder → Add UI Element → Custom → adicione os componentes na ordem, preencha campos escalares (valor estático ou CEL) e Publish.
/products/{item_code}: nav, product_detail, footer/categories/{category_code}: nav, product_grid, footer/cart: nav, cart_summary, footerDefina nav.brand e footer.text como strings estáticas; headings com rótulos estáticos.
O Page Builder na rota de categoria — o elemento product_grid selecionado, seu heading ligado por CEL.
O Page Builder na rota de produto — o elemento product_detail em um binding de produto.
Pegadinha do Page Builder paramétrico: nas duas rotas paramétricas, adicionar UI elements não persiste (os blocos ficam órfãos e a página renderiza em branco). Até ser corrigido, conecte os
block_idsdessas páginas diretamente viaupdate_pagedo Profound MCP e depois publique. (O/cartestático vincula normalmente.) Pelo mesmo motivo,ProductGridderiva seu heading da categoria que busca em vez de via CEL.
/categories/lighting → o grid. Clique em um produto → detalhe + Adicionar ao carrinho. /cart → Pagar./cart?status=success, e stripe listen mostra
checkout.session.completed.Uma página de produto renderizada — galeria, preço e Adicionar ao carrinho.
O carrinho — itens e um único botão Pay-with-Stripe.
Somente itens precificados são compráveis — compre um dos ~3 que você precificou na etapa 6.
Opcional — internacionalize. Traduza cada componente (todas as 35 línguas de uma vez), adicione um segmento
/{language}/…mapeado para o componente Systemlanguageembutido e mude campos ligados por CEL paradocuments.translated. Veja o tutorial do diretório de aeroportos, Parte 2 etapa 7.
git init && git add -A && git commit -m "Stripe storefront"
gh repo create store --private --source=. --push # --public também serve
Comando de build:
generated/cms-schemas.tsestá no .gitignore, então fixe o build para regenerá-lo — adicionevercel.json:{ "$schema": "https://openapi.vercel.sh/vercel.json", "buildCommand": "bun run generate-schemas && next build" }
Na Vercel: Add New → Project, importe store e adicione as variáveis de ambiente — PROFOUND_API_KEY,
NEXT_PUBLIC_PROFOUND_WEBSITE_ID, NEXT_PUBLIC_CMS_API_URL, NEXT_PUBLIC_BUNNY_CDN_URL,
STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET (o valor do endpoint implantado, abaixo) e
NEXT_PUBLIC_SITE_URL (sua URL de produção). Faça o deploy.
Depois conecte o webhook em produção (o segredo local do stripe listen era apenas local): Stripe
→ Developers → Webhooks → + Add endpoint → https://<prod>/api/stripe/webhook, evento
checkout.session.completed. Copie o whsec_… para a Vercel e faça redeploy.
Variáveis de ambiente ausentes = "funciona localmente, branco em produção" — o principal tropeço de deploy. Aqui implantamos com chaves de teste; troque STRIPE_SECRET_KEY e o segredo do webhook para valores live quando estiver pronto para aceitar pagamentos reais.
Ambos vêm com o scaffold.
<Refresher> atualiza a página que você está pré-visualizando quando um editor salva
no admin — sem redeploy. (É uma prévia para o editor; visitantes veem conteúdo publicado na revalidação normal.)?edit_mode=true a qualquer URL para ver overlays de edição. Visitantes públicos
veem a página limpa.Adicione a rota de preview que o scaffold omite. O admin carrega seu iframe de preview em
/cms-preview_<path>; sem essa rota cada preview dá 404. Adicione:// src/app/cms-preview_/[...slug]/page.tsx import { ParametricRoutePreviewPage } from "cms-renderer/lib/renderer"; import { registry } from "../../registry"; // extraia seu registry para um módulo compartilhado export default async function Page({ params, searchParams }) { const { slug } = await params; const PreviewPage = ParametricRoutePreviewPage as any; // RSC assíncrono; tipos 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} />; }Adicione também
src/app/cms-preview_/page.tsx(mesmo código,slug: []) para a raiz do segmento.
Uma vitrine da Stripe orientada a conteúdo: catálogo em CMS, páginas de listagem + detalhe a partir de um conjunto de rotas e um checkout hospedado funcional. A IA semeou o catálogo, conectou o design e escreveu o leitor + componentes do catálogo + carrinho headless; você montou os componentes, os vínculos de preço da Stripe, as três rotas, o cromo em CEL e três arquivos curtos da Stripe. CEL vincula o cromo; componentes buscam o catálogo. E a Stripe permaneceu enxuta — uma chamada a sessions.create e um webhook assinado, com o comprador pagando na própria página da Stripe.