Every skier and snowboarder has booked a trip on a resort’s own trail map: a painted cartoon where nothing is to scale and everything is blue sky. We built the opposite. Alpine Hito is a free 3D guide to 1,014 real mountains, built from elevation data you can check, that lets you fly any named run in a browser before you ever click book.
Terrain from AWS Terrain Tiles. Runs and lifts from OpenStreetMap contributors, ODbL (Open Database License). Counted from the live database, not estimated.

The thing riders actually want
Ask anyone planning a winter trip what they are trying to find out, and it is never “how many acres does this place have”.
It is a shorter, more nervous list. Is it too steep for me? Is there anything for my partner who has skied twice? Where are the parks? If the snow is bad, is there anything in the trees? Is this run I keep seeing on TikTok actually rideable by a human, or is it a cliff with a name?
Resort websites answer almost none of that. They are built to sell lift tickets, so the trail map is drawn by an illustrator to flatter the mountain, the photography is all bluebird powder, and the stats are whichever numbers look biggest. Third party directories are worse: pages of identical tables and star ratings from nobody in particular.
So the design brief wrote itself. Show people the real shape of the mountain, tell them where every number came from, and charge them nothing.
What we built
A rider lands on a mountain page and gets the honest version: real terrain, every named run and lift we have data for, the difficulty mix, the parks, the season, and a “Data Sources” block naming exactly where each fact came from. Then they hit Explore in 3D and the mountain stands up.

The explorer is real elevation data rendered with Three.js, not artwork. Hover any run for its name and difficulty. Pick one and the camera drops in and flies it top to bottom, the way you would actually ride it. It runs on a phone, on a laptop, and in a VR headset through the browser, with no app to install anywhere.
One build, three devices
Desktop, phone and VR headset are the same platform here. Not three products that look alike, one product that adapts.
The usual version of this story is three codebases, three budgets and three release cycles, and the honest ending is that the headset one never ships. We took the opposite bet: build it once for the web and let the device decide how to present it.
On a desktop a rider gets the full explorer, orbits with the mouse, and hovers runs for names. On a phone the same page becomes a touch surface: controls collapse behind a disclosure, targets grow to 44 pixels, the renderer scales its resolution down to what the device can actually push, and the back button closes whatever is open instead of dumping you off the page. In a headset, the same URL enters WebXR and the rider is standing over the mountain, with the controls moved onto their wrist because anything pinned to the world gets left behind the moment they move.
The payoff compounds. A feature built once appears on all three. There is one data layer, one design system and one deploy, so the site cannot drift into a good desktop experience and a neglected phone one. Nothing needs installing anywhere, so the barrier between reading about a mountain and standing on it is a single tap. And because it is all just web pages, every one of them is shareable, linkable and indexable, which is the part an app store listing can never give you.
Designing for all three made the work better rather than harder. A headset forces you to size type in degrees of vision instead of pixels, and a phone forces you to defend every kilobyte. Both of those pushed back into the desktop build and improved it.
Nine problems, rated like runs
Every one of these started as something that looked fine and measured badly. They are marked the way a mountain marks its trails.
A trail map is a drawing, not a mountain
The core insight is almost embarrassingly simple: people cannot read a trail map, because a trail map is a painting. Steepness is invented, distance is compressed, and the back side of the hill is wherever the artist had room.

Rendering the real digital elevation model fixes that without a single word of explanation. You look at it and you know. The feature we called Fly the Run does the rest: a camera that follows the fall line down a named run, so a rider watches the pitch change under them before they commit a day of their holiday to it.
Rankings nobody can argue with
Every ski site has a “best resorts” list and every one of them is somebody’s opinion, or worse, somebody’s ad budget. That is the fastest way to lose a rider’s trust, and trust is the whole product here.
So the rule for our ranking pages is absolute: each list ranks on exactly one measured number, and the page says which number before it says anything else. Biggest is terrain relief. Steepest is the share of advanced and expert runs. Best for beginners is the count of green runs. No stars, no favourites, nobody paid to be on them.

Eleven ranking pages, each stating its measure in the first line.
Zero editorial placements. Zero paid positions.
Feet or metres, and never a mismatch
Half the audience thinks in feet and vertical drop, half thinks in metres. Getting this wrong is not a rounding error, it is a rider misjudging a mountain.
Every measurement on the site goes through one conversion service, and the rule enforced in code is that a label never changes without the value converting with it. The default follows the rider: United States gets imperial, the rest of the world gets metric, and one switch in the header changes all of it.
We were shipping the whole database to show one mountain
This is the one that hurt. Every resort page was loading the entire resort database just to render a single record, because that was the simple thing to do when there were eight resorts and it quietly stayed the design when there were a thousand.
The fix was a precomputed per-resort file. The interesting part is how we generated it: rather than reimplementing the matching logic server side, where it would drift out of sync the first time anyone touched the original, we ran the site’s own functions in a real browser and wrote out what they produced. The fast path cannot disagree with the slow path, because the slow path built it.
Per resort page, before: 1.58 MB of database to render one record.
After: a 5.2 KB file. Same content, same code, same answers.
The snow looked beautiful and cooked people’s laptops
This is my favourite mistake on the project, because it was entirely self inflicted and it looked like a win right up until it did not.
Falling snow drifts across every page on the site. It is the first thing that tells you what kind of place this is, and the first version was lovely. It was also a requestAnimationFrame loop that never idled, running on two canvases, on every page, forever. In light mode each flake is drawn with a shadow blur, which is one of the more expensive things you can ask a 2D canvas to do. Then a rider told us their machine was running hot with the site open, and they were right.
The fix was three separate things, and only one of them was “draw less”:
Cap the loop at 30fps. Snow does not need 60. The frame skip draws nothing at all rather than drawing a cheaper frame, which halves the work for no visible change.
Move the motion to seconds instead of frames. The original advanced each flake by a fixed amount per frame, which quietly meant the same snow fell 2.4 times faster on a 144Hz monitor than on a 60Hz one. There was never one snow speed. Making the fall time based fixed the inconsistency and is what let us halve the frame rate without the snow slowing down.
Give it an off switch, and know when not to start. Snow never runs under prefers-reduced-motion, skips small screens, caps at around sixty flakes, and there is a snowflake chip in the header that turns it off and remembers.
After: 0.5% CPU at idle with the snow falling.
Loop rate 60 to 30fps. Fall speed identical on every refresh rate.
And then the fix shipped its own bug, which is the honest part of the story. Capping the frame rate moved the per frame canvas clear below the frame skip, so when a rider switched snow off the loop bailed out with the last frame still painted and the snow froze in mid air instead of disappearing. Teardown now lives in the stop branch as well as the toggle. The lesson I keep: when you add an early return to a render loop, go and check what it skipped on the way out.
On a phone, the filters ate the product
The resort finder has a lot of filters, because riders want a lot of filters. On a desktop that is a useful sidebar. On a phone it stacked above the results and pushed the first actual resort 2,071 pixels down the page, which is about two and a half screens of controls before you see a single mountain.


Measuring that number was the whole decision. Filters now start collapsed on touch, behind a control that shows how many are active, so the phone opens on content and the controls are there when somebody wants them.
Riders have to see themselves
Accounts come with a rider avatar, and an avatar system is a pile of quiet decisions about who belongs. Ours are enforced by a validator rather than left to whoever edits next.
Eyes are always covered, goggles or shades, never bare, because that is both how riders look and a small nod to not putting an unprotected face on a mountain. Everyone gets a full kit: jacket, pants, gloves or mittens, boots, and equipment that matches their sport. Stance only exists for snowboarders, because skiers do not have one. Body build is thin, medium or husky, it is cosmetic only, and it is never wired to skill, speed or ability anywhere in the code or the copy.

There is also a yeti, in full snow gear, because the mountain deserves a local.
A headset, with nothing to install
The 3D explorer runs in VR through WebXR, which means a rider puts on a headset, opens a URL, and is standing over the mountain. No store, no download, no separate build.
Designing for a headset broke most of my screen instincts. Text has to be sized in degrees of vision rather than pixels. Anything world locked gets left behind the moment the viewer moves, so the controls live on the wrist where a rider’s hands already are. And the terrain mesh is far too heavy to ask geometric questions of every frame, so pointing at a run marches the heightfield instead of ray casting the mesh: the same answer, 435 times faster, which is the difference between a smooth session and a queasy one.
Never invent a mountain
The rule that shaped the most code: the site never fabricates real resort data. Every factual claim traces to official sources, open data or public GIS, carries a source URL and a last-updated stamp, and demo records are labelled as placeholders rather than quietly passed off as facts.
That constraint is inconvenient in exactly the way good constraints are. It is why there are no invented star ratings, no made up snowfall averages, and no community statistics until there is a community to count. When a resort publishes a trail count that differs from what is in the map data, we show both and say which is which, instead of picking the flattering one.
Getting people excited, not just ranked
The content strategy and the SEO strategy are the same strategy, which is the only version of SEO I am interested in.
A rider’s search is rarely “ski resort”. It is biggest ski resorts in Japan, or resorts on the Ikon Pass, or best ski resorts for beginners. Those are real questions with measurable answers, so each one gets a page that answers it properly and says how it decided.
Underneath sit 1,014 individual mountain pages and 87 destination hubs for the country and state searches, all of it semantic HTML with structured data, Open Graph cards and descriptive URLs. The whole thing is static, so it loads fast everywhere, including on a phone with one bar in a lift queue.

But ranking is only the invitation. The reason someone stays is the moment the mountain stands up and they fly a run they have only ever seen in a video. That is the thing worth sharing with the group chat, and no amount of keyword work substitutes for it.
Decisions I would defend
Three.js, so the 3D reaches everybody
The single biggest reach decision on the project. A native app would have looked better and would have been seen by almost nobody: a rider planning a trip on their lunch break is not going to install something to look at a mountain. Building the 3D with Three.js on WebGL means the same URL works on a phone, a laptop and a VR headset with nothing to install, and it is shareable, linkable and indexable, which an app store listing never is.
That reach only counts if it actually runs on the average machine, which is why the performance work above is not a nice to have. On a mid range laptop the explorer draws a full mountain in 3.8ms per frame against a 33ms budget, and it scales its own resolution to the device rather than assuming a desktop GPU.
No framework, no build step
Vanilla JavaScript, 28 modules, no React, no bundler. There is not even a package.json. For a site that is mostly static content plus one heavy 3D canvas, a framework would have added weight and a build pipeline to the exact thing I wanted to keep cheap and fast. Three.js is the one real dependency, and it earns it.
Free, and honest about why
No paywall, no account required to look around. Accounts unlock saving and rating, not access. Monetisation is affiliate links placed where a rider is already planning a trip, which is a fair trade only if the recommendation would have been the same without it. Given the ranking rule, it is.
Measure before you redesign
Nearly every fix above started with a number: 1.58 MB, 2,071 pixels, 435 times. Redesigning on a hunch is how you ship a prettier version of the same problem. Every layout claim on this project was measured in a real browser at real breakpoints before and after.
Where it stands
The site went live in August 2026 and the database, the 3D explorer, accounts, rankings and the VR mode are all shipping. It is a young site, so the honest status is that the terrain coverage and the content are ahead of the audience, which is the right way round for something that gets better as more people find it.
Next up is a live opening day tracker, powder alerts by email, and bringing the remaining resort pages into the index. Then the winter does the rest.
Go stand on a mountain: alpinehito.com