LMC Orlando (lmcorlando.com) sells luxury massage chairs, the kind with a four-figure price tag that people research for weeks before buying. That buyer shaped every technical decision on the storefront. This is not an impulse purchase, so the site had to feel considered and load fast on a phone in a showroom, without getting in the way of someone comparing two chairs side by side.
We built it on Shopify, on top of the Dawn theme. The decision that shaped the whole build was a boring one: no third-party apps unless there was a real reason to add one. On a performance-sensitive luxury store, every app you install is another script tag, another monthly fee, and one more thing that shifts the layout while it loads. So the default was to build in the theme itself and reach for an app only when doing it in-house would clearly be worse. That rule is easy to state and expensive to keep, and most of what follows is what keeping it actually cost.
Why Dawn, and why fork it
Dawn is Shopify's reference theme. It is fast, accessible, and server-rendered, and it gives you a working cart and variant system out of the box. The trap is that a luxury brand cannot look like a Dawn site with the colors swapped. So we treated Dawn as a foundation to build on rather than a template to configure, and ported a full custom design system onto it in chunks, seventeen of them, each shipped as its own reviewed pull request. Working in chunks kept the diffs readable and let us hold Theme Check and the Playwright suite green the whole way, instead of merging one giant rewrite nobody could review.
The variant selector breaks silently if you fight Dawn
The first hard lesson was Dawn's variant system. We wanted custom color swatches instead of the default dropdowns. You can style those any way you like, but Dawn's JavaScript only wires up variant selection when the markup follows a specific contract: the inputs live inside a fieldset.product-form__input, each option carries a data-option-value-id, and the selected state is tracked through a [data-selected-value] hook.
Miss any part of that and nothing throws. The swatch renders, it visually selects, and then it adds the wrong variant to the cart. That is the worst kind of bug on a store: silent, and it costs the merchant money. So the custom swatch markup follows Dawn's contract to the letter, and the Quick View resolves the selected variant client-side in its own script, because it renders outside the normal product form and cannot rely on Dawn to do it. The general lesson showed up again and again on this build: in Dawn, styling is free, but behavior is a contract. You can restyle anything, but the moment you change the markup you take over the JavaScript that was relying on it.
Swapping the main image on color change, without an app
Massage chairs ship in a few upholstery colors, and picking a color should swap the main product image. Sounds trivial. It was not, because of a layout choice we made on purpose. The product page splits the gallery and the details into two separate sections, because the details rail is block-driven: the merchant can reorder the title, price, swatches, trust badges, and spec accordions right in the theme editor. That flexibility is worth a lot on a store the client runs themselves. The cost of it is that Dawn's media gallery, now sitting in a different section, can no longer reach the variant picker through the DOM the way it expects to.
So the color-to-image swap is custom. A small script subscribes to Dawn's variant-change event, finds the matching gallery slide by its media id suffix, and scrolls to it. The Quick View is its own separate problem: it keeps its own slide list, unsubscribes from Dawn's product-info controller so the two do not fight over state, and renders every variant image up front so the swap is instant. None of this needed a third-party app, which was the whole point.
Comparing chairs when the API hides your data
High-consideration buyers compare specs, so the site has a comparison view: roller system, heat, warranty, and so on. The reflex here is to install a comparison app. Building it into the theme instead is where we hit the reason most people don't: the standard /products.js endpoint does not return metafields, and every spec value lives in a metafield. Build the comparison the obvious way and it comes back empty.
The workaround is a dedicated product.compare-data JSON view rendered by the theme itself, which exposes the real metafield values. Those live under custom.* keys, things like custom.roller_system and custom.heat, not a branded namespace. Brand is the one field that uses Shopify's native vendor field instead. That is the sort of detail you only learn by walking into the wall first.
The cart, and a newsletter without an app
The cart is a custom drawer plus a full cart page, and it meant patching Dawn's cart.js in a few targeted places: the list of sections to re-render on update, inline quantity validation, and an empty-cart reload path. Small, specific changes, each one because Dawn's default did not match what a custom cart needed to do.
Email capture is a newsletter popup that stores addresses without a marketing app installed, with an optional handoff to Klaviyo if the brand wants it later. Same rule as everywhere else on this build: theme first, an app only with a concrete reason.
Shipping it without breaking it
A theme that auto-deploys to a live store is a good way to take down a storefront on a Friday afternoon, so the repo has real guardrails. Every pull request runs Theme Check, Shopify's linter, and a Playwright suite against a dev server. Client-side JavaScript errors report to Sentry through a loader wired into a theme setting. A Playwright smoke test runs after each deploy and twice a day, and UptimeRobot watches for the site going down, because Shopify will not alert you on any of that itself.
The store went live on June 27, 2026, and "launch" was almost boring by design: main is connected to the live theme through Shopify's GitHub integration, so merging to main deploys. The interesting engineering went into making that safe.
What is left
The honest version: the frontend is done, and the remaining work is merchant-side. Catalog, collections, menus, page content, metafield values, plus whatever apps the brand chooses to add later, like a reviews tool. On a store like this, code was never going to be the long pole, and the rule we started with is part of why. Because we refused to let apps accrete, the surface that has to be maintained stayed small: theme code and product data, not a stack of third-party integrations drifting out of sync. Getting the merchandising right is the work now, and the code will not fight the merchant while they do it.