1 · La ciudad 3D
La vitrina no es un vídeo ni una imagen: es una escena WebGL con geometría real. Cada edificio es un BoxGeometry con altura aleatoria, iluminado por tres luces —ambiente, direccional y un contraluz naranja que le da el borde cálido—.
El resaltado al pasar el cursor usa un Raycaster: se lanza un rayo desde la cámara hacia el puntero y se pinta de naranja el primer edificio que intersecta. Los anillos que laten en el suelo son RingGeometry escalando con la opacidad cayendo en función del tiempo.
const hit = ray.intersectObjects(buildings, false)[0];
if (hit?.object !== hovered) {
hovered?.material.color.copy(baseColor);
hovered = hit?.object ?? null;
hovered?.material.color.copy(hotColor);
}
La escena solo arranca cuando entra en el viewport, vía IntersectionObserver. Si el visitante nunca baja hasta ahí, no se gasta ni un ciclo de GPU.
Aquí la ciudad es una demostración, pero refleja cómo funciona de verdad: en producción los mapas son una capa independiente de la plataforma. Los datos geográficos viven como GeoJSON en un bucket de Cloudflare R2, y la plataforma los mantiene sincronizados con cada escritura sobre el inventario.
La ventaja de separarlo: la vitrina pública se sirve desde el borde como archivos estáticos. Aunque la plataforma esté en mantenimiento, el mapa que ven los clientes sigue en pie.
2 · Detectar el mercado sin pedir permiso
Un visitante en Lima ve zonas de Lima; uno en Madrid ve Madrid. No usamos geolocalización (obliga a un permiso que rompe la experiencia) ni geo-IP (dependencia externa, latencia y datos a terceros). Usamos la zona horaria del navegador.
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
// "America/Lima" → PE · "Europe/Madrid" → ES · "America/Caracas" → VE
Es instantáneo, no pide permisos, no viaja por la red y funciona sin conexión. Con 18 mercados mapeados y una regla de respaldo por idioma cuando no hay coincidencia.
3 · Bilingüe con un solo HTML fuente
Nada de duplicar la página a mano. Los textos viven en dos JSON y el marcado lleva referencias:
<h1 data-i18n="hero.title">Tus asesores trabajan.</h1>
Un script propio de unas cien líneas, sin dependencias, lee esa plantilla y los diccionarios y escribe dos versiones ya traducidas: / en español y /en/ en inglés, cada una con su canonical y sus etiquetas hreflang.
Esto no es capricho. Googlebot rastrea con configuración en inglés: si la traducción ocurriera solo en el navegador, indexaría el sitio entero en el idioma equivocado. Se comprobó simulando el robot, y pasaba exactamente eso.
Esta nota técnica sigue el mismo patrón: /making-of.html en español y /en/making-of.html en inglés, generadas en el build.
4 · Funciona sin conexión, de verdad
Un service worker precachea el HTML, los estilos, los scripts, los íconos, los diccionarios y la propia librería 3D. Probado con la red desconectada: la página carga completa y la ciudad renderiza.
caches.open(CACHE).then(c =>
c.addAll(ASSETS.map(u => new Request(u, { cache: 'reload' })))
);
El cache: 'reload' es clave: evita que el precache guarde una copia rancia tomada de la caché HTTP del navegador.
El problema clásico de los service workers es el contrario: que sirvan para siempre una versión vieja. Aquí se resuelve poniendo el hash del contenido en el nombre del archivo. Si el CSS cambia, cambia su URL, y tanto el navegador como el CDN descargan la nueva al instante.
theme.b3e100711f.css → theme.5cae4737cd.css
Eso permite cachear los assets un año entero con immutable sin miedo, mientras el HTML viaja siempre fresco. Y la versión del service worker sale del mismo hash, así que nadie tiene que acordarse de subir un número a mano.
5 · El simulador como producto
La calculadora de comisiones reparte un honorario entre dos puntas y contempla que una de ellas sea de un colaborador externo. El estado completo cabe en la URL, así que un director puede mandarle el escenario exacto a su socio:
?sim=100000-5-40-own-ext-USD-2
// valor-honorario-casa-captadora-vendedora-moneda-ops
La exportación a PDF no usa librería: es window.print() con una hoja @media print que oculta todo menos el recibo y le añade cabecera de marca.
Si dejas tu correo bajo la calculadora, el Worker encola un email transaccional vía Resend en el idioma del visitante (ES/EN). No recalculamos comisiones en el servidor (Opción 2): el correo trae un enlace de vuelta a ?sim=…#simulador para restaurar el escenario exacto.
{ "type": "simulation", "email": "…", "scenario": "https://usazenith.com/?sim=…" }
El envío va en waitUntil(): si Resend falla, el lead ya está en D1 y el visitante vio éxito. Telegram avisa en paralelo con la misma URL.
6 · El backend cabe en una función
Los formularios no necesitan servidor propio. Es una Cloudflare Function que corre en el borde, guarda en D1 (SQL distribuida) y avisa por Telegram. Cero máquinas que mantener y cero coste fijo.
Un solo endpoint discrimina por tipo de lead:
contact · simulation · plan
Contacto (sección #contacto), simulador (email + escenario) y solicitud de plan (modal en precios, con plan, billing y cotización según el toggle mensual/anual).
Lo interesante es el orden de las defensas: cada capa descarta trabajo antes de que la siguiente gaste recursos.
tamaño → JSON válido → honeypot → forma → cadencia → Turnstile
El cuerpo se rechaza si supera 16 KB. El honeypot es un campo invisible que solo rellenan los robots. La cadencia limita a cinco envíos cada diez minutos por origen. Turnstile va al final (render explícito, tema acorde al modo claro/oscuro, size: compact en el modal).
La IP nunca se guarda en claro: se almacena un hash con sal. Sirve igual para limitar abusos, pero si alguien accediera a la base no obtendría una lista de direcciones.
7 · Modal de solicitud de plan
Los CTAs de precios (Vitrina, Control, Ecosistema, Founders) abren un modal en lugar de hacer scroll al formulario largo. Muestra el plan elegido, el precio según facturación mensual o anual, nombre, apellido, email y Turnstile en una sola pantalla compacta.
El header lleva un chip de precio flotante; el cuerpo respeta el tema claro y oscuro del sitio (sin forzar fondo blanco sobre inputs oscuros). En móvil el captcha se apila bajo el correo.
{ "type": "plan", "plan": "control", "billing": "annual", "priceQuote": "USD 66/mes" }
8 · Accesibilidad, medida y no supuesta
Es fácil decir que un sitio es accesible. Este se auditó: todos los controles tienen nombre, la tabulación recorre la página en orden lógico con foco siempre visible, hay un solo h1 sin saltos de jerarquía y la FAQ usa <details> nativo, que funciona con teclado incluso sin JavaScript.
El hallazgo incómodo fue el color: el naranja de marca da 2,09:1 sobre fondos claros, muy por debajo del 4,5 que exige el nivel AA. Sobre los fondos oscuros, en cambio, da 6,21.
#FF9500 sobre #FBF9F6 → 2,09 ✗
#FF9500 sobre #0A2E56 → 6,21 ✓
La solución no fue renunciar al naranja, sino declarar dos variantes oscurecidas para fondos claros y dejar que la cascada las restituya al original dentro de las secciones oscuras. El botón principal pasó a llevar texto en azul pizarra: sigue siendo igual de vistoso y cumple.
9 · Detalles que cuestan poco y se notan
- Cursor propio con interpolación suave, que crece sobre elementos interactivos. Sin
mix-blend-mode: se probó y desaparecía sobre los fondos oscuros. - Botones magnéticos que se desplazan un porcentaje de la distancia al puntero.
- Tema claro y oscuro con
View Transitionsdonde el navegador lo soporta, y cruce instantáneo donde no. - Todo respeta
prefers-reduced-motion: si el sistema pide menos animación, las escenas renderizan un fotograma y se detienen.
¿Quieres algo así para tu producto?
Cuéntanos qué necesitas