Build a theme from scratch
A theme is a standalone web application. It does not run inside the store: it talks to it over HTTP, the way any client would.
That means you can use whatever stack you like — Next, Astro, Vite, Nuxt, SvelteKit, Remix — as long as you meet the contract.
The minimum
Section titled “The minimum”A folder inside themes/ with this:
my-theme/ theme.json ← required: who you are and what you bring settings.schema.json ← what can be customised from the panel sections.schema.json ← which blocks can be rearranged (optional) demo/contenido.json ← demo content (optional, strongly recommended) .env.example ← what variables you needWithout theme.json the panel does not see it.
theme.json
Section titled “theme.json”Declares what you are and what you can do:
{ "contract": "2.0", "id": "my-theme", "name": "My Theme", "version": "1.0.0", "runtime": { "stack": "next", "commands": { "install": "npm install", "dev": "npm run dev", "build": "npm run build" }, "env": [ { "name": "NEXT_PUBLIC_PCC_BACKEND_URL", "required": true }, { "name": "NEXT_PUBLIC_PCC_PUBLISHABLE_KEY", "required": true } ] }, "capabilities": { "pages": ["home", "catalog", "product", "cart", "checkout", "order-confirmation"], "blocks": ["hero", "bestsellers", "newsletter"], "locales": ["es"] }}capabilities.pages is not decorative: the panel warns you if you say you have
checkout and do not declare an order-confirmation page. A customer who pays and
lands nowhere is a lost cart.
The full list of fields is in the theme contract.
Talking to the store
Section titled “Talking to the store”Two values, and the second is the one most often forgotten:
COMMERCE_URL=https://mystore.comCOMMERCE_KEY=pk_...Without the publishable key your theme starts up and sees not a single product, and the error does not say a key is missing. It is the number-one failure when setting up a theme.
You can talk to the Store API bare with fetch, or use the data contract’s
adapter:
import { createCommerce } from "@pcreative/commerce-contract/api"
const commerce = createCommerce({ baseUrl: process.env.COMMERCE_URL, publishableKey: process.env.COMMERCE_KEY, countryCode: "es",})The adapter uses plain fetch, no SDK: an SDK assumes a browser, and that is
exactly what would stop an Astro theme or an edge runtime from using the same
code.
Demo content
Section titled “Demo content”An empty theme cannot be judged. Add a demo/contenido.json with categories and
products:
{ "categorias": [{ "handle": "cakes", "nombre": "Cakes" }], "productos": [{ "handle": "chocolate-cake", "titulo": "Chocolate cake", "categoria": "cakes", "imagenes": ["https://…"], "variantes": [{ "titulo": "8 servings", "precio": 28, "sku": "TC-8" }] }]}It is platform-agnostic: it describes products, not anyone’s tables. The panel validates it before importing and tells you what it brings.
Customisation from the panel
Section titled “Customisation from the panel”In settings.schema.json you declare what can be changed without programming:
colours, fonts, copy, images. The panel generates the screen itself from the
schema — you do not have to write any UI.
Four recurring mistakes
Section titled “Four recurring mistakes”“0 products” → the publishable key is missing, or points at a different sales channel.
CORS blocking everything → your address is not in the backend’s STORE_CORS.
When you stand the theme up on a new port, remember.
Prices as NaN → the Store API returns unit_price and currency_code; if
your UI reads price and currency, the mapping is missing. It is silent.
Checkout does not create the order → a step is missing. The contract
requires, in this order: setEmail, setAddresses, setShippingMethod (one
per parcel if there is a marketplace), selectPaymentMethod and complete.
Each returns the updated cart; the one that fails tells you what is missing.