Panduan praktis: bangun etalase Stripe berbasis konten di Profound CMS โ katalog yang bisa diedit pedagang tanpa kode, dua rute parametrik, keranjang headless, dan checkout yang di-host Stripe.
Toko yang sudah jadi dalam pergerakan โ jelajahi kategori, buka produk, tambah ke keranjang, selesaikan pembayaran.
Panduan praktis yang membangun toko berbasis konten di Profound CMS: sebuah katalog produk (kategori + item) yang dimodelkan di CMS, halaman daftar dan detail dari satu set rute, dan checkout yang di-host Stripe dikirim sebagai komponen headless.
Tulang punggungnya adalah Next.js yang ditulis manual plus admin Profound. Claude Code (melalui Profound MCP) menangani tiga pekerjaan besar โ mengisi katalog, menghubungkan sistem desain, dan menulis komponen etalase (termasuk keranjang headless). Tiga bagian: Setup, Build, Production.
Pembayaran dalam satu baris. Kami menggunakan Stripe-hosted Checkout: pembeli membayar di halaman Stripe, bukan milik Anda. Aplikasi Anda hanya melakukan dua hal sisi server โ membuat Checkout Session dan memverifikasi satu webhook. Tanpa kolom kartu, tanpa Stripe Elements, tanpa beban PCI.
/products/{item_code}, /categories/{category_code}) plus satu /cart statis, semuanya dari satu set komponen.useCart) dan checkout yang di-host Stripe, dengan harga selalu diselesaikan sisi server dari Stripe Price ID.curl -fsSL https://bun.sh/install | bashstripe login) untuk webhook lokal.gh) dan akun Vercel yang terhubung ke GitHub.Profound memisahkan konten dari rendering:
category, item); satu yang ditandai UI Element dapat ditempatkan di halaman (nav, product_grid, โฆ).meta.params.* di CEL, routeParams di React).cms-renderer; Stripe ditambahkan sebagai rute API biasa.Satu aturan yang membentuk pembangunan: CEL hanya mengikat field string/number. Jadi chrome skalar (merek nav, footer, heading) diikat dengan CEL, sementara apa pun yang kaya atau kumpulan (grid produk, galeri gambar, rich text) diambil di dalam komponen React oleh parameter rute. Dan Stripe adalah sumber kebenaran harga โ price di CMS hanya untuk tampilan; biaya selalu diselesaikan sisi server dari Stripe Price ID.
Keadaan akhir: katalog kecil yang dipublikasikan, aplikasi terhubung untuk membacanya, Stripe terpasang, desain siap โ belum ada yang dirender.
Daftar di Profound (WorkOS auth). Buat situs web bernama store, lalu salin website ID-nya (UUID di URL admin) dan sebuah read-tier API key (Deployments โ Create API key). Aplikasi hanya membaca; seed katalog nanti melalui MCP, yang melakukan autentikasi terpisah.
bunx create-profound-next store
cd store
bun add stripe
Scaffoldnya adalah proyek Next.js App Router yang sudah di-wire untuk Profound (cms-renderer SDK, rute catch-all, skrip generate-schemas, <Refresher>). Tidak menyertakan styling.
bun add stripe menarik server SDK โ satu-satunya dependensi pembayaran yang dibutuhkan hosted checkout.
Tambahkan nilai Anda ke .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 # menyajikan gambar yang di-host CMS
# Stripe
STRIPE_SECRET_KEY=sk_test_... # kunci uji di sini; ganti ke kunci live saat Anda go live
STRIPE_WEBHOOK_SECRET=whsec_... # diisi di langkah Build 4
NEXT_PUBLIC_SITE_URL=http://localhost:3000
Ambil STRIPE_SECRET_KEY dari Stripe โ Developers โ API keys. Kami menggunakan kunci uji (sk_test_โฆ) sehingga build tidak memindahkan uang nyata; ganti ke kunci live saat Anda siap menerima pembayaran nyata. Jalankan bun dev dan buka localhost:3000 โ starter-nya muncul.
Hosted checkout mengarahkan browser ke URL Stripe, jadi kunci rahasia server adalah satu-satunya yang dibutuhkan Stripe โ tanpa publishable key, tanpa client Stripe SDK.
category, product_image, dan itemBuat tiga Custom Component (Components โ Create new component) โ sumber data, jadi tanpa tag UI Element. Setiap komponen dibuat Active.
CMS tidak memiliki field "array of image", jadi galeri adalah array of references ke komponen kecil product_image. Buat category dan product_image (dan set aktif) sebelum item โ field referensi hanya bisa menargetkan komponen 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 referensi โ product_image), price (Number, sen โ hanya tampilan), currency (Select, usd), stripePriceId (Text), category (Reference โ category), active (Boolean)Biarkan semua field opsional. Admin akan membuat nama field menjadi lower_snake_case ("Stripe Price Id" โ stripe_price_id) โ itulah yang digunakan kode Anda, jadi baca nama sebenarnya kembali dari generate-schemas nanti. Kami menamai handle yang bisa dirutekan sebagai code (bukan slug): itu menjadi Route Slug dan kunci untuk lookup documents.getByCode yang bersih nanti.
Komponen item โ code sebagai Route Slug, images sebagai referensi ke product_image, plus stripePriceId dan referensi category.
bun run generate-schemas
Menulis skema + tipe Zod ke generated/cms-schemas.ts (categorySchema/Category, itemSchema/Item). Sekaligus memeriksa koneksi โ kredensial yang salah akan gagal di sini.
Instal dan autentikasi MCP sekali:
claude mcp add --transport http Profound http://107.21.107.99:8081/mcp
Jalankan mcp__Profound__authenticate, selesaikan alur WorkOS, lalu minta Claude:
Hasilkan katalog ecommerce kecil untuk toko bernama Edison's Inventions โ tiga kategori dan produk ini, dengan
descriptionsingkat yang akurat secara periode,pricedalam sen,currency: "usd", danactive: 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)Setiap kategori butuh
namedancodehuruf kecil itu; setiap item butuhname,codehuruf kecil tersebut,description,price(dalam sen),currency, danactive. Simpan kedata/catalog.jsondan validasi terhadap komponencategorydanitemkita. Kemudian gunakan Profound MCP untuk membuat masing-masing sebagai dokumen yang dipublikasikan: buat kategori terlebih dahulu, tangkap ID-nya, lalu buat item dengancategorydiisi referensi โ{ "_type": "reference", "_ref": "<category-id>", "_schema": "category" }. BiarkanstripePriceIdkosong dulu. Kerjakan item paralel.
Claude menulis data/catalog.json, memvalidasinya, dan menjalankan panggilan create_document paralel (status: "published"). Seed kategori sebelum item agar referensi menunjuk ke ID yang sudah ada.
Tiga kategori yang sudah di-seed, published dan Live.
Delapan produk yang di-seed, masing-masing terhubung ke kategori.
Katalog sudah di CMS; sekarang berikan beberapa produk harga Stripe nyata โ tugas pedagang, selesai di dua panel admin, tanpa kode:
price_โฆ).stripePriceId, simpan.CMS menyimpan katalog; Stripe menyimpan harga definitif; tautannya adalah satu string yang ditempel pedagang. (Ingin mengotomasi? Stripe MCP resmi bisa membuat Products/Prices untuk Anda โ tempelkan ID yang dikembalikan dengan cara yang sama.)
Scaffold dikirim tanpa gaya. Letakkan DESIGN.md (blok @theme Tailwind v4 + token) di root proyek โ milik Anda, atau unduh dari refero.design. Lalu minta Claude, dibatasi hanya styling:
Baca file desain yang baru saya tambahkan. Siapkan Tailwind jika perlu, lalu hubungkan tema dan font agar styling berjalan. Gunakan
next/fontuntuk font โ jangan memuat dari Google saat runtime. Hanya styling โ jangan membangun halaman atau komponen apa pun dulu.
Pastikan src/app/globals.css memiliki @import "tailwindcss"; + blok @theme dan localhost:3000 menampilkan token. Jaga prompt tetap ketat (prompt terbuka, agen bisa membuat halaman penuh), dan muat font lewat next/font, jangan impor runtime dari Google.
Opsional โ Anda bisa mencapai checkout yang berfungsi tanpa gambar. Untuk menambahkannya: buat dokumen product_image untuk setiap gambar (unggah ke field image), lalu referensikan dari array images produk. Bawa foto produk sendiri, atau hasilkan set yang kohesif dengan model gambar (minta Claude menurunkan prompt sesuai merek dari DESIGN.md dan kunci satu --sref Midjourney agar setiap bidikan selaras).
cms-renderermandiri tidak memiliki helper URL gambar, jadi masukkanbuildAssetUrlkesrc/lib/image.ts(~40 baris) โ fungsi ini menambahkan awalanNEXT_PUBLIC_BUNNY_CDN_URLdan ekstensi. Komponen di langkah Build 3 menggunakannya.
Bangun lapisan rendering dan checkout, berakhir dengan pembelian nyata dalam mode uji.
Lima komponen, masing-masing Active dan ditandai UI Element (Settings โ Tags), tanpa Route Slug:
nav โ brand ยท product_grid โ heading ยท product_detail โ heading ยท cart_summary โ heading ยท footer โ text (semuanya Text)Tag UI Element membuat komponen muncul di daftar Add UI Element Page Builder โ Active saja tidak cukup. Setiap field adalah skalar (tipe yang diikat CEL); data katalog bukan field di sini โ ProductGrid/ProductDetail mengambilnya melalui parameter rute (langkah 3).
bun run generate-schemas
Satu prompt membangun helper pembaca, lima komponen, keranjang, dan registry:
Bangun etalase kita di
src/, menggunakan SDKcms-rendererProfound.
src/lib/catalog.tsโ pembaca CMS sisi server. Buat klien dengangetCmsClient({ cmsUrl: process.env.NEXT_PUBLIC_CMS_API_URL!, apiKey: process.env.PROFOUND_API_KEY, websiteId: process.env.NEXT_PUBLIC_PROFOUND_WEBSITE_ID! })daricms-renderer/lib/cms-api. EksporgetItemByCode(code)โcms.documents.getByCode.query({ websiteId, schemaName: "item", code })yang mengembalikanres.document.published_content. EksporlistItems(categoryCode?)โcms.documents.list.query({ websiteId, schemaName: "item", status: "published", limit: 100 }), mappingres.documentske.published_content, filteractive !== false, dan jikacategoryCodediberikan, pertahankan item dengancategory._refsama dengandocument.idkategori. EksporresolveImages(refs)yang mengurai setiap referensiitem.imagesmelaluicms.documents.get.query({ websiteId, id: ref._ref })dan mengubah field gambarnya menjadi URL denganbuildAssetUrlyang dimasukkan (Bagian 1 langkah 8).
src/components/โ lima komponen elemen UI yang diregistrasikan di registry rute catch-all dengan nama komponen, snake_case sesuai admin:{ nav, product_grid, product_detail, cart_summary, footer }.NavdanFootermembaca field skalarnya dari propcontent(diketikBlockComponentProps<T>daricms-renderer/lib/types).ProductGriddanProductDetailadalah async server component yang membacarouteParamsdan fetch daricatalog.ts:routeParams.<param>adalah{ value, โฆ }โ baca.value, jadiProductGridmemanggillistItems(routeParams.category_code?.value)(kartu menaut ke/products/{code}) danProductDetailmemanggilgetItemByCode(routeParams.item_code?.value)(galeri lewatresolveImages, deskripsi rich-text, harga, tombol Tambah ke keranjang).CartSummarymerender keranjang dariuseCartdengan tombol Bayar. SimpanformatPricedalamsrc/lib/format.tsmurni agar komponen client tidak mengimporcatalog.tsyang hanya sisi server.
src/components/AddToCartButton.tsxโ tombol"use client"yang menerima{ code, name, priceLabel }dan memanggiluseCart().addItem({ code, name, priceLabel, quantity: 1 }). Gunakan di dalamProductDetail.
src/lib/useCart.tsโ keranjang headless: item{ code, name, priceLabel, quantity }dalam state, dipersist kelocalStorage, mengeksposaddItem/removeItem/updateQty/subtotaldancheckout()yang melakukan POST{ lines: [{ code, quantity }] }(hanya kode dan kuantitas โ jangan pernah harga) ke/api/stripe/checkout, lalu mengarahkan ulang keurlyang dikembalikan.Gaya semua dengan sistem desain kita, sebagai komponen milik kita sendiri โ jangan menyalin layout situs sumber.
Tiga hal yang perlu diketahui setelahnya:
content ({ content }: BlockComponentProps<T>) โ destructure field sebagai prop level atas dan block akan kosong. Data katalog berasal dari routeParams + fetch catalog.ts, karena CEL tidak bisa mengikat daftar atau galeri. Keranjang membawa kode item, bukan harga.routeParams.<param> adalah { value, schemaName, document } โ bacalah .value. Hasil baca mengembalikan published_content, bukan .content. Kunci registry adalah snake_case sesuai admin.@types/react/@types/react-dom ke v19 โ scaffold mengirim v18, yang mematahkan block server-component async di React 19.Tiga berkas server pendek โ satu-satunya kode pembayaran di aplikasi. Mereka memakai ulang getItemByCode, jadi biaya diselesaikan sisi 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 โ uraikan setiap item dari CMS, tagih harga Stripe:
import { NextResponse } from "next/server";
import { stripe } from "@/lib/stripe";
import { getItemByCode } from "@/lib/catalog"; // sisi server, kunci baca
export async function POST(req: Request) {
const { lines } = await req.json(); // [{ code, quantity }] โ tanpa harga dari klien
const line_items = await Promise.all(
lines.map(async ({ code, quantity }: { code: string; quantity: number }) => {
const item = await getItemByCode(code); // sisi server mengambil dari CMS
return { price: item!.stripe_price_id, quantity }; // harga dari CMS, bukan dari klien
})
);
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 }); // klien diarahkan ke sini
}
src/app/api/stripe/webhook/route.ts โ sinyal pemenuhan tepercaya:
import { stripe } from "@/lib/stripe";
export async function POST(req: Request) {
const body = await req.text(); // body RAW โ wajib untuk verifikasi tanda tangan
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") {
// penuhi pesanan: catat order / kirim struk.
}
return new Response(null, { status: 200 });
}
Perbaikan scaffold wajib:
src/proxy.tsdi scaffold meneruskan setiap/api/*ke CMS, sehingga rute Stripe Anda tidak pernah jalan. Biarkan mereka lolos dulu: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(); // tangani secara lokal } return cmsProxy(request as unknown as Parameters<typeof cmsProxy>[0]); }; // biarkan `export const config = { matcher: [...] }` scaffold tetapVerifikasi:
curl -X POST localhost:3000/api/stripe/webhook -d xmengembalikanBad signature.
Jalankan Stripe CLI untuk webhook lokal:
stripe login
stripe listen --forward-to localhost:3000/api/stripe/webhook
# salin whsec_... ke STRIPE_WEBHOOK_SECRET, restart bun dev
whsec_โฆ bersifat per sesi. Dua aturan menjaga keamanan: checkout menurunkan ulang harga dari CMS lewat code (keranjang yang diubah tidak bisa mengganti harga), dan webhook memverifikasi tanda tangan terhadap body mentah.
Admin โ Pages โ Create page, tiga kali. Petakan setiap parameter ke komponennya (field slug code):
/products/{item_code} โ item/categories/{category_code} โ category/cart โ halaman static (masukkan literal /cart, bukan /{cart})Untuk setiap rute: Page Builder โ Add UI Element โ Custom โ tambahkan komponen berurutan, isi field skalar (nilai statis atau CEL), Publish.
/products/{item_code}: nav, product_detail, footer/categories/{category_code}: nav, product_grid, footer/cart: nav, cart_summary, footerSet nav.brand dan footer.text ke string statis; heading ke label statis.
Page Builder di rute kategori โ elemen product_grid dipilih, heading-nya diikat oleh CEL.
Page Builder di rute produk โ elemen product_detail pada binding produk.
Gotcha Page Builder parametrik: pada dua rute parametrik, menambahkan elemen UI tidak tersimpan (block jadi yatim dan halaman kosong). Sampai diperbaiki, hubungkan
block_idsrute tersebut langsung lewat Profound MCPupdate_page, lalu publish. (/cartstatis terhubung normal.) Untuk alasan yang sama,ProductGridmengambil heading dari kategori yang di-fetch ketimbang via CEL.
/categories/lighting โ grid. Klik produk โ detail + Tambah ke keranjang. /cart โ Bayar./cart?status=success, dan stripe listen menampilkan checkout.session.completed.Halaman produk yang dirender โ galeri, harga, dan tombol Tambah ke keranjang.
Keranjang โ item baris dan satu tombol Bayar dengan Stripe.
Hanya item yang punya harga yang bisa dibeli โ belilah salah satu dari ~3 yang sudah Anda beri harga di langkah 6.
Opsional โ internasionalisasi. Terjemahkan tiap komponen (semua 35 bahasa sekaligus), tambahkan segmen
/{language}/โฆyang dipetakan ke komponen Sistemlanguagebawaan, dan ubah field yang diikat CEL kedocuments.translated. Lihat tutorial direktori bandara, Bagian 2 langkah 7.
git init && git add -A && git commit -m "Stripe storefront"
gh repo create store --private --source=. --push # --public juga boleh
Perintah build:
generated/cms-schemas.tsdiabaikan git, jadi pin build untuk menghasilkannya lagi โ tambahkanvercel.json:{ "$schema": "https://openapi.vercel.sh/vercel.json", "buildCommand": "bun run generate-schemas && next build" }
Di Vercel: Add New โ Project, impor store, dan tambahkan variabel env โ PROFOUND_API_KEY, NEXT_PUBLIC_PROFOUND_WEBSITE_ID, NEXT_PUBLIC_CMS_API_URL, NEXT_PUBLIC_BUNNY_CDN_URL, STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET (nilai endpoint ter-deploy, di bawah), dan NEXT_PUBLIC_SITE_URL (URL produksi Anda). Deploy.
Lalu hubungkan webhook produksi (rahasia stripe listen lokal hanya untuk lokal): Stripe โ Developers โ Webhooks โ + Add endpoint โ https://<prod>/api/stripe/webhook, event checkout.session.completed. Salin whsec_โฆ-nya ke Vercel dan deploy ulang.
Env vars yang hilang = "berfungsi lokal, kosong di produksi" โ jebakan deploy nomor satu. Kami deploy dengan kunci uji di sini; ganti STRIPE_SECRET_KEY dan rahasia webhook ke nilai live saat siap menerima pembayaran nyata.
Keduanya dikirim bersama scaffold.
<Refresher> memperbarui halaman yang sedang Anda pratinjau ketika editor menyimpan di admin โ tanpa redeploy. (Ini pratinjau untuk editor; pengunjung melihat konten published pada revalidasi normal.)?edit_mode=true ke URL apa pun untuk overlay edit. Pengunjung umum melihat halaman bersih.Tambahkan rute preview yang tidak disertakan scaffold. Admin memuat iframe pratinjau di
/cms-preview_<path>; tanpa rute itu setiap pratinjau 404. Tambahkan:// src/app/cms-preview_/[...slug]/page.tsx import { ParametricRoutePreviewPage } from "cms-renderer/lib/renderer"; import { registry } from "../../registry"; // ekstrak registry Anda ke modul bersama export default async function Page({ params, searchParams }) { const { slug } = await params; const PreviewPage = ParametricRoutePreviewPage as any; // async RSC; tipe 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} />; }Tambahkan juga
src/app/cms-preview_/page.tsx(sama,slug: []) untuk root segmennya.
Etalase Stripe berbasis konten: katalog CMS, halaman daftar + detail dari satu set rute, dan checkout hosted yang berfungsi. AI men-seed katalog, menghubungkan desain, dan menulis pembaca katalog + komponen + keranjang headless; Anda membuat komponen, tautan harga Stripe, tiga rute, chrome CEL, dan tiga berkas Stripe singkat. CEL mengikat chrome; komponen mengambil katalog. Dan Stripe tetap sederhana โ satu panggilan sessions.create dan satu webhook bertanda tangan, dengan pembeli membayar di halaman Stripe sendiri.