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 de forma automática con cada escritura sobre el inventario. Publicar, editar o marcar vendida una propiedad actualiza la vitrina sin que nadie toque nada.
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.
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.
6 · El backend cabe en una función
El formulario no necesita servidor propio. Es una Cloudflare Function que corre en el borde y guarda en D1, la base SQL distribuida de Cloudflare. Cero máquinas que mantener y cero coste fijo.
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. Y el captcha se deja para el final, porque es lo único que implica una llamada de red.
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 · 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.
8 · 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