Skip to content

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.

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 need

Without theme.json the panel does not see it.

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.

Two values, and the second is the one most often forgotten:

Terminal window
COMMERCE_URL=https://mystore.com
COMMERCE_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.

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.

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.

“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.