1 · The 3D city
The storefront isn't a video or image: it's a WebGL scene with real geometry. Each building is a BoxGeometry with random height, lit by three lights — ambient, directional and an orange rim light for the warm edge.
Hover highlighting uses a Raycaster: a ray is cast from the camera through the pointer and the first intersected building turns orange. The pulsing rings on the ground are RingGeometry scaling while opacity fades over time.
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);
}
The scene only starts when it enters the viewport, via IntersectionObserver. If the visitor never scrolls there, not a single GPU cycle is spent.
Here the city is a demo, but it mirrors production: maps are an independent layer of the platform. Geographic data lives as GeoJSON in a Cloudflare R2 bucket, kept in sync automatically with every inventory write.
The upside of separating them: the public storefront is served from the edge as static files. Even if the platform is in maintenance, the map clients see stays up.
2 · Detecting market without asking permission
A visitor in Lima sees Lima zones; one in Madrid sees Madrid. We don't use geolocation (it requires a permission that breaks the experience) or geo-IP (external dependency, latency and third-party data). We use the browser's time zone.
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
// "America/Lima" → PE · "Europe/Madrid" → ES · "America/Caracas" → VE
It's instant, needs no permissions, doesn't hit the network and works offline. Eighteen mapped markets plus a language-based fallback when there's no match.
3 · Bilingual with a single HTML source
No hand-duplicated pages. Copy lives in two JSON files and the markup carries references:
<h1 data-i18n="hero.title">Tus asesores trabajan.</h1>
A ~100-line script with no dependencies reads the template and dictionaries and writes two translated versions: / in Spanish and /en/ in English, each with its canonical and hreflang tags.
This isn't pedantry. Googlebot crawls with English settings: if translation happened only in the browser, it would index the whole site in the wrong language. We simulated the bot — exactly that happened.
This technical note follows the same pattern: /making-of.html in Spanish and /en/making-of.html in English, generated at build time.
4 · Offline mode, for real
A service worker precaches HTML, styles, scripts, icons, dictionaries and the 3D library itself. Tested with the network off: the page loads fully and the city renders.
caches.open(CACHE).then(c =>
c.addAll(ASSETS.map(u => new Request(u, { cache: 'reload' })))
);
cache: 'reload' is key: it stops precache from storing a stale copy taken from the browser's HTTP cache.
The classic service worker problem is the opposite: serving an old version forever. Here we fix it by putting the content hash in the filename. If CSS changes, its URL changes, and both browser and CDN fetch the new one instantly.
theme.b3e100711f.css → theme.5cae4737cd.css
That lets us cache assets for a full year with immutable safely, while HTML always travels fresh. The service worker version comes from the same hash — nobody has to remember to bump a number by hand.
5 · The simulator as product
The commission calculator splits a fee between two sides and handles an external collaborator on either side. Full state fits in the URL, so a director can send the exact scenario to a partner:
?sim=100000-5-40-own-ext-USD-2
// valor-honorario-casa-captadora-vendedora-moneda-ops
PDF export uses no library: it's window.print() with a @media print sheet that hides everything except the receipt and adds brand header.
If you leave your email below the calculator, the Worker queues a transactional email via Resend in the visitor's language (ES/EN). We don't recalculate commissions on the server (Option 2): the email links back to ?sim=…#simulador to restore the exact scenario.
{ "type": "simulation", "email": "…", "scenario": "https://usazenith.com/?sim=…" }
Sending runs in waitUntil(): if Resend fails, the lead is already in D1 and the visitor saw success. Telegram notifies in parallel with the same URL.
6 · The backend fits in one function
Forms don't need their own server. A Cloudflare Function runs at the edge, saves to D1 (distributed SQL) and alerts via Telegram. Zero machines to maintain and zero fixed cost.
One endpoint discriminates by lead type:
contact · simulation · plan
Contact (#contacto section), simulator (email + scenario) and plan request (pricing modal, with plan, billing and quote from the monthly/annual toggle).
The interesting part is the defense order: each layer drops work before the next spends resources.
tamaño → JSON válido → honeypot → forma → cadencia → Turnstile
The body is rejected above 16 KB. The honeypot is an invisible field only bots fill. Rate limiting caps five submissions per ten minutes per origin. Turnstile comes last (explicit render, theme matching light/dark mode, size: compact in the modal).
IP is never stored in plain text: a salted hash is saved instead. Same for abuse limits, but someone with DB access wouldn't get a list of addresses.
7 · Plan request modal
Pricing CTAs (Vitrina, Control, Ecosistema, Founders) open a modal instead of scrolling to the long contact form. It shows the chosen plan, price for monthly or annual billing, first name, last name, email and Turnstile on one compact screen.
The header has a floating price chip; the body respects the site's light and dark theme (no forced white background on dark inputs). On mobile the captcha stacks below the email field.
{ "type": "plan", "plan": "control", "billing": "annual", "priceQuote": "USD 66/mes" }
8 · Accessibility, measured not assumed
It's easy to say a site is accessible. This one was audited: every control has a name, tab order follows a logical path with visible focus, there's a single h1 without hierarchy jumps and the FAQ uses native <details>, which works with keyboard even without JavaScript.
The uncomfortable finding was color: brand orange gives 2.09:1 on light backgrounds, well below the 4.5 AA requires. On dark backgrounds it gives 6.21.
#FF9500 sobre #FBF9F6 → 2,09 ✗
#FF9500 sobre #0A2E56 → 6,21 ✓
The fix wasn't dropping orange, but declaring two darkened variants for light surfaces and letting the cascade restore the original inside dark sections. The main button uses slate-blue text: still eye-catching and compliant.
9 · Details that cost little and show
- Custom cursor with smooth interpolation, growing over interactive elements. No
mix-blend-mode: we tried it and it vanished on dark backgrounds. - Magnetic buttons that shift a percentage of the distance to the pointer.
- Light and dark theme with
View Transitionswhere the browser supports them, instant switch where it doesn't. - Everything respects
prefers-reduced-motion: when the system asks for less animation, scenes render one frame and stop.
Want something like this for your product?
Tell us what you need