Before the course catalogue existed, FairwayPlan had a wizard. You told it your dates, your regions, your budget, and it handed you an itinerary. That was fine for people who already knew what they wanted. But it did nothing for the person who just landed on the site wondering what courses are even in Canterbury? There was no way to browse. No way to compare. No way to discover a course you didn't already know the name of.
So I built a catalogue. And for the design, I did what any sensible person does: I opened Airbnb, opened Booking.com, and studied what two decades of travel-booking UX research had already figured out.
The split-pane layout
Open Airbnb on a desktop and search for accommodation in Queenstown. You get a map on the left, a scrollable list of cards on the right. The two panes are linked: click a pin on the map, the card scrolls into view. Click a card, the map centres on that pin. You never leave the page. You never lose your place.
This is the single most important UX pattern in location-based browsing. It works because the two representations answer different questions. The map answers where is this? The list answers what is this? By keeping both visible simultaneously, you eliminate the constant back-and-forth that plagued older booking interfaces. No more clicking into a listing, losing your map position, hitting back, and re-orienting.
FairwayPlan's catalogue copies this pattern directly. Leaflet map on the left, scrollable course cards on the right. Click a marker and the card slides into view. Click a card and the map pans to highlight that course. Select a region from the dropdown and the map flies to those bounds while the list filters to match. On mobile, where side-by-side is impractical, a toggle switches between map and list views, matching how Airbnb handles the same constraint.
Faceted filtering: the Booking.com school
Booking.com is the master class in faceted search. Star rating. Price range. Property type. Free cancellation. Guest review score. Breakfast included. Each filter is independent (you can stack them or use them alone) and the result count updates instantly. The key insight is that filters should feel like refinement, not restriction. You're narrowing your options, not wrestling with a query language.
The course catalogue borrows this structure with filters mapped to the golf domain. Instead of star rating and property type, you get:
- Region: a dropdown covering all 16 NZ regions, from Northland to Southland
- Course type: toggleable chips for Links, Parkland, Mountain, and Inland, each colour-coded to its terrain
- Must-play: a gold star toggle showing hand-picked standout courses, two per region
- Search: free-text filtering by course name or region
All filters compose. You can search for “bay” while filtering to links courses with the starred toggle active, and the result count updates live: Showing 3 of 370. A clear-all button appears when any filter is active, again straight from Booking.com's playbook.
Price tiers instead of exact prices
Airbnb shows exact nightly rates. Booking.com shows exact nightly rates. But when I looked at how golfers actually talk about green fees, nobody says “that course is $127.50.” They say “that's a cheap round” or “that's a big-money course.” The mental model is categorical, not numeric.
So course cards show a price tier badge: $ for courses under $80, $$ for $80–$150, $$$ for anything above that. The exact fee is still there in the expanded details, but the badge lets you scan twenty cards and immediately sort them into “budget round” and “splurge round” without reading numbers. This is a departure from the travel-booking model, and it works better for this domain.
The card: less is more until you ask for more
Airbnb cards show a photo, a title, a price, a rating, and a short description. Five pieces of information, maximum. You can take in a whole page of results without reading anything closely. Details come later, when you click into the listing. Booking.com does the same: star rating, property name, location, price, review score. The initial scan is ruthlessly compressed.
Course cards follow this principle. In the collapsed state you see the course name, region, type badge, price tier, and one line of metadata: holes, par, hilliness. That's it. No address, no phone number, no cart availability, no booking links. Those are all there but tucked behind a “More details” toggle that expands the card in-place. The user controls when they cross from browsing into evaluating.
This progressive disclosure pattern is critical. With 409 courses, the page would be unreadable if every card showed every field. The collapsed card is for scanning. The expanded card is for deciding.
With 409 courses, progressive disclosure is not optional. The user controls when they cross from browsing into evaluating.
Where golf needed something different
Not everything transplanted cleanly from the travel-booking world. Some things had to be invented.
Terrain-coded type badges
Hotels have star ratings. Courses have terrain types. But unlike a five-star scale, terrain types are not ranked. Links is not better than parkland; it is different. Showing them as identical grey badges would miss the point. So each type gets a colour drawn from its real-world environment: navy blue for links courses that sit beside the ocean, forest green for parkland, sage for mountain courses, warm sand for inland. The colours carry semantic weight even before you read the label.
The starred course system
Booking.com has “Genius properties” and “Top picks.” Airbnb has “Guest favourites.” These are algorithmic. They're driven by review volume, conversion rates, supplier participation. Golf in New Zealand does not have that data infrastructure. There is no review corpus large enough to rank 370 courses algorithmically.
Instead, the catalogue uses an editorial approach: two hand-picked must-play courses per region, curated for standout character, setting, or challenge. Cape Kidnappers in Hawke's Bay. Paraparaumu Beach in Wellington. Jack's Point in Queenstown Lakes. These are not the most expensive or the highest rated. They are the courses a local golfer would tell you not to miss. The gold star badge and the “must-play” filter let you find them instantly.
Map bounds as a filter
This one came directly from using Airbnb: when you pan and zoom the map, the list updates to show only the listings visible in the current viewport. It is the most natural spatial filter possible. Instead of picking a region from a dropdown, you just look at the part of the country you care about. On the catalogue, the same behaviour applies. Pan to the Coromandel Peninsula, and the list shows only courses within those bounds. Zoom out and everything comes back. The region dropdown still works too, and flying to a region resets the map bounds, but the direct-manipulation approach feels right for exploration.
Making it work on a phone
The split-pane layout is a desktop pattern. On a phone, two side-by-side panes do not fit. Airbnb solves this with a toggle: you are either looking at the map or looking at the list, never both. The catalogue does the same. Below the md breakpoint, a map/list toggle appears and the panes stack into a single view. Tap “Map” and the list disappears. Tap “List” and the map goes away. All the linking still works: select a region in list view, switch to map, and the map has already flown to those bounds.
The filter bar needed its own mobile treatment. On desktop, the region dropdown, type chips, starred toggle, and search field all sit in a single row. On a phone, the region selector collapses into a stacked control and the type chips become a horizontally scrollable strip with momentum scrolling enabled and the scrollbar hidden. It sounds small, but a filter bar that wraps awkwardly on mobile kills the browsing experience. People give up before they start filtering.
Performance with 370 markers
Leaflet is not a small library. Loading it on every page visit, including mobile users on cellular connections who might never tap the map tab, would be wasteful. So the map component is dynamically imported with ssr: false and, on mobile, only mounted when the user actually taps “Map.” Until then, the Leaflet bundle stays out of the critical path entirely.
Even after the map loads, rendering 370 markers on every pan and zoom would cause visible jank. The bounds refresh is debounced: 400ms after a user pan, 600ms after a programmatic fly. A ref flag distinguishes between the two so that code-driven moves (region selection, card clicks) do not accidentally trigger the “user moved the map” logic that would clear the region filter. Marker icons are rebuilt only when the filtered course list changes. When a single marker becomes active, only that marker's icon is swapped, not all 409.
The course list itself renders all cards into the DOM without virtualisation. With 370 items and no images, the DOM weight is manageable. Each card uses CSS line clamping to keep descriptions to two lines until expanded, and the description text is memoised to avoid re-parsing HTML on every render. The whole filtered course list is wrapped in useMemo so filtering and sorting only recalculate when a dependency actually changes.
Sort by geography, not popularity
Booking.com defaults to sorting by “Our top picks,” which is really a revenue-optimised ranking. Airbnb defaults to a relevance model trained on booking data. Neither of these was available or appropriate here.
The default sort is north to south by latitude. This mirrors how most visitors think about a New Zealand golf trip: you either start in Auckland and drive south, or start in Queenstown and drive north. The list follows the road. Alternative sorts are price low-to-high and alphabetical, but geography-first felt like the most honest default for a country shaped like a long thin island.
Auditing against Golf NZ: 39 missing courses
A week after launch, I ran the catalogue against the Golf NZ club directory. The result was humbling. Despite starting with what I thought was a comprehensive list, 39 courses were missing entirely, and six existing entries had wrong names.
The gaps followed a pattern. Christchurch alone was missing seven courses: Avondale, Bottle Lake, Coringa, Harewood, Rawhiti, Waimairi Beach, and Waitikiri. The South Canterbury cluster around Timaru was missing Gleniti, Highfield, and Tinwald. Dunedin was missing Belleknowes and St Clair, two well-known local clubs. Auckland was missing Maungakiekie, Pakuranga, Waitakere, Wattle Downs, and Northridge. None of these are obscure. They are clubs with hundreds of members, active competitive programmes, and proper websites.
The name corrections were just as instructive. “Whitianga Golf Club” is actually Mercury Bay Golf & Country Club. “Ohakune Golf Club” is Waimarino Golf Club, and it has 18 holes, not 9. “Clyde Golf Club” is properly called Dunstan Golf Club. “Oamaru Golf Club” goes by North Otago Golf Club. “Windcross Farms” is Windross Farm. And “Amberly” was simply misspelled; it should be Amberley.
You can build the best browse experience in the world, but if the data underneath it has holes, none of that UX work matters.
The fix was systematic. I pulled every club from the Golf NZ API, matched them against our database using coordinates and fuzzy name matching, and classified every mismatch as either genuinely missing, a name discrepancy, or a regional body that is not a playable course. The 39 new entries each got descriptions, enriched descriptions, coordinates, hole counts, and default seasonal pricing. The six renames propagated through every file that referenced the old names: init.sql, enriched_descriptions.sql, the dotgolf ID seed file.
The whole operation shipped as a single idempotent migration. On production, it was one command: make migrate-prod SQL=backend/db/migrations/add_missing_courses.sql, followed by a Redis flush so the course lists refreshed. The catalogue went from 370 to 409 courses overnight.
The lesson is one I keep relearning. Data completeness is a feature. Every missing course is a golfer whose home club does not appear when they search. Every wrong name is a moment of doubt: is this the right place? The UX can be polished to a shine, but if the data has gaps, trust erodes quietly. Now there is a repeatable process for running the audit again whenever Golf NZ updates their directory.
What I learned from copying well
There is a difference between copying a UI pattern and understanding why it works. The split-pane layout works because location context and listing detail are complementary, not competitive. Faceted filters work because each filter dimension is independent and the feedback is instant. Progressive disclosure works because browsing and evaluating are separate cognitive modes. Price tiers work in golf because golfers think categorically about green fees.
Two decades of A/B testing at Airbnb and Booking.com represent an enormous amount of accumulated knowledge about how people search for things in physical space. Most of it transfers directly to golf courses.
The patterns that Airbnb and Booking.com have refined over years of A/B testing represent an enormous amount of accumulated knowledge about how people search for things in physical space. Most of that knowledge transfers directly to golf courses. The places where it didn't transfer (the type badges, the editorial curation, the geography-first sort) are exactly the places where the domain has its own logic. Respecting both gave the catalogue a structure that feels familiar to anyone who has booked a hotel online, but tuned for someone planning a round.
The course catalogue is live at fairwayplan.com/courses. 409 courses, 16 regions, no login required. Browse, filter, find something you did not know existed, and then let the wizard do the scheduling.