// Case Study · Shopify operations, end to end
Giraux Fine Jewelry
Shopify · Luxury Jewelry · Sole Technical Owner
Giraux Fine Jewelry has sold diamonds and custom work in San Francisco since 1977. I took over the storefront in April 2025 as its only technical owner. What I inherited was a stock theme on Shopify Basic serving a 25 MB mobile homepage with an 18-second LCP, 360° product pages weighing roughly 80 MB each, two dead analytics tags collecting nothing, and appointments that only happened by phone.
// 01 · A theme with no build step
The storefront runs an extended Showcase theme on Shopify Basic. There is no package.json, no bundler, no asset pipeline. CSS and JavaScript are served straight out of the assets directory and Liquid processes the .css.liquid files server side. That single constraint shapes everything else: every abstraction a bundler would normally hand you has to be paid for another way.
The answer was to make each custom section fully self-contained. Comment header, Liquid assigns, a style block namespaced under the section id, markup, an IIFE, then the schema. Custom properties inherit from one shared token file so sections stay visually consistent without a shared stylesheet they could all break. It is now 131 section files, 108 snippets, and 124,223 lines across sections, snippets, and layout.
The pattern exists because of a specific bug. One header section had its style block placed after the closing tag, so the header painted as unstyled HTML: roughly 150 ms of bulleted navigation list on every page of the site before the CSS landed.
The decision I would defend hardest is seven type-specific product sections instead of one configuration-driven section. A loose diamond page and an engagement ring page share almost no fields. One shows carat, colour, clarity, cut, and a GIA certificate; the other shows metal, setting style, and ring size. A single section with schema toggles for every jewelry type becomes a conditional tree nobody can safely edit. The cost is real and I pay it in the open: a behaviour change fans out to all seven files, and the commit log says things like "remove the eager cert pdf.js aspect-probe from all 7 product sections" for exactly that reason. I took a known mechanical cost over an unknown tangled one.

// 02 · Booking, built against the raw API
Appointments used to be phone-only. They now run through a four-step flow built directly against the SimplyBook JSON-RPC API, about 7,200 lines across two sections. SimplyBook ships its own hosted widget and I did not use it.
The reason is ordering. Putting date and time selection in step one, ahead of service selection, is the single choice the whole flow is built around, and no hosted widget lets you do that. The widget also cannot be measured with our own GA4 events. The cost of that decision is that I now own every quirk of their API, including provider biographies that render live from SimplyBook and cannot be edited in the theme, which surprises everyone who expects to change that text in Shopify.
The hard part was latency. Their month-availability endpoint takes about 10 seconds when called with a null unit over a month-wide range. That is a dead page for someone who has just decided to come in. The fix is several things working together: one call per provider issued in parallel rather than one call per day, matrices merged client-side into a single slot structure, step two and the timeslot list derived from that structure with no further network calls, authentication started at parse time instead of on first interaction, preconnect hints, and a short-lived session cache with a 15-minute TTL on the auth token. Ten seconds became roughly 1.3.
File attachments post to a Cloudflare Worker endpoint and land in R2, surfaced to staff as links in the provider notes. The public CDN deliberately 404s that prefix; the files are reachable only through the access-gated admin domain.
// 03 · Modelling a catalog that fights you
Stone attributes are metafields, with only the filterable subset mirrored as tags. The conventional Shopify answer is variant options, and it does not work here: a loose diamond has ten or more independent attributes and Shopify allows three variant options per product. Variants would have forced a lossy model onto the exact data customers shop by.
The trap that justifies the whole approach is a collision. Tagging a stone "Emerald" is ambiguous between the emerald cut and the gemstone emerald. A collection rule written against that tag returned 66 products where the true count was 24. Gemstone type now lives in a metafield and type tags are namespaced, with accented values normalised in Liquid so a tag typed by a human in the admin still matches.
A related one, found after shipping: Shopify’s carat filter parameter requires the string "1.0" and silently returns nothing for "1". No error, just an empty grid.
Price visibility is data rather than code. Products priced at zero render as "Inquire for Price", driven by metafields, which is what lets the owner change what is quotable without touching the theme.
// 04 · What went wrong
A case study with no failures is marketing. These are all backed by a commit, a note, or both.
The one that still stings is the certificate aspect-ratio probe. Every product page showing a GIA certificate loaded roughly 300 KB of pdf.js and its worker from a CDN in order to measure a PDF’s aspect ratio. The certificate PDFs send no CORS header, so the byte read failed every single time and the CSS default rendered anyway. It had been doing nothing, on the critical path of the most important template on the site. Removed from all seven product sections, and the snippet is left on disk deliberately orphaned so nobody rewires it.
- I broke the contact form in production. The submit button was disabled on pointerdown to prevent double-submits, which fired before the form submitted and blocked it entirely. A guard against a rare problem caused total failure of a revenue path.
- The inquiry form double-emailed every submission. The first version of file upload intercepted the native Shopify contact submit to upload and re-post. Rewritten to upload on the file input change event and let the native form post exactly once. The rule I took from it: never intercept a native form submit to re-post it.
- A sizes attribute written assuming a two-column mobile grid, when the CSS is auto-fill with a 220px minimum and goes single column on narrow phones. The browser picked a source too small and images rendered blurry on exactly the devices that mattered most. A wrong sizes is worse than no sizes.
- Gemstone subcollections rendered a completely empty filter bar for an unknown period, because the filter gate was hardcoded to two collection handles instead of checking the template suffix. Fifteen collections affected, nothing errored.
- A header compact state reverted the same day it shipped, a gallery lightbox that took five commits to place, and a filter bar redesign reverted to the original. Three visual decisions made in public on the live preview.
// 05 · The document that stops it regressing
The performance rebuild ran over about two weeks and is written up in a file in the repo that I keep as a constraint document for my own future self. It records eight hard rules, each one written after breaking the thing it prevents.
Two are worth repeating here. Never trust a single mobile Lighthouse run: identical code has swung between 25 and 68 across runs, so take the median of three to five and prefer PageSpeed Insights over local DevTools. And verify a sizes attribute against the actual grid CSS before shipping, because the browser will believe you.
Some entries are permanent constraints rather than to-do items. The main stylesheet is 48.8 KiB and render-blocking for about 2.4 seconds; roughly 600 KB of theme JavaScript ships with duplicate copies of three libraries. Neither is fixable without a build step, so they are documented as accepted rather than quietly carried as debt.
I do not push to the live theme. Everything goes to a preview theme and the owner publishes.
Built and run solo
Figures on this page come from browser network profiles, measured API timings, and counts taken from the theme repository on 2026-07-30. Business figures belonging to the merchant are published with permission and are not mine to detail further.
