Back to Projects

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.

Four soy wax melt products from the storefront catalog: black raspberry vanilla, lavender, cotton candy, and vanilla bean buttercream

Project Details

Tags

PHP Square API OAuth E-commerce Structured Data Technical SEO Client Work

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.

Decisions Worth Defending

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.

Before
<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 -->
After
<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.

1Inventory, in Square, where the business already kept it
3Fulfilment methods modelled as real Square order fulfilments
0Code changes needed for normal operation after handoff

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.