Skip to content
pcreative Commerce

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 in capabilities.pages along with order-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 /en or /es routes; adding a second language is your job.

Which one. The first five render on the server. vite-react is 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-*.zip

It 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 -d

It 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.txt

Then, in your theme:

cp .env.example .env.local     # set the URL and the publishable key
npm install
npm run dev

Without 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 code

Locales 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:

  1. It renders in any position, and repeated.
  2. It survives having nothing — an empty setting must not take down the page.
  3. It carries data-pcc-seccion with 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:

eventwhenfor
pcc:seccion:descargadabefore the old node is removedstop loops, detach observers, release WebGL
pcc:seccion:cargadaafter the new one is inremount 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 info

If audit says "no dependency lockfile", run npm install first. 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"   # signed

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

See also