Temas
Construye, prueba y publica la tienda online como código con React Router y el CLI ecomiq.
Bajas el código de «Mi tienda», agregas una sección de recién llegados, la
pruebas en local con los productos reales y la publicas. El tema es un proyecto
React Router con el SDK de Ecomiq, y el CLI ecomiq lo baja y lo publica. Solo
ecomiq publish --live cambia lo que ven tus clientes.
Requisitos
- Node.js 22.22 o superior.
- Una cuenta de Ecomiq con acceso a la tienda. Si el tema es para otra tienda, pide al dueño que te agregue como miembro.
- El permiso «Ver páginas y menús» para
ecomiq inityecomiq dev. - El permiso «Publicar cambios de la tienda online» para
ecomiq publish. - El permiso «Editar páginas y menús» para importar un ZIP.
Instala el CLI
npm install -g ecomiq
ecomiq --version0.2.6| Comando | Qué hace |
|---|---|
ecomiq login [tienda] | Inicia sesión y elige la tienda en el navegador. |
ecomiq logout | Cierra la sesión en este equipo y en Ecomiq. |
ecomiq init [carpeta] | Baja el código de la tienda a una carpeta nueva. |
ecomiq dev | Levanta la tienda en local con sus datos reales. |
ecomiq pack [archivo] | Construye el tema y lo comprime en un ZIP. |
ecomiq publish | Construye y sube una vista previa. |
ecomiq publish --live | Construye, sube y publica en vivo. |
ecomiq --version | Muestra la versión del CLI. |
Si prefieres no instalar nada, npm create ecomiq@latest mi-tienda inicia sesión y baja el código de una vez. Después usa
npx ecomiq en lugar de ecomiq.
Inicia sesión
ecomiq login mi-tiendaSe abre el navegador. Inicia sesión, elige «Mi tienda» y pulsa Conectar. Cuando la página diga «Listo», vuelve a la terminal.
Si el navegador no se abre, entra a:
https://auth.ecomiq.pe/connect/authorize?client_id=ecomiq-cli&…
✓ Sesión iniciada · tienda mi-tiendaEl slug es opcional. Si lo pasas y eliges otra tienda en el navegador, el CLI cierra la sesión y te pide repetir.
La sesión se guarda en ~/ y se renueva sola.
Para cambiar de tienda, vuelve a ejecutar ecomiq login.
Prepara el proyecto
Un tema es un proyecto React Router 8 en modo framework, con React 19,
Tailwind CSS 4 y componentes shadcn. Los datos vienen del SDK
@ecomiq/; el diseño es tuyo.
Baja el código de la tienda a una carpeta nueva e instala sus dependencias:
ecomiq init mi-tienda
cd mi-tienda
npm install✓ /Users/tu-usuario/mi-tienda · 214 archivos
Sigue con: cd mi-tienda && npm install && npx ecomiq devinit baja el código que hoy tiene el editor de la tienda y le suma los
archivos de la plataforma. Sin carpeta, usa el slug de la tienda. La carpeta
debe estar vacía o no existir.
app/
root.tsx layout y loader raíz
routes.ts mapa de rutas
routes/ páginas
components/
sections/ secciones editables
ui/ componentes shadcn
app.css variables del tema y Tailwind
public/
images/ imágenes del tema
package.json| Archivo | Qué es | Lo editas |
|---|---|---|
app/ | Layout, cabecera, pie y loader raíz. | Sí |
app/ | Mapa de rutas. | Sí, con reglas |
app/ | Páginas: cargan datos y componen secciones. | Sí |
app/ | Secciones con ajustes editables. | Sí |
app/ | Componentes shadcn. | Sí |
app/ | Colores, radio y fuentes del tema. | Sí |
public/ | Imágenes del tema. | Sí |
package.json | Dependencias. | Solo las permitidas |
vite.config.ts, react-router.config.ts, workers/, wrangler.jsonc | Archivos de la plataforma. | No |
Usa ~/ para importar desde app/, como ~/.
Prueba en local
ecomiq dev✓ Tienda mi-tienda
➜ Local: http://localhost:5173/Abre http:/. Ves tu tema con los productos, precios y stock
de la tienda. Al guardar un archivo, la página se actualiza.
ecomiq dev guarda la conexión con la tienda en .dev.vars. No lo subas a tu
repositorio. Los argumentos extra pasan a react-router dev, por ejemplo
ecomiq dev --port 3000.
Los datos son reales
Un checkout completado en local crea un pedido real en la tienda.
Crea una sección
Una sección es un componente en app/. Exporta settings
con los textos, imágenes y productos que el comerciante puede cambiar en el
editor de Ecomiq.
import type { ProductSummary } from "@ecomiq/core";
import { money, setting, StorefrontLink } from "@ecomiq/storefront";
import { Link } from "react-router";
export const settings = {
arrivals: {
title: setting.text("Recién llegados", "Título"),
collection: setting.collection("novedades", "Colección"),
limit: setting.number(8, "Cantidad"),
link: setting.link("/collections/novedades", "Enlace"),
},
};
type Props = { products: ProductSummary[]; currency: string; language: string };
export function NewArrivals({ products, currency, language }: Props) {
if (products.length === 0) return null;
return (
<section className="mx-auto max-w-7xl px-4 py-16 md:px-8">
<div className="flex items-end justify-between gap-4">
<h2 className="font-heading text-3xl">{settings.arrivals.title}</h2>
<StorefrontLink
to={settings.arrivals.link}
className="text-sm underline"
>
Ver todo
</StorefrontLink>
</div>
<ul className="mt-8 grid grid-cols-2 gap-6 md:grid-cols-4">
{products.map((product) => (
<li key={product.id} className="flex flex-col gap-2">
<img
src={product.imageUrl}
alt={product.name}
className="aspect-[4/5] w-full rounded-lg bg-muted object-cover"
/>
<Link to={product.url} className="font-medium">
{product.name}
</Link>
<p className="text-sm text-muted-foreground">
{money(product.price, currency, language)}
</p>
</li>
))}
</ul>
</section>
);
}| Ajuste | Guarda |
|---|---|
setting.text | Un texto. |
setting.image | La ruta de una imagen, como /. |
setting.link | Un enlace. Úsalo con StorefrontLink. |
setting.collection | El slug de una colección. |
setting.category | El slug de una categoría. |
setting.product | El slug de un producto. |
setting.products | Una lista de slugs de productos. |
setting.number | Un número. |
setting.toggle | Sí o no. |
- Agrupa los ajustes por sección:
settings.<grupo>.<campo>. - El primer argumento es el valor inicial y debe ser literal. El segundo es la etiqueta que ve el comerciante.
- El editor guarda los cambios del comerciante en ese mismo archivo.
- Si la sección no recibe props obligatorias, el comerciante puede agregarla a cualquier página desde el editor.
Carga los datos
Cada ruta carga sus datos en el loader. store(context) devuelve el cliente
de la tienda. Los precios, el stock y los productos salen siempre de ahí.
import { pickProducts, store } from "@ecomiq/storefront";
import type { Route } from "./+types/home";
import {
NewArrivals,
settings as newArrivals,
} from "~/components/sections/new-arrivals";
export async function loader({ context }: Route.LoaderArgs) {
const client = store(context);
const { arrivals } = newArrivals;
const [shop, products] = await Promise.all([
client.getSettings(),
pickProducts(client, {
collection: arrivals.collection,
limit: arrivals.limit,
}),
]);
return {
products,
currency: shop.store.currency,
language: shop.store.language,
};
}
export default function Home({ loaderData }: Route.ComponentProps) {
return <NewArrivals {...loaderData} />;
}El loader lee los ajustes de la sección. Si el comerciante cambia la colección,
la página muestra otros productos. pickProducts acepta collection,
category, products, limit y sort.
Pide los datos a la vez
Cada lectura que espera el resultado de otra suma un viaje a la API. Desde
@ecomiq/ 3.3.0, el SDK manda en una sola consulta las lecturas que
el loader pide a la vez, como getSettings y pickProducts en el ejemplo.
-
Junta con
Promise.alllas lecturas que no dependen entre sí. -
Si la sección tiene un respaldo, pide las dos listas juntas y elige después:
const [chosen, bestSelling] = await Promise.all([ pickProducts(client, { collection: arrivals.collection, limit: 8 }), pickProducts(client, { limit: 8, sort: "best_selling" }), ]); const products = chosen.length > 0 ? chosen : bestSelling; -
Para los productos de una colección no leas antes la lista:
pickProductsla busca por su slug. -
Cuando necesitas la colección y algo de sus productos, pídelo en la misma lectura:
| Necesitas | Usa |
|---|---|
| Colecciones con foto de portada | client.getCollectionCovers({ limit }) y withImages |
| La primera colección o las que sobran, con sus productos | client.getCollectionsWithProducts({ limit, products, sort }) |
| Productos con sus reseñas | pickProductsWithReviews(client, fuente, { limit, sortBy, sortDirection }) |
| Vendedores de un marketplace | client.getSettings() y client.getSellers() a la vez |
El carrito usa loadCart y cartAction en /:
import { cartAction, loadCart } from "@ecomiq/storefront";
import type { Route } from "./+types/cart";
export async function loader({ request, context }: Route.LoaderArgs) {
return loadCart(request, context);
}
export async function action({ request, context }: Route.ActionArgs) {
return cartAction(request, context);
}Agrega al carrito con un formulario que envía a /:
import { CustomerCsrfField } from "@ecomiq/storefront";
import { useFetcher } from "react-router";
import { Button } from "~/components/ui/button";
export function AddToCart({ variantId }: { variantId: string }) {
const fetcher = useFetcher();
return (
<fetcher.Form method="post" action="/cart" className="flex gap-3">
<CustomerCsrfField />
<input type="hidden" name="intent" value="add" />
<input type="hidden" name="variantId" value={variantId} />
<input
type="number"
name="quantity"
defaultValue={1}
min={1}
className="w-20 rounded-md border px-3"
/>
<Button type="submit" disabled={fetcher.state !== "idle"}>
Agregar al carrito
</Button>
</fetcher.Form>
);
}intent | Campos | Qué hace |
|---|---|---|
add | variantId o product, quantity, next | Agrega la variante. Con next=/, va al checkout. |
buy | variantId o product, quantity | Agrega y va al checkout. |
quick | variantId o product | Agrega una unidad. |
update | itemId, quantity | Cambia la cantidad. |
remove | itemId | Quita el artículo. |
product es el slug: el SDK elige la primera variante disponible. La cantidad
máxima es 999. Si falla, la acción devuelve ok: false y un message para
mostrar.
Define el estilo
Los colores, el radio y las fuentes viven en :root de app/. El
editor de Ecomiq cambia estos valores sin tocar tus componentes.
:root {
--radius: 0.25rem;
--background: #f6f6f5;
--foreground: #1c1c1b;
--card: #ffffff;
--card-foreground: #1c1c1b;
--popover: #ffffff;
--popover-foreground: #1c1c1b;
--primary: #1f1f1e;
--primary-foreground: #fafafa;
--secondary: #e8e8e6;
--secondary-foreground: #1c1c1b;
--muted: #efefed;
--muted-foreground: #6b6b69;
--accent: #e4e4e2;
--accent-foreground: #1c1c1b;
--destructive: #b3432f;
--border: #e0e0dd;
--input: #d4d4d1;
--ring: #1f1f1e;
--heading: "Playfair Display", ui-serif, Georgia, serif;
--body: "Outfit", ui-sans-serif, system-ui, sans-serif;
}El bloque @theme inline del mismo archivo las convierte en clases:
bg-primary, text-muted-foreground, border-border, rounded-lg y
font-heading.
Carga las fuentes de --heading y --body desde Google Fonts en links de
app/. Así el editor también puede cambiarlas.
export const links: Route.LinksFunction = () => [
{ rel: "preconnect", href: "https://fonts.googleapis.com" },
{
rel: "preconnect",
href: "https://fonts.gstatic.com",
crossOrigin: "anonymous",
},
{
rel: "stylesheet",
href: "https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400..800&family=Outfit:wght@400..800&display=swap",
},
];- Usa las clases de las variables del tema, como
bg-primary. Un color fijo comobg-[#ff0000]no cambia cuando el comerciante edita el estilo. - Diseña primero para móvil y agrega
md:para escritorio. - Guarda las imágenes en
public/, en webp y con menos de 1 MB. Úsalas comoimages/ /y conimages/ <archivo> alt.
Publica una vista previa
ecomiq publishEl CLI construye el tema en tu equipo, sube el build y el código, y espera a que la versión esté lista. Si el build falla, no sube nada.
Subiendo…
Desplegando…
✓ Vista previa lista: https://mi-tienda.ecomiq.app/?preview=q7m2vk4xh5c3n6p3r2t4w5y7za- Al abrir el enlace, tu navegador ve esa versión durante una hora.
- Tus clientes siguen viendo la versión en vivo.
- La vista previa usa los datos reales de la tienda.
- Para volver a la versión en vivo, abre la tienda con
?preview=off.
publish no sube node_modules, build, .dev.vars, los archivos .env ni
los archivos ocultos de la raíz, como .git o .npmrc; de esos solo sube
.gitignore. Del build tampoco sube los .map. El build puede pesar hasta
64 MB comprimido y el código, hasta 32 MB.
Ponla en vivo
ecomiq publish --liveSubiendo…
Desplegando…
✓ En vivo en mi-tienda--live construye y sube de nuevo. Para poner en vivo una vista previa que ya
subiste, o para volver a una versión anterior, entra a Tienda online →
Personalizar tienda → Historial → Publicadas y elige Poner en vivo.
El editor tiene su propio borrador
Publicar con el CLI no cambia el borrador del editor de Ecomiq. Si alguien publica desde el editor, reemplaza tu versión. Para que el editor parta de tu código, elige Restaurar código en esa versión.
Importa el tema como ZIP
Para instalar el tema en una tienda sin publicarlo con el CLI, empaquétalo:
ecomiq pack✓ /Users/tu-usuario/temas/mi-tienda.zip · 131 archivos
Impórtalo en Tienda online → Personalizar tienda → Tema → Cambiar → Importar ZIP.pack construye el tema y, si el build pasa, guarda <carpeta>.zip junto a la
carpeta del proyecto. Para elegir otro destino, pásalo como argumento:
ecomiq pack ~/. No necesita sesión. El ZIP deja fuera
lo mismo que publish.
En Ecomiq, entra a Tienda online → Personalizar tienda → Tema → Cambiar, pulsa Importar ZIP, elige el archivo y confirma. El editor reemplaza el código de la tienda con el del ZIP, y puedes deshacerlo desde Historial. Tus clientes siguen viendo la versión en vivo hasta que publiques.
- También sirve un ZIP de la carpeta del tema comprimido a mano. El editor
ignora
node_modules,build,.dev.vars, los archivos.env, los metadatos de macOS y los archivos de la plataforma. - El ZIP debe tener
package.json,app/yroot.tsx app/, y cumplir las reglas del tema.routes.ts - Un tema importado es código propio: no recibe las actualizaciones del tema de la galería.
Rutas y SDK
Cada ruta del tema llama al SDK. Puedes cambiar el diseño de cualquier página; conserva el loader y la acción.
| Ruta | Archivo | SDK |
|---|---|---|
/ | routes/ | store, pickProducts |
/ | routes/ | readCatalog |
/ | routes/ | loadProduct, canonicalUrl, requireCanonical |
/ | routes/ | found, readCatalog |
/ | routes/ | loadPage |
/ | routes/ | loadCart, cartAction |
/ | routes/ | loadCheckout, checkoutAction |
/ | routes/ | loadOrderStatus, orderPaymentAction |
/ | routes/ | loadOrderDraft, orderDraftAction |
/ | routes/ | loadTracking |
/ | routes/ | loadReview, reviewAction |
/ | routes/ | startCustomerLogin, submitCustomerLogin |
/ | routes/ | loadCustomerRegistration, submitCustomerRegistration |
/ | routes/ | completeCustomerLogin |
/ | routes/ | loadAccount |
/ | routes/ | loadAddresses, addressAction |
/ | routes/ | loadProfile, profileAction |
* | routes/ | resolveUrl |
Los correos y las pasarelas de pago enlazan a /, /, / y
/. Conserva esas rutas.
| Tipo | Nombres |
|---|---|
| Hooks | useCheckout, useFacets, useSort, usePagination, useSearchSuggestions, useGeoCascade, useOrderPayment |
| Componentes | StorefrontLink, StorefrontAnchor, CustomerLoginForm, CustomerRegistrationForm, CustomerCsrfField, PaymentLogo, OrderPaymentContinuation |
| Utilidades | money, discountPercent, pickVariant, isVariantAvailable, variantOptions, variantSwatches |
| Cliente | store(context): getProducts, getProductBySlug, getCollections, getCollectionBySlug, getSettings |
El loader de app/ devuelve storefrontLinks y customerCsrfToken.
StorefrontLink y CustomerCsrfField los leen de ahí.
Reglas del tema
El editor de Ecomiq abre y publica tu código con estas reglas. Si no las
cumples, ecomiq publish puede funcionar, pero el editor no podrá abrir tu
código.
| Regla | Detalle |
|---|---|
| Dependencias | Solo react, react-dom, react-router, @ecomiq/, @ecomiq/, radix-ui, @radix-ui/, @radix-ui/, @radix-ui/, class-variance-authority, clsx, tailwind-merge, tw-animate-css, embla-carousel-react, sonner, vaul, zod, isbot y los íconos lucide-react, @phosphor-icons/, @heroicons/, @remixicon/, iconoir-react, @mdi/ y @mdi/. |
| Archivos de la plataforma | No edites vite.config.ts, react-router.config.ts, workers/ ni wrangler.jsonc: el editor usa los suyos. |
| Otra configuración | No agregues en la raíz postcss.config.*, tailwind.config.*, babel.config.*, .babelrc, .postcssrc ni vitest.config.*. |
app/ | Solo index, route, layout y prefix de @react-router/, con valores literales. |
| CSS | Sin @plugin ni @config. |
| Archivos | Hasta 1 MB cada uno y 2000 en total. |
| Formularios | Todo formulario post lleva <CustomerCsrfField /. Sin él, el SDK rechaza el envío de un cliente con sesión. |
Errores frecuentes
El CLI muestra los errores con ✗ y termina con código 1.
| Mensaje | Qué hacer |
|---|---|
No has iniciado sesión. Ejecuta `ecomiq login`. | Inicia sesión. |
La sesión expiró. Ejecuta `ecomiq login`. | Vuelve a iniciar sesión. |
Ejecuta este comando dentro de tu proyecto (falta react-router.config.ts). | Entra a la carpeta del tema. |
La carpeta … no está vacía. | Usa una carpeta nueva con ecomiq init. |
La tienda … todavía no tiene tema. Elige uno en Ecomiq. | Elige un tema en Tienda online → Personalizar tienda. |
Instala las dependencias del proyecto (npm install). | Ejecuta npm install. |
Autorizaste la tienda …, no …. | Vuelve a iniciar sesión y elige la tienda correcta. |
No tienes permiso para esta operación. | Pide el permiso que falta. Ver Requisitos. |
build terminó con código 1. | Corrige el error del build que aparece arriba. |
El build excede el tamaño permitido. | Reduce imágenes y dependencias. |
El despliegue tardó demasiado. Revisa su estado en Ecomiq. | Revisa Historial → Publicadas en el editor. |
Siguientes pasos
- Storefront API: todas las lecturas y escrituras de la tienda.
- Tienda headless: el mismo flujo de compra en tu propio frontend.
- Skills para agentes: instala el skill de temas en tu agente de código.
Última actualización: