mjdc.io Get in touch →
← Back to the notebook

Project

Building a Shopify Store to Help Out My Local Bike Shop

My local bike shop got hacked offline and quoted $20k for a Shopify rebuild. I designed and built one for him instead, most of it in a weekend.

1 July 2026 · Michael Conroy

How I rebuilt my local bike shop’s store on Shopify. The constraints that shaped it, the shortcuts that worked, and the decisions that made a fast turnaround possible.

My local bike shop, Vanilla Cycles, lost its website. The old WordPress site got hacked, and the quote to rebuild it on Shopify came back at $20,000. Mark, who runs the shop, had thrown up a Squarespace stopgap himself, but he felt like the shop had lost its online identity a bit.

Mark’s a legend. And Vanilla is a genuine local institution for cyclists of all kinds, so I offered to rebuild it for him. Most of it got built over one weekend in late June, plus a few weekday sessions either side, with an agent writing a lot of the code. Here’s how I turned it around.

Built for the handoff, not for me

The most important call came before anyone wrote a line of Liquid: the site had to be fully editable by Mark once I left. He’s not opening a code editor. Price changes, photo swaps, section reordering, all of it has to happen in the Shopify theme editor.

So every piece of marketing copy, image and link is a section setting or a block. Every custom section ships with a schema and sensible presets. Repeated things like service tiers are blocks, and product cards come straight from collections, so a new bike is a product edit, not a page edit. The test I held to: could Mark rebuild this page from the editor if it got deleted? If not, I’d hardcoded something I shouldn’t have.

It’s more work upfront. It’s also the whole point.

Designed around very little photography

The other constraint was that Mark didn’t have any professional photography. No shoot budget, no library, just whatever was already out in the wild. I ended up mining the shop’s Facebook and old Instagram for everything I could salvage.

That rules out most modern Shopify themes, which are photography-first. Big edge-to-edge heroes and full-bleed lifestyle shots look broken the moment you can’t feed them, and faking it with stock would’ve undone the whole point. So I designed for scarcity. Before building anything, I worked the direction out with Claude Design: lean on typography, the brand colour and cream/sand bands to carry the pages, so a handful of imperfect real photos go further than a hundred polished ones. Settling that first meant I wasn’t redesigning around missing images mid-weekend. The photos punctuate the design; they don’t hold it up.

Afterwards I measured both homepages at desktop width. Shopify’s own Horizon demo is 65.5% photograph. Vanilla’s is 11.1%, the rest mostly colour bands and big type. Different stores, so it’s rough, but that’s the gap between a theme you have to feed and one you don’t.

Vanilla Cycles

How much of each homepage is photos

Most shop themes are built around big, beautiful photos. The shop didn’t have any, so I built a homepage that holds up without them.

Shopify’s Horizon demo store

65.5%photograph

2.3%type and colour

A full-bleed hero and eight product photos.

Vanilla Cycles

11.1%photograph

84.7%type and colour

Three photos, three striped placeholders. The rest is type and colour.

Both pages are the same width, but Vanilla’s runs 5,075 px to Horizon’s 1,926 px. It’s 2.6× longer and still 85% type and colour.

Go through Horizon’s demo homepage cell by cell and it’s mostly a full-bleed hero and eight product photos. Take those away and it’s 97.7% blank white. Vanilla’s homepage is cream and sand bands, ink, teal and big type, so when its three photos go, the slots drop back to stripes and the page still holds up.

What each homepage is made of, share of cells
Painted asHorizon demoVanilla Cycles
Photograph65.5%11.1%
Striped placeholder0%4.2%
Type2.3%9.9%
Colour band0%74.8%
White32.2%0%

Both live homepages, checked on a 20px grid at 1440px wide in October 2026.The photos here are blocky stand-ins for the real ones.

Horizon with custom sections

I built on Horizon and tried to leave its header, footer and announcement bar alone rather than fight them. I didn’t quite manage it. The header gained an editable “Book a Service” button, the announcement bar got restyled and the footer is my own. Everything else is a set of custom vc-* sections: hero, service pricing, shop grid, X-Lab showcase, garage, visit/map, reviews, footer.

Each one is self-contained, with its markup, styles and any JS in a single .liquid file. Shopify serves all that CSS together, so the isolation really comes from a vc- prefix on every class. Adding a section is one safe operation, and deleting it takes its CSS and JS with it. On a weekend, that isolation is what let me move section by section without regressions stacking up.

To make the site feel coherent and cheap to rebrand later, I defined the visual language once. It lives in a single snippet, vc-brand.liquid, rendered in the <head>: fonts, plus CSS custom properties for surfaces, text, layout constants and colour. Every section references those tokens. Mostly. There are still 102 hardcoded hex colours in the section CSS (white, placeholder stripes, tints) and no type scale at all. I’d close both gaps next time.

Keep the work that already works

Mark had already made his own booking app on Replit, and it worked, matching how the shop actually takes service jobs, and he understood it because he wrote it. The catch was that his app occasionally drops offline, because Replit apps sleep. A “Book a Service” button pointing at a dead page is worse than no button.

Mechanically, every booking link on the site is a plain <a> to /pages/book, an on-site request form. On page load, not on click, a bit of vanilla JS fires a fetch at Mark’s app with a 4-second AbortController timeout. App answers, every booking link gets upgraded to point at it in a new tab. Times out or errors, nothing changes, and the customer lands on a page built to catch them.

My first version did it the other way round, app first with the form as the fallback. Eight minutes later I flipped it. This way round, no JavaScript, a slow connection or a sleeping app all still leave the customer somewhere that works. It’s not a real health check, though. The request goes out as no-cors, so all the browser learns is that something answered. It’s a knock on the door.

Migrating out of Squarespace

Products came in via Shopify’s CSV import: 49 rows, 27 products. The Squarespace image export URLs were a little unreliable, and at least one was simply broken and had to be fixed by hand before the import would resolve it. Good lesson to never trust an export’s asset URLs, because a bad row fails quietly and you find it later as a blank card.

What I’d do again

The store’s live at vanillacycles.com. It’s fast, secure, fully in Mark’s hands, and finally reading like the great community shop that it is.

More from the notebook.All writing →