Motorhome.is
A motorhome rental company with a booking backend it did not control and a marketing site that could not talk to it. The site had to sell the trip and sell the van, and those two things lived in different systems.

Two systems, one
booking, no shared truth.
Every commercial fact — what vans exist, what they cost this week, what insurance applies, whether a seat is free — lives in Caren, an Icelandic rental platform the client already ran their fleet on. Everything that makes someone want the trip — photography, feature copy, campsite maps, blog posts — belongs in a CMS the rental team can edit without me. Neither system knows the other exists.
The first version tried to close that gap by copying: a nightly cron pulled classes, countries, extras and insurances out of Caren into a local database, cached in Redis, and the site read from the copy. It was fast and it was frequently wrong. A price changed at 09:00 and the site kept quoting yesterday's number until midnight.
Three decisions that
shaped everything else.
The rewrite started from one rule: Caren owns money and availability, the CMS owns everything else, and nothing gets copied between them.
Join on a number, not a sync
Each vehicle in Prismic carries a single numeric `classid` matched against Caren's class ID at request time. Photography, the new-model badge and six categories of feature icons come from the CMS; specs, price and availability come from the API; they meet in one function and never in a database. There is no local copy to go stale.
Put the rental API behind the app
Sixteen route handlers stand between the browser and Caren, so the API key, username and rental ID stay server-side. It also means Caren's wire format — reservations expressed as nested string arrays, countries as proprietary numeric IDs — is translated once at the boundary instead of leaking into components.
Stream the shell, suspend the price
The page starts the slow Caren fetches and deliberately does not await them. The promises are handed to the slices that need them and consumed inside Suspense, so navigation, hero and footer paint immediately while only the vehicle list waits. The slowest data in the product stopped being the thing that gated first paint.
Six steps, one state machine
Search, availability, insurance and extras, driver details, payment, confirmation — all of it driven by one context that mirrors itself into sessionStorage as it goes. Refreshing the page mid-booking does not lose the booking. Once a reservation exists in Caren, the details step stops creating and starts editing, and a deep comparison against a snapshot means it only calls the API when something actually changed.

Thirteen slices the client can rearrange
Pages are composed in Prismic from thirteen slices with eighteen variations, including one that mounts the entire booking widget — so the booking form is something an editor can place on a page rather than something I have to deploy. Navigation, footer, SEO metadata and a date-windowed site-wide banner are all CMS-owned. Vans get fifty-four hand-built feature icons across six categories.

823 points of interest, served as static files
Campsites, fuel and groceries on a MapLibre map of Iceland. Rather than query a live geo API on every page view, a one-shot script pulls the data out of OpenStreetMap's Overpass endpoint and writes static GeoJSON into the repo — 303 campsites, 263 gas stations, 257 grocery stores, 227 KB in total. The map has no runtime dependency on anything but the tile server.

The coupon that
refuses to say its price.
Caren reports discounts as two totals and never attributes an amount to an individual offer. So when a guest types a coupon code and the page has to show them what it saved, there is no field to read. Worse, Caren accepts a code that does not exist without raising an error — it just returns the same price back, which means a typo and a real discount are indistinguishable from the response alone.
The fix is to measure the coupon rather than ask for it. Applying a code re-prices the booking twice against the same endpoint with the same parameters: once clean to establish a baseline, once with the code. The difference is the coupon's value, and if the total did not move, the code did nothing and the guest is told so. The named breakdown is then reconstructed by diffing the offers active before and after, and relabelled with the code the guest actually typed rather than Caren's internal name for it.
The trap is that the baseline goes stale the moment anything else changes. A pricing fingerprint — reservation, vehicle, both dates, both times, insurance, and a sorted list of extras with quantities — invalidates the snapshot whenever the booking shifts underneath it. Struck-through prices sit next to live ones on that page, so a mismatch is not a rendering glitch, it is the site lying about money.