Build a theme
A theme is a standalone web app. It does not run inside the store: it talks to it over HTTP, like any other client would.
A theme is a standalone web app. It does not run inside the store: it talks to it over HTTP, like any other client would.
That is why you can use whatever stack you like. Six come with a skeleton —Next, Astro, SvelteKit, Vue (Nuxt), React Router and Vite + React— and the contract is the same across all of them.
1. Start
npx @pcreative/theme-contract init my-theme --name "My Theme" --stack astro
cd my-theme--stack takes next, astro, sveltekit, nuxt, react-router and
vite-react. Nothing to install first: npx fetches the tool, creates the
theme, and the theme then carries it as a dependency, so from there on
npx pcc-theme is enough.
That does not create an empty folder: it creates a theme that already talks to the store. Composable home page, server-paginated catalogue, product page, category, cart, blog and standalone pages — all against a real storefront.
What it does not bring, so you are not surprised:
- A checkout. The cart says so where the button used to point at a page
that did not exist: the button now goes back to the catalogue, and the note
next to it names the contract calls that resolve a checkout (
setEmail,setAddresses,listShippingOptions,setShippingMethod,listPaymentMethods,selectPaymentMethod,complete). When you build the page, declare it incapabilities.pagesalong withorder-confirmation: the skeleton leaves both out, because declaring a page that does not exist sends the installer to a 404 and the editor to composing something nobody paints. - More than one language. The skeleton is written in a single language:
English, unless your terminal is in Spanish (
LANG=es…), in which case it comes out in Spanish. It brings one pair of locale files and no/enor/esroutes; adding a second language is your job.
Which one. The first five render on the server.
vite-reactis static: the HTML arrives empty until JavaScript runs, so a search engine sees no product pages at all. For a public storefront that is the wrong choice; for a shop inside an app, behind a password, or on a kiosk, it is perfect.
2. See it running
You need a store to talk to, and the real one fits on your machine. Download pcreative Commerce from pcreativecommerce.dev and start it with the installer that comes next to the package:
sh instalar.sh pcreative-commerce-*.zipIt unpacks it, creates .env with a random database password and starts it.
If you prefer to do it by hand, the password is not optional: without
POSTGRES_PASSWORD in .env the database refuses to start.
unzip pcreative-commerce-*.zip && cd pcreative-commerce-*/
cp .env.docker.example .env # and fill in POSTGRES_PASSWORD
docker compose up -dIt comes already built: nothing compiles. Open the panel at http://localhost:7001/admin, finish the setup wizard and let it load the sample catalogue. The publishable key your theme needs is here:
docker compose exec backend cat /app/data/clave-publicable.txtThen, in your theme:
cp .env.example .env.local # set the URL and the publishable key
npm install
npm run devWithout the publishable key your theme starts and sees no products at all, and the error does not say a key is missing. It is the number-one first-day failure.
3. What you already get
my-theme/
theme.json who you are and what you bring
tokens.json colours, type, spacing — in a standard format (W3C DTCG)
settings.schema.json what can be customised from the panel
settings.json the factory values
sections.schema.json which sections you know how to render
templates/*.json how your pages are composed out of the box
locales/ your copy, in two files (see below)
demo/contenido.json sample products
src/ your codeLocales live in two files, not one. en.json is the storefront copy —what a
shopper reads— and en.schema.json is the customiser copy —what the shop owner
reads (es.json and es.schema.json if the skeleton came out in Spanish). Almost everyone only does the first, which is how an agency ships a
flawless French site with a Spanish admin panel.
4. Design
Colours are never written in code. They live in tokens.json and reach the
browser as CSS variables. A component with a hardcoded #fff is out of the
customiser forever:
/* yes */ color: var(--color-brand-primary);
/* no */ color: #1f5f57;What can be changed without coding is declared in settings.schema.json.
The panel builds the screen itself from the schema — you write no UI.
Sections are the blocks that get recomposed from the editor. Your theme
declares which ones it can render in sections.schema.json, and a section has
three rules:
- It renders in any position, and repeated.
- It survives having nothing — an empty setting must not take down the page.
- It carries
data-pcc-seccionwith its id. The skeleton emits it in a single place on purpose, so it cannot be forgotten on section number nine. Under any other attribute name it renders perfectly and the editor never sees it.
5. Sections with JavaScript
When someone changes a setting, the editor swaps that section's HTML and leaves the rest of the page alone. That has two consequences, and neither raises an error.
Your framework's lifecycle hooks do not fire. Not onMount, not
useEffect, not onMounted. The HTML arrives already rendered. Two events
exist for this; they fire on the section node and bubble:
| event | when | for |
|---|---|---|
pcc:seccion:descargada | before the old node is removed | stop loops, detach observers, release WebGL |
pcc:seccion:cargada | after the new one is in | remount carousels and animations |
section.addEventListener("pcc:seccion:cargada", (e) => mount(e.detail.el))
section.addEventListener("pcc:seccion:descargada", (e) => teardown(e.detail.el))🔴 Without listening to the unload one, every edit stacks another set of listeners and another animation loop on top of the last. Ten edits in, the page crawls.
And some sections cannot be rendered alone. A hero that pins the whole page's scroll, a shared WebGL canvas, or a timeline spanning three sections do not exist outside their page: swapping their node does not fail, it leaves something rendered and broken. Declare it and the editor reloads the page for that section instead:
{ "type": "cinematic-hero", "name": "Cinematic hero", "aislable": false }The default is true, which is what you want for 95 % of sections.
6. Talking to the store
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.
🔴 Money is not calculated in the theme. The amount and the currency come from the store; you format them. A theme that multiplies price by quantity ends up showing a believable, wrong number the moment tax, discounts or quantity promotions exist.
🔴 No secrets. A theme talks to the public API with the publishable key,
which is public. Asking for the database, the Admin API or a payment gateway
makes validate fail.
7. Blog and pages
They ship with the skeleton: templates/blog.json, blog-post.json and
page.json, with their routes. Standalone pages —terms, shipping, about us—
are written by the shop owner in their panel; you only render them.
Their content is rendered as unescaped HTML, because it comes from that shop's own panel: whoever can write there can already change the whole storefront, so this grants no new permission. If you ever render a customer's HTML —a review, a question— through that same door, sanitise it first.
8. Check
npx pcc-theme validate # shape and coherence — must come out WITH NO WARNINGS
npx pcc-theme audit # what would run, and which domains it talks to
npx pcc-theme infoIf
auditsays "no dependency lockfile", runnpm installfirst. It is right to ask: without one there is no way to know what whoever installs your theme will download.
No warnings, not "few". A theme shipped with warnings teaches people to ignore warnings, and from then on the validator is worthless.
🔴 And validating does not prove it compiles. Of the skeletons in this
project, three passed validate without a single warning and did not build — and
one built and did not start.
Run npm run build before you ship anything.
9. Ship
npx pcc-theme pack
npx pcc-theme pack --key key.pem --publisher "Your Studio" # signedOne command, and it does the seven things the .zip needs for the panel to
accept it: validates, audits, scans for secrets in what was about to be
packaged, copies to a staging area —your folder is never touched—, vendors the
contracts inside, installs and actually builds, and writes the archive.
🔴 Do not build the .zip by hand. If your dependencies point at a folder
on your machine, the package installs on your machine and nowhere else — and you
will not find out until someone uploads it. pack exists to fix exactly that.
Only what belongs to a theme goes in: if you have a folder of your own, pack
names what it left out.
10. If you sell it elsewhere
Declare in theme.json that it is sold under licence:
"license_check": { "product": "my-theme", "gracia": 7 }Your buyer pastes their key in the panel, on the theme's card. From then on:
- Verification works offline. The token is signed and checked against the embedded public key; only renewing it talks to the server.
- The key is used once and thrown away. What the shop stores is a token that expires and is bound to that domain, never your buyer's key.
- A licence NEVER switches off the storefront. The only thing it refuses is publishing, with the owner right there and the previous theme untouched. A licence server going down cannot black out someone who already paid you.
- Bought in the pcreative marketplace, no key is asked for: the purchase was already verified at download.
The mistakes that keep happening
"0 products" → the publishable key is missing, or points at another sales channel.
The editor cannot see a section → the attribute is data-pcc-seccion. With
data-seccion it renders perfectly and the editor never finds it.
The section renders and is dead → JavaScript does not re-run when it is
swapped. Listen for pcc:seccion:cargada.
The page crawls after ten edits → nobody listens for
pcc:seccion:descargada, so listeners are piling up.
The .zip will not install on the buyer's server → it was built by hand.
Use pack.
Panel colours change nothing → there is a hardcoded colour instead of a token.
It builds on your laptop and dies on the server → next/font/google
downloads the fonts while it builds, and the panel builds themes with no
network. On your laptop they are already cached, so you never see it. Put the
.woff2 files inside the theme and load them with next/font/local, licence
next to them. audit blocks this.
And once it is finished
Everything that comes after "it works" — packaging and signing it properly, publishing it in the marketplace, selling it elsewhere, what gets a listing turned down and what the licences actually do — is in Selling the theme you built.