Saltar al contenido
pcreative Commerce

Contrato de tema — pcreative Commerce

Estándar que cumple todo tema de storefront para que sean intercambiables, personalizables desde el panel y actualizables sin perder la configuración del…

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: el paquete @pcreative/theme-contract. Tema de referencia: mascotas, uno de los escaparates que vienen con la tienda.


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.02.0
theme.config.ts (TypeScript)theme.json + settings.schema.json + settings.json (JSON)
tokens en un objeto TStokens.json en formato W3C/DTCG
theme-tokens.ts genera CSS a manoel contrato lo genera, y Style Dictionary lo compila a CSS/SCSS/JS/iOS/Android
lib/commerce.ts envolviendo el SDK del backendcontrato 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 admincada tema declara sus ajustes; el panel genera el formulario

Consecuencia práctica: un tema puede estar escrito en Next, Astro, Nuxt, SvelteKit, Remix/React Router, Vue o Eleventy, 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). Los temas en PHP no se ejecutan: ver qué stacks corren.


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)
├── sections.schema.json    qué secciones sabe pintar
├── templates/*.json        cómo se componen sus páginas de fábrica
├── locales/                sus textos: <idioma>.json (escaparate) y <idioma>.schema.json (customizer)
├── demo/contenido.json     contenido de ejemplo
├── 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 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

Schema: schema/theme.schema.json, dentro del paquete @pcreative/theme-contract.

{
  "contract": "2.0",
  "id": "mi-tema",                   // kebab-case, único en el catálogo
  "name": "Mi Tema",                 // lo que enseña el panel
  "version": "1.2.0",
  "author": "Tu estudio",
  "license": "Propietaria",
  "description": "Tienda de mascotas. Se compra por animal, no por categoría.",
  "niche": ["mascotas", "petshop"],   // filtra el catálogo (máx. 12)
  "preview": "preview.png",
  "screenshots": ["capturas/inicio.png", "capturas/producto.png"],
  "demo": "demo/contenido.json",     // valor por defecto

  "runtime": {                       // ← el campo que hace agnóstico el contrato
    "target": "ssr",                 // ssr | static | spa  (el schema admite también php | native)
    "stack": "next",                 // next | astro | nuxt | sveltekit | remix | react-router | vite-react | vue | eleventy
    "engine": "node>=20",
    "packageManager": "npm",         // npm | pnpm | yarn | bun
    "commands": { "install": "npm ci", "build": "next build", "start": "next start" },  // solo informativo
    "env": [
      { "name": "COMMERCE_URL", "required": true, "example": "http://localhost:9000" },
      { "name": "COMMERCE_KEY", "required": true }
    ],
    "hosts": ["cdn.ejemplo.com", "fonts.gstatic.com"]  // dominios de fuera con los que habla el código
  },

  "capabilities": {                  // qué implementa: el panel avisa antes de activarlo
    "pages":    ["home", "catalog", "product", "cart", "checkout", "..."],
    "blocks":   ["hero", "featured-products", "testimonials"],
    "features": ["search", "server-pagination", "structured-data"],
    "locales":  ["en", "es"],        // el primero es el idioma base
    "a11y":     "wcag-aa"
  },

  "commerce": { "contract": "1.0", "adapters": [] },

  "tokens": "tokens.json",           // valor por defecto
  "settings": { "schema": "settings.schema.json", "defaults": "settings.json" },  // valores por defecto

  "extiende": "base",                // opcional: hereda de otro tema (ver abajo)
  "migraciones": [                   // opcional: qué le pasó a cada ajuste entre versiones
    { "desde": "2", "renombra": { "design.accent": "design.primary" }, "elimina": ["design.oldFont"] }
  ],
  "license_check": { "product": "mi-tema", "gracia": 7 }  // solo en temas vendidos con licencia
}
CampoQué es
id / nameEl identificador (kebab-case, único) y el nombre que enseña el panel. Son cosas distintas: mascotas es el id, «Mascotas» el nombre.
niche, preview, screenshots, descriptionDatos del catálogo: filtrar y la ficha del tema.
demoRuta al contenido de ejemplo, dentro del tema. Por defecto demo/contenido.json.
runtime.hostsDominios de fuera que el tema necesita (CDN de imágenes, fuentes…). La auditoría avisa de cualquier dominio con el que hable el código y que no esté declarado aquí.
tokens, settingsRutas de los ficheros de diseño y del customizer, si no son las de por defecto.
extiendeId del tema padre. El hijo solo lleva lo que cambia; tokens, ajustes, secciones y plantillas salen del padre, fusionados por clave. La identidad (id, name, version) nunca se hereda.
migracionesLista de cambios por versión mayor (desde): renombra (viejo → nuevo, el viejo se conserva), elimina (se avisa, no se borra) y anade (ajustes nuevos con su valor, solo si faltan). Son datos, no un guion: actualizar nunca ejecuta código del tema.
license_checkSolo en temas vendidos con licencia: product, endpoint opcional (servidor de licencias, por defecto el de pcreative) y gracia, los días que sigue funcionando cuando el servidor de licencias no contesta — de 1 a 90, por defecto 7. El escaparate nunca se apaga por esto. Los gratuitos lo omiten.

Qué stacks corren. El schema deja stack como lista abierta, pero el panel solo instala y despliega los que sabe ejecutar: next, astro, nuxt, sveltekit, remix, react-router, vite-react, vue y eleventy. Con cualquier otro (Laravel o cualquier cosa en PHP, por ejemplo) el tema puede validar, pero el panel lo rechaza al instalarlo con «stack no soportado». Lo mismo con packageManager: npm, pnpm, yarn y bun. Con target static o spa el tema se construye y se sirve como ficheros, sin proceso propio.

runtime.commands es solo informativo. Documenta cómo se trabaja el tema a mano, pero no es lo que se ejecuta: los comandos reales los decide el panel a partir de stack y packageManager (instalando con los scripts desactivados). Si un tema pudiera dictar la orden, instalarlo sería ejecutar lo que quisiera su autor. pcc-theme audit enseña exactamente qué se ejecutaría.


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ónPara qué
dev.pcreative.deriveDeriva 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.cssFija el nombre de la variable emitida. Sirve para adoptar el contrato en un tema que ya existe sin tocar un solo componente.
dev.pcreative.privateEl token existe pero no se emite como variable CSS.

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

Schema: schema/settings.schema.json, dentro del paquete del contrato. 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

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 } }
]}}

Cuando el editor repinta una sección

Al cambiar un ajuste, el editor sustituye el HTML de esa sección y deja el resto de la página en paz. Quien está editando no pierde el desplazamiento ni el carrito, y no se vuelve a pedir el catálogo entero por tocar una palabra.

Eso tiene dos consecuencias que no dan ningún error, y que hay que conocer.

1. El JavaScript no se vuelve a ejecutar, y lo anterior no se deshace solo. Los ciclos de vida del framework (onMount, useEffect, onMounted) no se disparan: el HTML entra ya hecho. Hay dos eventos para eso, que salen sobre el nodo de la sección y burbujean:

eventocuándopara qué
pcc:seccion:descargadaantes de quitar el nodo viejoparar bucles, quitar observadores, liberar WebGL
pcc:seccion:cargadadespués de poner el nuevovolver a montar carruseles y animaciones

Los dos llevan detail: { id, el }. 🔴 Sin escuchar el de descarga, cada edición apila otro juego de escuchadores y otro bucle de animación sobre el anterior; a las diez ediciones la página se arrastra.

2. Hay secciones que no se pueden pintar solas. Un héroe que fija el scroll de toda la página, un lienzo WebGL compartido, un scroll suave o una línea de tiempo que abarca varias secciones no existen fuera de su página: sustituir su nodo no falla, deja algo pintado y roto. Se declara en el catálogo y el editor recarga la página para esa sección en vez de sustituirla:

{ "type": "hero-cinematografico", "name": "Héroe cinematográfico", "aislable": false }

Por defecto es true. Cuesta medio segundo más y enseña la verdad.


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

Schema: schema/override.schema.json, dentro del paquete del contrato.

{
  "contract": "2.0",
  "theme": "mascotas",
  "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)

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.

Qué puede pedir un tema, método a método: Contrato de comercio.


8. Herramientas

pcc-theme init     mi-tema --name "Mi Tema" --stack astro   # tema nuevo que ya vende y ya valida
pcc-theme validate themes/mascotas   # forma (JSON Schema) + coherencia entre ficheros
pcc-theme audit    themes/mascotas   # qué se ejecutaría, con qué dominios habla, bloqueantes
pcc-theme pack     themes/mascotas   # valida, audita, busca secretos, construye y hace el .zip
pcc-theme sign     themes/mascotas --key privada.pem --publisher "Tu estudio"
pcc-theme verify   themes/mascotas --pubkey publica.pem
pcc-theme css      themes/mascotas   # variables CSS ya resueltas
pcc-theme info     themes/mascotas   # resumen del tema

init crea el tema entero (ficheros del contrato, plantillas, textos, demo y código). Necesita una carpeta o --id; --stack admite next, astro, sveltekit, nuxt, react-router y vite-react (por defecto next).

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.

audit revisa el tema como lo haría el panel antes de instalarlo: enseña los comandos de instalar, construir y arrancar que se ejecutarían de verdad, lista los dominios de fuera con los que habla el código (avisando de los que no estén declarados en runtime.hosts) y separa los bloqueantes (sin fichero de bloqueo, scripts de instalación peligrosos, hallazgos graves en el código) de los avisos. Sale con error si hay algún bloqueante.

pack lo recorre todo en orden —contrato, auditoría, qué ficheros entran, secretos, dependencias portátiles (las rutas file: a @pcreative/* se empaquetan en vendor/), instalación y construcción reales— y escribe <id>-<version>.zip (--salida para cambiar el nombre). Con --key y --publisher además firma el paquete; --sin-construir se salta la construcción.

sign firma el tema con tu clave privada y deja la firma en theme.sig. Pide --key y --publisher a la vez; --kid nombra la clave (por defecto, 1). verify comprueba esa firma con una clave pública, de un fichero (--pubkey) o de una dirección (--url), y si no casa dice qué ficheros se han alterado, cuáles sobran y cuáles faltan. Así sabe quien instala que el tema es el que firmaste y que nadie lo ha tocado después.

Todos los comandos y sus opciones: Los comandos.


9. Migrar un tema de la 1.0

  1. theme.meta.json + la parte de identidad de theme.config.ts → theme.json.
  2. tokens de theme.config.ts → tokens.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.ts → settings.schema.json (los campos) y settings.json (los valores).
  5. theme.override.ts → theme.override.json.
  6. lib/commerce.ts → adaptador del contrato de datos.
  7. pcc-theme validate hasta que salga limpio.

mascotas está migrado y sirve de plantilla.