Nota técnica

Cómo está hecha esta página

Una ciudad 3D navegable, funcionamiento sin conexión, dos idiomas indexables, detección de mercado y un backend que vive en el borde. Sin React y sin bundler. Esto es lo que hay debajo.

0frameworks
0dependencias de compilación
2idiomas, un solo HTML fuente
AAcontraste verificado

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 Transitions donde 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