Saltar al contenido
Tienda online

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 init y ecomiq 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 --version
0.2.6
ComandoQué hace
ecomiq login [tienda]Inicia sesión y elige la tienda en el navegador.
ecomiq logoutCierra la sesión en este equipo y en Ecomiq.
ecomiq init [carpeta]Baja el código de la tienda a una carpeta nueva.
ecomiq devLevanta la tienda en local con sus datos reales.
ecomiq pack [archivo]Construye el tema y lo comprime en un ZIP.
ecomiq publishConstruye y sube una vista previa.
ecomiq publish --liveConstruye, sube y publica en vivo.
ecomiq --versionMuestra 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-tienda

Se 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-tienda

El 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 ~/.config/ecomiq/credentials.json 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/storefront; 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 dev

init 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
ArchivoQué esLo editas
app/root.tsxLayout, cabecera, pie y loader raíz.Sí
app/routes.tsMapa de rutas.Sí, con reglas
app/routes/*Páginas: cargan datos y componen secciones.Sí
app/components/sections/*Secciones con ajustes editables.Sí
app/components/ui/*Componentes shadcn.Sí
app/app.cssColores, radio y fuentes del tema.Sí
public/images/*Imágenes del tema.Sí
package.jsonDependencias.Solo las permitidas
vite.config.ts, react-router.config.ts, workers/app.ts, wrangler.jsoncArchivos de la plataforma.No

Usa ~/ para importar desde app/, como ~/components/ui/button.

Prueba en local

ecomiq dev
✓ Tienda mi-tienda
  ➜  Local:   http://localhost:5173/

Abre http://localhost:5173. 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/components/sections/. Exporta settings con los textos, imágenes y productos que el comerciante puede cambiar en el editor de Ecomiq.

app/components/sections/new-arrivals.tsx
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>
  );
}
AjusteGuarda
setting.textUn texto.
setting.imageLa ruta de una imagen, como /images/hero.webp.
setting.linkUn enlace. Úsalo con StorefrontLink.
setting.collectionEl slug de una colección.
setting.categoryEl slug de una categoría.
setting.productEl slug de un producto.
setting.productsUna lista de slugs de productos.
setting.numberUn número.
setting.toggleSí 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í.

app/routes/home.tsx
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/storefront 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.all las 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: pickProducts la busca por su slug.

  • Cuando necesitas la colección y algo de sus productos, pídelo en la misma lectura:

NecesitasUsa
Colecciones con foto de portadaclient.getCollectionCovers({ limit }) y withImages
La primera colección o las que sobran, con sus productosclient.getCollectionsWithProducts({ limit, products, sort })
Productos con sus reseñaspickProductsWithReviews(client, fuente, { limit, sortBy, sortDirection })
Vendedores de un marketplaceclient.getSettings() y client.getSellers() a la vez

El carrito usa loadCart y cartAction en /cart:

app/routes/cart.tsx
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 /cart:

app/components/add-to-cart.tsx
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>
  );
}
intentCamposQué hace
addvariantId o product, quantity, nextAgrega la variante. Con next=/checkout, va al checkout.
buyvariantId o product, quantityAgrega y va al checkout.
quickvariantId o productAgrega una unidad.
updateitemId, quantityCambia la cantidad.
removeitemIdQuita 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/app.css. El editor de Ecomiq cambia estos valores sin tocar tus componentes.

app/app.css
: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/root.tsx. Así el editor también puede cambiarlas.

app/root.tsx
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 como bg-[#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/images/, en webp y con menos de 1 MB. Úsalas como /images/<archivo> y con alt.

Publica una vista previa

ecomiq publish

El 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 --live
Subiendo…
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 ~/Escritorio/mi-tema.zip. 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/root.tsx y app/routes.ts, y cumplir las reglas del tema.
  • 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.

RutaArchivoSDK
/routes/home.tsxstore, pickProducts
/productsroutes/products.tsxreadCatalog
/products/:handleroutes/product.tsxloadProduct, canonicalUrl, requireCanonical
/collections/:handleroutes/collection.tsxfound, readCatalog
/pages/:handleroutes/page.tsxloadPage
/cartroutes/cart.tsxloadCart, cartAction
/checkoutroutes/checkout.tsxloadCheckout, checkoutAction
/orders/:tokenroutes/order-status.tsxloadOrderStatus, orderPaymentAction
/drafts/:tokenroutes/draft.tsxloadOrderDraft, orderDraftAction
/track/:code?routes/track.tsxloadTracking
/review/:tokenroutes/review.tsxloadReview, reviewAction
/account/loginroutes/account/login.tsxstartCustomerLogin, submitCustomerLogin
/account/registerroutes/account/register.tsxloadCustomerRegistration, submitCustomerRegistration
/account/callbackroutes/account/callback.tscompleteCustomerLogin
/accountroutes/account/layout.tsxloadAccount
/account/addressesroutes/account/addresses.tsxloadAddresses, addressAction
/account/profileroutes/account/profile.tsxloadProfile, profileAction
*routes/resolve.tsxresolveUrl

Los correos y las pasarelas de pago enlazan a /orders, /drafts, /track y /review. Conserva esas rutas.

TipoNombres
HooksuseCheckout, useFacets, useSort, usePagination, useSearchSuggestions, useGeoCascade, useOrderPayment
ComponentesStorefrontLink, StorefrontAnchor, CustomerLoginForm, CustomerRegistrationForm, CustomerCsrfField, PaymentLogo, OrderPaymentContinuation
Utilidadesmoney, discountPercent, pickVariant, isVariantAvailable, variantOptions, variantSwatches
Clientestore(context): getProducts, getProductBySlug, getCollections, getCollectionBySlug, getSettings

El loader de app/root.tsx 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.

ReglaDetalle
DependenciasSolo react, react-dom, react-router, @ecomiq/storefront, @ecomiq/core, radix-ui, @radix-ui/react-dialog, @radix-ui/react-label, @radix-ui/react-slot, class-variance-authority, clsx, tailwind-merge, tw-animate-css, embla-carousel-react, sonner, vaul, zod, isbot y los íconos lucide-react, @phosphor-icons/react, @heroicons/react, @remixicon/react, iconoir-react, @mdi/react y @mdi/js.
Archivos de la plataformaNo edites vite.config.ts, react-router.config.ts, workers/app.ts ni wrangler.jsonc: el editor usa los suyos.
Otra configuraciónNo agregues en la raíz postcss.config.*, tailwind.config.*, babel.config.*, .babelrc, .postcssrc ni vitest.config.*.
app/routes.tsSolo index, route, layout y prefix de @react-router/dev/routes, con valores literales.
CSSSin @plugin ni @config.
ArchivosHasta 1 MB cada uno y 2000 en total.
FormulariosTodo 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.

MensajeQué 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

Última actualización:

En esta página