Ir al contenido

Contrato de tema — pcreative Commerce

Versión 2.0 · sustituye a la 1.0 (temas atados a Next.js/React).

Estándar que cumple todo tema de storefront para que sean intercambiables, personalizables desde el panel y actualizables sin perder la configuración del cliente, sin renunciar a que cada tema tenga diseño 100 % propio.

Implementación de referencia: packages/theme-contract. Tema de referencia: themes/growshop-premium.


La 1.0 fijaba el stack: «Next.js 15 + React 19 + Tailwind 4 + el SDK del backend». Funcionaba, pero convertía el catálogo de temas en «una plantilla React más», y ahí compite con Vercel Commerce, que es gratis. El backend es headless: lo que ataba a React no era el sistema, eran dos ficheros TypeScript y una envoltura.

1.0 2.0
theme.config.ts (TypeScript) theme.json + settings.schema.json + settings.json (JSON)
tokens en un objeto TS tokens.json en formato W3C/DTCG
theme-tokens.ts genera CSS a mano el contrato lo genera, y Style Dictionary lo compila a CSS/SCSS/JS/iOS/Android
lib/commerce.ts envolviendo el SDK del backend contrato de datos sobre la API de la tienda + adaptador por backend
theme.override.ts (código) theme.override.json (datos)
formulario del customizer hardcodeado en el admin cada tema declara sus ajustes; el panel genera el formulario

Consecuencia práctica: un tema puede estar escrito en Next, Astro, Nuxt, SvelteKit o incluso PHP, y el mismo panel lo instala, lo personaliza y lo despliega. Eso es lo que no tiene ni Vercel Commerce (un solo storefront React) ni WooCommerce/PrestaShop (miles de temas, pero atados a PHP).


themes/<id>/
├── theme.json manifiesto: identidad, runtime, qué implementa
├── tokens.json design tokens en formato DTCG
├── settings.schema.json qué se le puede tocar (define el customizer)
├── settings.json valores de fábrica del tema (la demo)
├── theme.override.json ← lo escribe el panel, POR TIENDA (no lo trae el tema)
├── preview.png
└── src/ LIBRE: el código del tema, en el stack que sea

Los cinco JSON son el contrato. src/ es territorio del tema: ni el panel ni el instalador miran ahí.

Regla que sostiene todo lo demás: personalizar nunca edita el tema, solo theme.override.json. Por eso actualizar un tema a una versión nueva no pisa la marca ni los colores del cliente — que es exactamente lo que se rompe en cualquier sistema donde «personalizar» significa tocar los ficheros del tema.


Schema: theme.schema.json.

{
"contract": "2.0",
"id": "growshop-premium",
"name": "Growshop Premium",
"version": "2.0.0",
"runtime": { // ← el campo que hace agnóstico el contrato
"target": "ssr", // ssr | static | spa | php | native
"stack": "next", // lista ABIERTA: astro, nuxt, sveltekit, laravel…
"engine": "node>=20",
"packageManager": "npm",
"commands": { "install": "npm ci", "build": "next build", "start": "next start" },
"env": [
{ "name": "NEXT_PUBLIC_COMMERCE_URL", "required": true },
{ "name": "REVALIDATE_SECRET", "secret": true }
]
},
"capabilities": { // qué implementa: el panel avisa antes de activarlo
"pages": ["home", "catalog", "product", "cart", "checkout", "..."],
"blocks": ["hero", "featured-categories", "bestsellers"],
"features": ["age-gate", "blog", "reviews", "search"],
"locales": ["es"],
"a11y": "wcag-aa"
},
"commerce": { "contract": "1.0", "adapters": [] }
}

El despliegue no adivina nada: los comandos y las variables de entorno los declara el tema. Un tema Astro pone "stack": "astro" y "build": "astro build" y el panel no necesita saber qué es Astro.


3. tokens.json — diseño en formato estándar

Sección titulada «3. tokens.json — diseño en formato estándar»

Formato: Design Tokens Format Module (DTCG), el estándar del W3C Community Group que en 2025 alcanzó estabilidad y que ya soportan Figma, Style Dictionary, Tokens Studio, Penpot y Terrazzo. No hay nada que inventar aquí: un diseñador exporta de Figma y el tema se lo come.

{
"color": {
"$type": "color",
"brand": {
"primary": {
"$value": { "colorSpace": "srgb", "components": [0.0863, 0.6392, 0.2902], "hex": "#16a34a" }
}
},
"scale": {
"leaf-400": {
"$value": "{color.brand.primary}",
"$extensions": {
"dev.pcreative.derive": { "from": "{color.brand.primary}", "lighten": 0.24 },
"dev.pcreative.css": "--color-leaf-400"
}
}
}
}
}

Tres extensiones propias, todas dentro de $extensions como manda la spec:

Extensión Para qué
dev.pcreative.derive Deriva un color de otro aclarándolo/oscureciéndolo. Es lo que permite que cambiar un color en el panel repinte la escala entera: una variable CSS no sabe recalcularse sola.
dev.pcreative.css Fija el nombre de la variable emitida. Sirve para adoptar el contrato en un tema que ya existe sin tocar un solo componente.
dev.pcreative.private El token existe pero no se emite como variable CSS.

Los nombres de variable se derivan del path: color.brand.primary--color-brand-primary (idéntico a name/kebab de Style Dictionary, así el build y el customizer no se contradicen).

  • En vivo (customizer): tokensToCss() del contrato — sin dependencias, corre en Node, en el edge y en el navegador. El panel guarda el override, el storefront inyecta el CSS y la web se repinta sin rebuild.
  • En build (distribución): preset de Style Dictionary v5, que entiende DTCG de serie —incluido el color como objeto— y saca CSS, SCSS, JS y, si algún día hace falta, iOS y Android.

4. settings.schema.json — el customizer, declarado por el tema

Sección titulada «4. settings.schema.json — el customizer, declarado por el tema»

Schema: settings.schema.json. El planteamiento es el de settings_schema.json de Shopify, con los tipos alineados a los tokens.

{
"groups": [
{
"id": "design", "label": "Diseño", "icon": "palette",
"settings": [
{ "type": "color", "id": "primary", "label": "Color principal",
"token": "color.brand.primary" }, // ← el puente diseño ↔ formulario
{ "type": "select", "id": "mode", "label": "Modo", "default": "dark",
"options": [{ "value": "dark", "label": "Oscuro" }] }
]
},
{
"id": "commerce", "label": "Tienda",
"settings": [
{ "type": "number", "id": "codSurcharge", "label": "Recargo contrarreembolso", "unit": "€",
"visibleIf": { "setting": "commerce.paymentMethods", "equals": "cod" } }
]
}
],
"blocks": [
{ "type": "hero", "label": "Portada", "limit": 1,
"settings": [{ "type": "text", "id": "heading", "label": "Titular", "required": true }] }
]
}

Tipos de campo: text, textarea, richtext, number, range, checkbox, select, radio, color, font, image, url, email, tel, list, token, más header y paragraph para maquetar el formulario.

Un campo con token es el único puente entre el formulario y el diseño: cuando el cliente lo cambia, el valor va al token, de ahí a la variable CSS y de ahí a todo lo que deriva de él. Por eso el panel no necesita saber nada del stack del tema para recolorearlo.


Rutas estándar (capabilities.pages), para que el catálogo sea predecible: home · catalog · category · product · search · cart · checkout · order-confirmation · account · legal · about · contact · faq · blog · blog-post · not-found.

Los bloques son las secciones componibles. El tema declara cuáles sabe renderizar; el override dice en qué orden van y con qué ajustes:

{ "sections": { "home": [
{ "type": "hero", "settings": { "heading": "Cultivo de interior, bien hecho" } },
{ "type": "bestsellers", "settings": { "limit": 8 } }
]}}

6. theme.override.json — la capa de la tienda

Sección titulada «6. theme.override.json — la capa de la tienda»

Schema: override.schema.json.

{
"contract": "2.0",
"theme": "growshop-premium",
"themeVersion": "2.0.0",
"settings": { "brand": { "name": "Green Room" }, "design": { "primary": "#16a34a" } },
"tokens": { "color.surface.base": "#0d0f0e" },
"sections": { "home": [ /* … */ ] }
}

Solo guarda lo que difiere. El orden de mezcla es defaults del schema ← settings.json del tema ← override de la tienda, y la mezcla es por campo, no por grupo: un override parcial no borra el resto.


theme.json declara commerce.contract, no un SDK. Un tema no importa el SDK del backend: consume el contrato de datos y el adaptador resuelve. Así el mismo tema sirve para este backend hoy y para otro mañana, y —al revés— el backend no queda casado con temas React.

Ver packages/commerce-contract.


Terminal window
pcc-theme validate themes/growshop-premium # forma (JSON Schema) + coherencia entre ficheros
pcc-theme css themes/growshop-premium # variables CSS ya resueltas
pcc-theme info themes/growshop-premium # resumen del tema

validate hace dos pasadas. La de forma valida cada fichero contra su JSON Schema. La de coherencia es la que caza lo que ningún schema ve: un campo de color apuntando a un token que no existe, un bloque que el customizer ofrece y el tema no implementa, un override de otra versión, un color cuyo hex y cuyos components no son el mismo color.


  1. theme.meta.json + la parte de identidad de theme.config.tstheme.json.
  2. tokens de theme.config.tstokens.json. Para no tocar componentes, cada token lleva dev.pcreative.css con el nombre de variable que ya usaban.
  3. Las escalas que theme-tokens.ts calculaba en TypeScript → tokens con dev.pcreative.derive. Misma función, pero declarada.
  4. El resto de theme.config.tssettings.schema.json (los campos) y settings.json (los valores).
  5. theme.override.tstheme.override.json.
  6. lib/commerce.ts → adaptador del contrato de datos.
  7. pcc-theme validate hasta que salga limpio.

growshop-premium está migrado y sirve de plantilla.