feat: catálogo como centro del embudo y captura de baja fricción
Reestructuración pedida por el cliente tras revisar la primera versión. Conversión: - Formulario de 14 campos a 4: nombre, correo, teléfono y mensaje. La clasificación de riesgo ya no se pregunta, se deduce del contexto de la página y viaja oculta. Menos fricción, misma riqueza para el análisis. - El inicio deja de bifurcar y empuja al catálogo: 36 llamados desembocan ahí, y las rutas por industria y escenario pasan a ser la forma de entrar al catálogo ya filtrado, en lugar de competir como destino. - Catálogo rehecho como tienda: barra lateral con filtros, búsqueda que acepta expresiones regulares, títulos que abren la ficha, un solo botón primario por tarjeta y el comparador detrás de un desplegable. - Ficha de producto orientada al cierre: bloque en lenguaje llano sobre si el equipo corresponde al caso, y "Cotizar este equipo" con el formulario precargado. Se retiraron los enlaces que sacaban del embudo a leer normas. - Todos los llamados se unifican en "Recibir cotización". - Widget de WhatsApp: nombre, teléfono, correo y mensaje libre. El texto que escribe la persona es el que viaja a la conversación. Doble audiencia: - 13 escenarios con la iconografía original de FSPM traducen un lugar reconocible a clases de fuego y productos, para quien no domina la terminología. La capa técnica se conserva para búsqueda. - Héroe con fotografía en todas las landings. Correcciones: - El contenedor moría al arrancar porque el CLI de Prisma no podía escribir sus motores; el chown ahora incluye /app/prisma-cli. - Iconos repetidos entre escenarios. El set original no tiene icono de cocina, así que ese escenario usa un marcador tipográfico explícito. - FIREMIKS no aparecía en ningún escenario. - Los ids de campo del formulario colisionaban con dos instancias por página. - Se documentó el despliegue, que faltaba por completo. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
3c6bdcfc06
commit
0e0331c1fc
@@ -0,0 +1,149 @@
|
||||
# Despliegue
|
||||
|
||||
Sitio de FSPM con micro CRM. Un solo contenedor: Next.js en modo `standalone` y
|
||||
SQLite en un volumen. No hay base de datos externa.
|
||||
|
||||
## Variables de entorno
|
||||
|
||||
| Variable | Requerida | Qué pasa si falta |
|
||||
|---|---|---|
|
||||
| `DATABASE_URL` | Sí | Prisma no arranca. En el contenedor ya viene fijada a `file:/app/data/fspm.db`; **no la cambies** salvo que muevas el volumen. |
|
||||
| `AUTH_SECRET` | Sí | El arranque falla con un error explícito. Firma las cookies de sesión del panel. |
|
||||
| `NEXT_PUBLIC_SITE_URL` | Sí, **en construcción** | Las URL canónicas, el sitemap y Open Graph apuntan al dominio equivocado. Ver la advertencia de abajo. |
|
||||
| `NEXT_PUBLIC_WHATSAPP` | Sí, **en construcción** | El widget de WhatsApp abre una conversación con un número vacío. |
|
||||
| `DEMO_ACCESO_RAPIDO` | No | Con cualquier valor distinto de `false`, el acceso de un clic por perfil está activo en `/admin/login`. |
|
||||
| `NODE_ENV` | No | Ya viene en `production` dentro de la imagen. |
|
||||
|
||||
### Advertencia sobre las variables `NEXT_PUBLIC_*`
|
||||
|
||||
Next las sustituye **literalmente durante `next build`**, no las lee al arrancar.
|
||||
Cambiar `NEXT_PUBLIC_SITE_URL` o `NEXT_PUBLIC_WHATSAPP` en el entorno del
|
||||
contenedor **no tiene ningún efecto**: hay que reconstruir la imagen pasándolas
|
||||
como `--build-arg`. Es el error más fácil de cometer aquí, y se manifiesta como
|
||||
un sitemap con el dominio incorrecto y un widget de WhatsApp que no marca a nadie.
|
||||
|
||||
```bash
|
||||
docker build \
|
||||
--build-arg NEXT_PUBLIC_SITE_URL=https://fspm-demo.urieljareth.org \
|
||||
--build-arg NEXT_PUBLIC_WHATSAPP=525540348395 \
|
||||
-t fspm-web:demo .
|
||||
```
|
||||
|
||||
### Generar un `AUTH_SECRET`
|
||||
|
||||
```bash
|
||||
node -e "console.log(require('crypto').randomBytes(48).toString('base64url'))"
|
||||
```
|
||||
|
||||
Uno por entorno. No reutilices el de desarrollo: cualquiera que lo tenga puede
|
||||
firmar una cookie de sesión válida y entrar al panel como administrador.
|
||||
|
||||
## El volumen — la parte frágil
|
||||
|
||||
**Si no montas `/app/data`, cada redespliegue borra los leads capturados.**
|
||||
|
||||
La base de demostración se hornea dentro de la imagen durante la construcción.
|
||||
Al arrancar, el punto de entrada la copia al volumen **solo si el volumen está
|
||||
vacío**; si ya hay una base, no la toca. Ese es el mecanismo que permite que los
|
||||
leads que llegan por el formulario y por el widget sobrevivan a una nueva versión
|
||||
de la imagen.
|
||||
|
||||
Sin volumen, el contenedor sigue funcionando —arranca con los datos sembrados—,
|
||||
así que la falla es silenciosa: nadie se entera hasta que un lead real desaparece.
|
||||
|
||||
En Coolify: **Storages → Add → Volume Mount**, con destino `/app/data`.
|
||||
Verifícalo después del primer despliegue con:
|
||||
|
||||
```bash
|
||||
docker inspect <contenedor> --format '{{json .Mounts}}'
|
||||
```
|
||||
|
||||
## Local con Docker
|
||||
|
||||
```bash
|
||||
docker build -t fspm-web:demo .
|
||||
|
||||
docker run -d --name fspm-demo -p 3100:3000 \
|
||||
-v fspm-datos:/app/data \
|
||||
-e AUTH_SECRET="<secreto generado>" \
|
||||
fspm-web:demo
|
||||
|
||||
curl -s http://127.0.0.1:3100/api/salud
|
||||
```
|
||||
|
||||
`/api/salud` consulta la base, así que un volumen no escribible se reporta como
|
||||
contenedor enfermo en lugar de pasar por sano.
|
||||
|
||||
## Coolify
|
||||
|
||||
Los endpoints `POST /applications/*` de la API devuelven 404 en esta instancia
|
||||
(verificado en 4.1.2 y de nuevo en 4.3.12), así que la aplicación **se crea desde
|
||||
la interfaz web** y solo el despliegue se dispara por API.
|
||||
|
||||
1. Crea la aplicación apuntando al repositorio de Gitea, rama `main`,
|
||||
tipo **Dockerfile**.
|
||||
2. Fija el dominio, para que Coolify no asigne uno aleatorio y devuelva 503.
|
||||
3. Carga las variables de entorno. Las `NEXT_PUBLIC_*` van también como
|
||||
argumentos de construcción.
|
||||
4. Monta el volumen en `/app/data`.
|
||||
5. Despliega y vigila el registro.
|
||||
|
||||
Despliegues posteriores:
|
||||
|
||||
```bash
|
||||
curl -H "Authorization: Bearer $COOLIFY_TOKEN" \
|
||||
"$COOLIFY_API_URL/deploy?uuid=<uuid de la aplicación>"
|
||||
```
|
||||
|
||||
## Verificación posterior al despliegue
|
||||
|
||||
Hazla en este orden; cada punto descarta una clase distinta de falla.
|
||||
|
||||
```bash
|
||||
BASE=https://fspm-demo.urieljareth.org
|
||||
|
||||
curl -s $BASE/api/salud # {"ok":true,...}
|
||||
curl -s $BASE/robots.txt | grep admin # debe prohibir /admin
|
||||
curl -s $BASE/sitemap.xml | grep -c admin # debe dar 0
|
||||
curl -sI $BASE/icon | grep -i content-type # image/*
|
||||
curl -sI $BASE/opengraph-image | grep -i content-type
|
||||
curl -s $BASE/sitemap.xml | grep -o 'https://[^<]*' | head -3 # ¿el dominio correcto?
|
||||
```
|
||||
|
||||
Después, a mano:
|
||||
|
||||
1. Envía el formulario del inicio y confirma que devuelve folio.
|
||||
2. Abre el widget de WhatsApp, envíalo y revisa que el enlace lleve el prefijo
|
||||
`[DEMO]`, el folio y el mensaje que escribiste.
|
||||
3. Entra a `/admin/login` —**no está enlazado desde el sitio**, es intencional— y
|
||||
usa el acceso rápido para ver el panel desde los dos roles.
|
||||
4. Confirma que la oportunidad de los pasos 1 y 2 aparece en el panel con su
|
||||
campaña y su producto de interés.
|
||||
5. Redespliega y vuelve a entrar al panel: los leads de los pasos 1 y 2 **deben
|
||||
seguir ahí**. Si desaparecieron, el volumen no está montado.
|
||||
|
||||
## Antes de enseñarlo a un cliente
|
||||
|
||||
- `AUTH_SECRET` distinto del de desarrollo.
|
||||
- `DEMO_ACCESO_RAPIDO=false` si la demo queda publicada sin acompañamiento: con
|
||||
el acceso rápido activo, cualquiera que llegue a `/admin/login` entra como
|
||||
administrador.
|
||||
- Los datos del panel son ficticios y los precios de los pedidos son inventados.
|
||||
Conviene decirlo en voz alta antes de abrir la pantalla de pedidos.
|
||||
|
||||
## Notas del entorno de desarrollo
|
||||
|
||||
- **El repositorio vive en una carpeta sincronizada a la nube.** El cliente de
|
||||
sincronización mueve archivos de `.next` a media compilación y `npm run build`
|
||||
falla con `Cannot find module './NNNN.js'` aunque el código esté correcto.
|
||||
Está reproducido. La construcción dentro de Docker no se ve afectada, porque
|
||||
copia el contexto y compila en el contenedor. Para verificar en local, usa la
|
||||
imagen, no `next build`.
|
||||
- `next.config.ts` acepta `NEXT_DIST_DIR` para mover el directorio de salida,
|
||||
pero Next lo resuelve relativo a la raíz del proyecto y no acepta rutas
|
||||
absolutas, así que no resuelve el problema anterior por sí solo.
|
||||
- El `chown` del Dockerfile incluye `/app/prisma-cli` a propósito: al arrancar,
|
||||
`prisma db push` reescribe sus motores dentro de su propio `node_modules`, y
|
||||
sin permiso el contenedor muere con
|
||||
`Can't write to /app/prisma-cli/node_modules/@prisma/engines`. Se detectó con
|
||||
el contenedor ya construido, nunca durante el build.
|
||||
Reference in New Issue
Block a user