Wickedly Delightful Scents – One Inventory, Not Two
A custom storefront built directly on the client's existing Square catalog, so the point-of-sale she already runs at craft fairs stays the only place stock lives — plus a structured-data pass that made the site legible to search engines and AI assistants.
Project Details
Tags
Challenge
A small-batch soy wax melt maker in Port Huron sells two ways: in person at markets, and increasingly online. The in-person half already ran on Square, which meant Square already held the real catalog, the real prices, and the real stock count.
Every standard answer to "I need an online store" duplicates that. WooCommerce or Shopify would stand up a second catalog and a second inventory, and from that moment the business has two numbers for how many lavender melts exist. They agree right up until a busy Saturday market, and then a customer buys something online that was sold out of a plastic tub three hours earlier.
There is no elegant fix for that once it exists. Sync plugins turn a data problem into an integration problem you now also have to maintain. So the interesting constraint was to build the storefront such that a second inventory never comes into being.
Solution
The storefront is plain PHP reading Square as the system of record. Products, variations, prices, images, and stock levels are Square's. The site renders them and hands checkout back to Square. There is nothing to reconcile because there is only one copy.
- The cart validates against live stock, twice. Quantities are checked against Square inventory before a checkout is created, and existing carts are normalised in the browser on load — so a cart left open in a tab overnight cannot carry yesterday's availability into today's checkout.
- Fulfilment is modelled explicitly. Standard shipping, express, and local pickup are sent to Square as real order fulfilments rather than as line items pretending to be shipping. Shipped orders ask for an address; pickup orders never show a shipping field at all, because asking a neighbour for a delivery address to walk to the door is how carts get abandoned.
- Admin is Square OAuth, not another password. The owner signs in with the account she already has. There is no separate credential to forget, reset, or reuse.
- A cache with a visible lever. Catalog responses are cached for speed, and "Clear Product Cache" sits in the admin where the owner can reach it. The alternative — explaining that a new product will appear in fifteen minutes — is a support call every time she adds a scent.
Decisions Worth Defending
- The storefront locks to one merchant account, permanently, on first connect. An OAuth storefront that will connect to whatever Square account authorises it is a storefront that can be quietly repointed at someone else's catalog. Locking on first connect closes that.
- The unlock is a one-time recovery key, shown once. Locking creates an obvious failure mode: connect the wrong account and the site is bricked. So the lock ships with its own key — generated at connect time, displayed exactly once, redeemable at a recovery URL to clear the token and merchant lock. Day-one checklist item number two is putting it in her password manager.
- An empty catalog shows an empty state, not a spinner. A spinner that never resolves reads as a broken website. "Nothing here right now" reads as a shop between batches, which is the truth.
- The homepage contact form was deliberately deferred. Shipping a form with no backend so the page looks complete means silently dropping customer messages. Routing support to a real, working contact section was the honest interim.
The Structured Data Pass
Once the store worked, it still described itself like nothing in particular. The title said "Handcrafted Wax Melts" — true of several thousand shops — and there was no machine-readable statement anywhere that this was a business in Port Huron, Michigan, selling specific products at specific prices. Search engines had to infer all of it, and AI assistants answering "where can I buy soy wax melts near Port Huron" had nothing to read.
<title>Wickedly Delightful Scents — Handcrafted Wax Melts</title> <meta name="description" content="Handcrafted wax melts made with premium ingredients."> <!-- no canonical --> <!-- no location --> <!-- no structured data --> <!-- no share cards -->
<title>Handcrafted Soy Wax Melts | Wickedly Delightful Scents | Port Huron, MI</title> <link rel="canonical" …> <meta name="geo.placename" content="Port Huron, Michigan"> "@type": ["Store","LocalBusiness"] "@type": "Product" × catalog "@type": "AggregateOffer" + OpenGraph, + Twitter cards
The part worth describing is not the tags — it is that the product markup is generated, not written. The shop page walks the live Square catalog and emits an ItemList of Product nodes; each product page emits its own node with a single Offer when there is one price and an AggregateOffer with a real low, high, and offer count when the item has variations. Availability, price, and category all come from the same call that renders the page, so the structured data cannot drift from what the customer sees. A hand-written block would have been stale the first time she raised a price.
Two smaller details I would defend in a review. Product pages that fail to resolve emit noindex, follow rather than a soft 404 — a discontinued scent should stop being indexed without severing the links pointing at it. And product descriptions fall back to a generated sentence naming the scent and where it was poured when the Square description is empty, because a missing meta description is a page Google summarises for you, badly.
Outcome
The storefront is live at wickedlydelightfulscents.shop and handed off, with the day-one checklist run end to end.
- Custom PHP storefront, cart drawer, and product pages reading Square catalog and inventory directly
- Square-hosted checkout with shipping and pickup fulfilments, validated against live stock before creation
- OAuth admin with merchant lock, one-time owner recovery key, and a documented account-switching path
- Full structured-data layer generated from the live catalog:
Store/LocalBusiness,Product,Offer/AggregateOffer,ItemList - Written handoff covering admin actions, recovery, support boundaries, and what the client should never need a developer for
What I would carry into the next one is the handoff document itself. Writing down the support boundary — products and inventory in Square, cache clearing in admin, recovery key in the password manager, and nothing else — did more to keep the project finished than any amount of code quality. A build is not done when it works; it is done when someone else can run it.