Proyectos

05 / 05Caso de estudio

Delicaté 4.0

E-commerce y sitio de marca

Jabones artesanales de República Dominicana, en línea con dominio propio: catálogo administrable en Django, carrito sin cuenta que se mantiene al día y pedidos que se cierran por WhatsApp.

Portada de Delicaté con el titular «Cuidado hecho con amor» y jabones artesanales con coco, naranja y avenaCarrito en un teléfono con tres productos y el botón verde Finalizar por WhatsApp
Mi papel
Levantamiento de necesidades, UX/UI, desarrollo full-stack y despliegue
Stack
React 19 · Vite 8 · Django 5.2 · Django REST Framework · PostgreSQL · Docker · Railway
Estado
En línea con dominio propio · salida comercial en validación

Alcance

  • 18pruebas automatizadas, en CI en cada push
  • 0cuentas necesarias para comprar
  • 68 KBde JavaScript en producción, con gzip

El reto

El negocio necesitaba presentar su catálogo con una identidad propia y recibir pedidos bien formados sin adoptar pagos en línea ni una operación logística que no existe detrás.

Lo que construí
Rediseñé y reconstruí la tienda completa —API de catálogo en Django REST, administración del inventario y una tienda responsive en React— y la llevé a producción en Railway con dominio propio, integración continua y 18 pruebas.
Decisión técnica clave
Retiré el registro de clientes, el carrito en servidor y el checkout de la versión anterior, y convertí el carrito en un pedido estructurado por WhatsApp: el mismo canal donde el negocio ya confirma, cobra y entrega.

Decisiones de diseño

  1. Portada de Delicaté con el titular «Cuidado hecho con amor» y jabones artesanales con coco, naranja y avena
    Problema
    El precio de un jabón artesanal se sostiene con la marca, no con un listado.
    Decisión
    La portada presenta la marca antes que la tienda, con una sola acción principal.
  2. Ficha de Cacao & Almendras con precio, existencias, beneficio, tipo de piel e ingredientes
    Problema
    Antes de comprar, el cliente pregunta por beneficio, tipo de piel e ingredientes.
    Decisión
    La ficha lo responde en un diálogo nativo, sin salir del catálogo.
  3. Carrito con Avena Calma, Corazón de Lavanda y Café Despierto y un total estimado de RD$1,575
    Problema
    Un checkout que no puede cobrar es una promesa rota.
    Decisión
    El carrito arma el pedido, aclara que no cobra y lo cierra en WhatsApp.
  4. Carrito con el aviso de actualización de precios y disponibilidad y Avena Calma limitada a 12 unidades
    Problema
    Un carrito guardado días antes puede enviar precios viejos o cantidades sin existencias.
    Decisión
    Al volver, el carrito se actualiza con el catálogo real y avisa de lo que cambió.
  5. Tarjeta de Arcilla & Avena en móvil con el filtro Botánicos activo y el botón Agregar visible
    Problema
    En una pantalla táctil no hay hover que descubra el botón de compra.
    Decisión
    En táctil «Agregar» queda siempre visible y los filtros se desplazan en una fila.
  6. Catálogo filtrado Para regalar con Corazón de Lavanda y Flor de Ámbar marcado como agotado
    Problema
    Si un jabón agotado desaparece del catálogo, quien lo buscaba no sabe si volverá.
    Decisión
    Sigue a la vista con «Agotado» y la compra desactivada, igual en la tarjeta que en la ficha.
  7. Mensaje de catálogo no disponible con el botón Volver a intentar
    Problema
    Si la API falla, mostrar productos inventados engaña al comprador.
    Decisión
    Producción muestra un error con reintento; el catálogo de demostración sólo existe en desarrollo.
  8. Administración de Django con diez productos y precio, existencias, destacado y activo editables
    Problema
    El negocio tenía que cambiar su catálogo sin tocar código ni recompilar.
    Decisión
    Precio, existencias, destacado y visibilidad se editan en la propia lista del administrador.

Sistema

Elige un módulo: se enciende su ruta.Elige un módulo: debajo, su decisión.

Arquitectura18 módulos · 19 conexiones

La marca antes que la tienda: una sola acción principal lleva a la colección.

React 19 · CSS propio

Se lee entero de la API, siguiendo todas sus páginas, y se filtra en el navegador; los filtros declaran su estado con aria-pressed.

fetch con AbortController · React 19

Un diálogo nativo responde beneficio, piel, ingredientes y existencias sin salir del catálogo.

<dialog> nativo · React 19

Sin cuenta. Al volver se cruza con el catálogo: toma el precio actual, limita cada cantidad a las existencias y avisa de lo que cambió.

useReducer · Hook propio useCart

Pensada para el teléfono: en táctil «Agregar» siempre está visible y el carrito ocupa la pantalla entera.

@media (hover: none) · CSS propio

El formulario no guarda nada en el servidor: convierte la consulta en un mensaje de WhatsApp.

React 19

Pública y de sólo lectura: listado paginado con filtros y ficha por slug. El catálogo sólo se escribe desde el administrador.

Django REST Framework 3.17 · Paginación hasta 100

Precio, existencias, destacado y visibilidad se editan en la propia lista; las fotos se validan (JPG, PNG o WebP, hasta 5 MB).

Django Admin · Pillow 12.3

CSP y demás cabeceras en toda respuesta, HSTS de un año y límites de frecuencia que cuentan la IP real detrás del proxy.

Middleware propio · Throttling de DRF

El carrito vive en el navegador; el servidor nunca guarda pedidos ni datos de compradores.

localStorage

Cuatro modelos, frente a ocho en 2024: productos, mensajes, suscripciones y el usuario del equipo.

PostgreSQL · psycopg 3.3 · dj-database-url 3.1

Las fotos subidas viven en un volumen persistente; Django no arranca si su ruta es relativa.

Volumen de Railway · Caché de 7 días

En cada push: check de Django, migraciones pendientes, las 18 pruebas, lint y build.

GitHub Actions

Imagen multietapa: Node construye la tienda y Python la sirve, con DEBUG apagado por defecto.

node:24-alpine · python:3.13-slim

Migra antes de publicar y sólo da tráfico a la versión nueva si el healthcheck, que consulta la base, responde 200.

Railway · railway.json

Un solo proceso sirve tienda, API, administración y fotos desde el mismo origen: sin CORS.

Gunicorn 23 · WhiteNoise 6.12

El pedido sale como mensaje prearmado por wa.me, en el canal donde el negocio ya confirma, cobra y entrega.

Enlace wa.me

Al compartir el enlace, una imagen de 1200×630 y la URL canónica salen de la configuración del build.

Open Graph · Twitter Card

Inspector18 decisiones

Cliente

Portada

La marca antes que la tienda: una sola acción principal lleva a la colección.

Tecnologías

43 herramientas en 10 áreas. En el sistema, cada módulo dice las suyas.

Lenguajes

  • Python 3.13
  • JavaScript (JSX)
  • CSS
  • HTML

Frontend

  • React 19.2
  • Vite 8.2
  • Rolldown 1.2
  • @vitejs/plugin-react 6.0

Interfaz

  • CSS propio sin framework
  • Lightning CSS 1.33
  • Tipografía del sistema
  • 8 iconos SVG en línea
  • <dialog> nativo

Estado en el navegador

  • useReducer y un hook propio
  • localStorage
  • fetch con AbortController

Backend

  • Django 5.2 LTS
  • Django REST Framework 3.17
  • Django Admin
  • django-cors-headers 4.9
  • Pillow 12.3
  • python-dotenv 1.2

Datos

  • PostgreSQL
  • SQLite
  • psycopg 3.3
  • dj-database-url 3.1

Servidor y despliegue

  • Docker multietapa
  • Gunicorn 23
  • WhiteNoise 6.12
  • Railway
  • Volumen persistente
  • Dominio propio con TLS

Integraciones

  • Enlace wa.me de WhatsApp
  • Open Graph y Twitter Card

Calidad y CI

  • Test runner de Django
  • APITestCase de DRF
  • ESLint 10.8
  • eslint-plugin-react-hooks 7.1
  • GitHub Actions

Herramientas

  • Node.js 24
  • npm
  • Git
  • Railway CLI

Resultados verificables

  • En línea en delicate.jonasjavier.dev como un solo contenedor Docker en Railway, con PostgreSQL y un volumen para las fotos
  • Catálogo administrable desde Django, sin tocar código ni recompilar la tienda
  • Carrito sin cuenta que se guarda en el navegador y, al volver, se actualiza con precios y existencias reales
  • Pedido completo, con cantidades y total estimado, prearmado para WhatsApp; el sitio no cobra
  • Migraciones antes de publicar, healthcheck con base de datos, CSP y límites de frecuencia
  • 18 pruebas automatizadas y una CI que comprueba migraciones, lint y build en cada push

Recorrido por módulos

30 pantallas en 7 módulos.

Portada y marca3 pantallas

  • Portada de Delicaté con el titular «Cuidado hecho con amor» y jabones artesanales con coco, naranja y avena

    La portada vende la marca antes que el producto, con una sola acción principal.

  • Sección Nuestra historia con una foto de jabones cortándose a mano y el texto de la marca

    La historia de la marca respalda el precio de lo artesanal.

  • Bloque verde con la frase Cada barra guarda el tiempo, el cuidado y el amor que ponemos en crearla

    Una pausa editorial refuerza el tono de la marca.

Catálogo y ficha6 pantallas

  • Catálogo de Delicaté con filtros por categoría y tres jabones marcados como favoritos

    El catálogo se carga desde Django y se filtra al instante.

  • Catálogo filtrado por Botánicos con Arcilla & Avena, Café Despierto y Jardín Botánico

    Cada tarjeta resume precio, tipo de piel y peso antes de abrir la ficha.

  • Tarjeta de Cacao & Almendras con el botón Agregar sobre la foto al pasar el ratón

    En escritorio, «Agregar» aparece al pasar el ratón sobre la foto.

  • Ficha de Cacao & Almendras con precio, existencias, beneficio, tipo de piel e ingredientes

    La ficha responde beneficio, piel, ingredientes y existencias sin salir del catálogo.

  • Catálogo filtrado Para regalar con Corazón de Lavanda y Flor de Ámbar marcado como agotado

    Un producto sin existencias sigue a la vista, pero no se puede agregar.

  • Ficha de Flor de Ámbar con la etiqueta Temporalmente agotado y el botón deshabilitado

    La ficha avisa que está agotado y desactiva la compra.

Carrito y pedido3 pantallas

  • Carrito con Avena Calma, Corazón de Lavanda y Café Despierto y un total estimado de RD$1,575

    El carrito arma el pedido y aclara que el sitio no cobra.

  • Carrito lateral vacío con el mensaje Elige algo hecho con amor

    El carrito vacío invita a volver a la colección.

  • Carrito con el aviso de actualización de precios y disponibilidad y Avena Calma limitada a 12 unidades

    Al volver, el carrito se actualiza con los precios y las existencias de hoy.

Cómo se pide3 pantallas

  • Sección Pedir es muy fácil con tres pasos numerados

    Tres pasos explican cómo se pide sin un checkout.

  • Preguntas frecuentes con la primera respuesta desplegada

    Las preguntas resuelven dudas antes de escribir por WhatsApp.

  • Formulario de contacto con nombre y consulta y el botón Enviar por WhatsApp

    El formulario convierte una consulta en un mensaje de WhatsApp.

Administración2 pantallas

  • Administración de Django con diez productos y precio, existencias, destacado y activo editables

    Precio, existencias y visibilidad se cambian desde la propia lista.

  • Lista de productos del administrador filtrada por Botánicos con tres resultados

    El administrador filtra por categoría, destacado y estado.

En el teléfono11 pantallas

  • Carrito en un teléfono con tres productos y el botón verde Finalizar por WhatsApp

    El pedido sale por WhatsApp desde el mismo teléfono.

  • Carrito en móvil con el aviso Actualizamos tu pedido con los precios y la disponibilidad de hoy

    El aviso explica el cambio antes de enviar el pedido.

  • Tarjeta de Arcilla & Avena en móvil con el filtro Botánicos activo y el botón Agregar visible

    En táctil, «Agregar» siempre está visible y los filtros se deslizan.

  • Ficha de Cacao & Almendras en móvil con foto, precio y beneficios apilados

    En el teléfono la ficha se apila y se desplaza dentro del diálogo.

  • Parte final de la ficha en móvil con ingredientes y el botón Agregar al carrito

    La acción de compra cierra la ficha a todo el ancho.

  • Portada de Delicaté en un teléfono con el titular y el botón Descubrir la colección

    El titular y la acción principal quedan al alcance del pulgar.

  • Menú desplegable en móvil con Colección, Nuestra historia, Proceso, Preguntas y Hablemos

    El menú se cierra con Escape o al elegir un destino.

  • Sección Nuestra historia en móvil con la foto del corte de jabones

    La historia se lee de arriba abajo.

  • Pasos para pedir en Delicaté apilados en un teléfono

    Los tres pasos para pedir, apilados.

  • Mensaje de catálogo no disponible en móvil con el botón Volver a intentar

    El error se entiende y se resuelve igual en el teléfono.

  • Mensaje de categoría vacía en móvil con el enlace Ver todos

    El estado vacío también está resuelto en el teléfono.

9 min de lectura

El caso completo

Contexto

Delicaté es una marca de jabones artesanales hechos en pequeñas tandas en República Dominicana, y el sitio lo hice para ella. Vende por conversación: el cliente pregunta, elige y coordina la entrega por WhatsApp. El sitio tenía que presentar la marca y el catálogo con una experiencia propia y llevar al cliente a esa conversación con el pedido ya armado.

El repositorio conserva dos generaciones. La de 2024 era una tienda convencional: registro e inicio de sesión con Google, carrito guardado en el servidor, reseñas, historial de pedidos y blog. La 4.0, reconstruida en agosto de 2026 y en producción desde septiembre, parte de preguntar qué necesitaba de verdad el negocio.

Problema

Una tienda con cuentas, checkout, pagos e inventario promete una operación que este negocio no tiene. Pedir registro para comprar un jabón añade fricción sin aportar nada, y un checkout que no puede cobrar es una promesa rota. Lo que sí hacía falta era mostrar bien los productos, ayudar a elegir y recibir un pedido claro, porque la confirmación, el cobro y la entrega seguirían ocurriendo por WhatsApp.

Mi función

Levanté las necesidades con el negocio, definí la arquitectura de información y el recorrido, diseñé la interfaz y desarrollé la tienda, la API y la administración. También la desplegué con su dominio y escribí la documentación técnica, la lista de salida a producción y la especificación de una futura línea de jabones personalizados.

Usuarios

El comprador explora la colección, filtra por categoría, abre la ficha de cada jabón y arma su pedido sin crear una cuenta. El equipo del negocio mantiene productos, precios, existencias, fotografías y destacados desde el administrador de Django, y recibe en WhatsApp un mensaje legible con cada producto, su cantidad y el total.

Restricciones

  • Sin pagos, sin logística y sin prometer existencias en tiempo real. Por eso el total del carrito es «estimado».
  • Un catálogo que cambia sin recompilar nada. La tienda lo lee de la API en cada visita.
  • Primero el teléfono. Es donde compra el público de la marca y donde vive WhatsApp.
  • Ningún dato de compradores. La tienda no guarda perfiles, direcciones ni pedidos.

Cómo lo construí

Partí del recorrido comercial real —descubrir la marca, encontrar un producto, entender sus beneficios, armar el pedido y abrir la conversación— y retiré todo lo que no servía a ese recorrido. La reconstrucción eliminó los modelos de la versión anterior con migraciones explícitas (carrito, líneas de carrito, reseñas y perfiles de usuario) y añadió una migración de datos para dar a cada producto un slug único. Pasó de ocho modelos a cuatro y de treinta dependencias de ejecución en el frontend a dos: React y React DOM.

El backend son tres aplicaciones de Django: shop (catálogo, API y administración), contact (mensajes y suscripciones) y accounts (el usuario del equipo, que entra con su correo). El frontend son seis componentes de React, un hook para el carrito y CSS propio, sin librerías de estado ni de componentes. En septiembre de 2026 la preparé para producción: endurecí Django, hice que el carrito se sincronizara con el catálogo y la desplegué con su dominio.

Decisiones de arquitectura

  • Sin cuentas de cliente. El usuario personalizado de Django existe sólo para el equipo; el comprador nunca se registra.
  • API pública de sólo lectura. El catálogo se consulta por la API y se escribe únicamente desde el administrador. La tienda pide todas las páginas del listado, así que un catálogo más grande no deja productos fuera.
  • Un carrito que no miente. Vive en localStorage y sobrevive a la recarga. Al llegar el catálogo, cada línea toma el precio actual, su cantidad se limita a las existencias y los productos retirados salen del carrito; si algo cambió, el cajón lo dice.
  • Un solo contenedor en producción. Django sirve la API, el administrador, las fotos y el build de React desde el mismo dominio, con WhiteNoise: no hace falta CORS. En desarrollo, Vite hace de proxy hacia Django.
  • La demostración nunca sustituye a los datos reales. El catálogo de respaldo sólo aparece en desarrollo o si se activa explícitamente, con un aviso visible. En producción, si la API falla, el cliente ve un error con reintento, no productos inventados.

El pedido por WhatsApp

«Finalizar por WhatsApp» abre un enlace wa.me con el pedido ya escrito. No hay integración con la API de WhatsApp ni nada que pagar por mensaje: el servidor no registra el pedido y la conversación sigue en el teléfono del negocio. Éste es el mensaje que genera el carrito de las capturas:

¡Hola, Delicaté!
Quiero realizar este pedido:

• 2 × Avena Calma — RD$700
• 1 × Corazón de Lavanda — RD$425
• 1 × Café Despierto — RD$450

Total estimado: RD$1,575

Mi nombre es:
Prefiero: entrega / recoger

¿Me confirman disponibilidad y forma de entrega? Gracias.

El saludo llevaba un emoji que la página web de WhatsApp convertía en «�» al redirigir el enlace; ahora va sin él. El formulario de contacto hace lo mismo con el nombre y la consulta: tampoco guarda nada en el servidor.

Producción y seguridad

  • Publicar sólo lo que funciona. Cada push a main construye la imagen, aplica las migraciones antes de publicar y sólo da tráfico a la versión nueva si /api/health/, que consulta la base de datos, responde 200. Si algo falla, sigue la anterior.
  • Cabeceras en todas las respuestas, también en las que sirve WhiteNoise: política de seguridad de contenidos, Permissions-Policy y X-Frame-Options; HTTPS obligatorio, HSTS de un año y cookies seguras.
  • Límites de frecuencia que cuentan la IP real detrás del proxy: mil peticiones por hora y visitante, y topes propios para los endpoints de escritura.
  • Arranque seguro. Con DEBUG apagado, Django se niega a arrancar con la clave de desarrollo o con una ruta de fotos relativa.
  • Fotos validadas por formato (JPG, PNG, WebP) y peso (5 MB), y servidas con caché de siete días.

Desafíos

El principal fue construir una experiencia de compra convincente sin fingir una infraestructura de pagos o logística. El segundo, mantener coherentes el catálogo administrado, los filtros, la ficha y un carrito que sobrevive entre visitas cuando cambian los precios o las existencias. Y el tercero fue reducir: pasar de una tienda sobredimensionada a un producto de unas 2.600 líneas que se puede mantener.

Lo que enseñó el despliegue

  • Railway no aplicó railway.json al servicio creado desde la línea de comandos, y la primera versión salió sin migraciones. La configuración quedó fijada en el propio servicio.
  • Git Bash convirtió la ruta /data/media en una ruta de Windows y las fotos se perdieron en un redespliegue. Lo corregí y añadí la validación que hoy impide arrancar con una ruta relativa.
  • Al añadir el dominio propio, Railway dejó de exponer la dirección *.up.railway.app; ahora los dos hosts están autorizados de forma explícita.

Diseño y UX

La dirección visual usa una paleta cálida de crema, verde bosque y terracota, una serif editorial para los titulares y mucho aire para comunicar artesanía, sin descargar fuentes web. La página cuenta una historia en orden: portada, valores, colección, historia de la marca, cómo pedir en tres pasos, preguntas frecuentes y contacto. Al agregar un producto, el carrito se abre para confirmar el gesto.

Los estados están resueltos: carga, error con reintento, categoría vacía y producto agotado. En accesibilidad, la ficha es un <dialog> nativo, el carrito atrapa y devuelve el foco, el menú móvil se cierra con Escape, hay enlace para saltar al contenido, los filtros declaran su estado con aria-pressed y se respeta prefers-reduced-motion.

Lo que no tiene

Para que nadie lo dé por supuesto: no hay pagos, ni búsqueda en la tienda (sólo la API acepta ?search=), ni variantes de producto, ni más de una foto por producto, ni una página propia con URL para cada jabón: la ficha es un diálogo. Tampoco hay panel de pedidos, porque los pedidos no pasan por el servidor. La API conserva dos endpoints de contacto y boletín, con límite de frecuencia, que ninguna pantalla de la tienda usa hoy.

Sobre las capturas

Las capturas se hicieron el 25 de septiembre de 2026 sobre el build de producción de la tienda, servido en local con una base de datos desechable y los diez productos del catálogo. Algunos estados se provocaron a propósito para poder enseñarlos: el producto agotado, el catálogo que tarda o falla, la categoría vacía y el carrito guardado con precios viejos. Lo que se ve en cada uno es el código de producción. Las fotografías de producto son las del proyecto original; las de ambiente de la portada y la historia se generaron para esta versión. Las pantallas con algún defecto visible se dejaron fuera.

Aprendizajes

Un buen producto no necesita fingir más infraestructura de la que el negocio puede operar. Quitar cuentas, carrito en servidor y checkout hizo el sistema más pequeño, más seguro y más fácil de adoptar, y convirtió una limitación real en un flujo coherente. La versión que más aporta no fue la que más hacía, sino la que mejor se ajustaba a cómo vende el negocio.

Estado actual

En línea en delicate.jonasjavier.dev desde el 25 de septiembre de 2026: un servicio Docker en Railway con PostgreSQL, un volumen para las fotos y dominio propio con HTTPS. El repositorio es público para evaluación, bajo licencia propietaria, con la integración continua en verde y 18 pruebas automatizadas que pasan. Antes de anunciarlo, quedan las validaciones que dependen del negocio y están en su lista de salida: políticas de entrega y privacidad, un pedido completo probado en Android e iPhone, copias de seguridad y monitoreo. No publico ventas ni visitas porque no las mido.

La siguiente línea del producto ya está especificada: un configurador guiado de jabón personalizado cuyo resultado es una solicitud por WhatsApp, no una compra automática. Todavía no forma parte del sistema.