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.
Qué cambió respecto a la 1.0 y por qué
Sección titulada «Qué cambió respecto a la 1.0 y por qué»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).
1. Estructura de un paquete de tema
Sección titulada «1. Estructura de un paquete de tema»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 seaLos 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.
2. theme.json — el manifiesto
Sección titulada «2. theme.json — el manifiesto»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. |
Cómo llegan los tokens al tema
Sección titulada «Cómo llegan los tokens al tema»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.
5. Páginas y bloques
Sección titulada «5. Páginas y bloques»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.
7. Contrato de datos (commerce)
Sección titulada «7. Contrato de datos (commerce)»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.
8. Herramientas
Sección titulada «8. Herramientas»pcc-theme validate themes/growshop-premium # forma (JSON Schema) + coherencia entre ficherospcc-theme css themes/growshop-premium # variables CSS ya resueltaspcc-theme info themes/growshop-premium # resumen del temavalidate 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.
9. Migrar un tema de la 1.0
Sección titulada «9. Migrar un tema de la 1.0»theme.meta.json+ la parte de identidad detheme.config.ts→theme.json.tokensdetheme.config.ts→tokens.json. Para no tocar componentes, cada token llevadev.pcreative.csscon el nombre de variable que ya usaban.- Las escalas que
theme-tokens.tscalculaba en TypeScript → tokens condev.pcreative.derive. Misma función, pero declarada. - El resto de
theme.config.ts→settings.schema.json(los campos) ysettings.json(los valores). theme.override.ts→theme.override.json.lib/commerce.ts→ adaptador del contrato de datos.pcc-theme validatehasta que salga limpio.
growshop-premium está migrado y sirve de plantilla.