In short: a complete online jewelry store for the UAE — real-URL storefront, camera-based virtual try-on that tracks the face and hand in the browser, a guided ring, bracelet and necklace size finder, a cash-on-delivery checkout that re-prices every order on the server, order tracking, and a full admin panel. Plain PHP 8.1 with no framework and no database: catalogue, orders and settings live in locked JSON files, so it runs on ordinary shared hosting. Search, AI-search and shopping visibility are built in rather than bolted on — Product and Offer structured data with UAE shipping and return policies, a Google Merchant / Meta / TikTok product feed, a generated llms.txt and IndexNow pings. Designed, built and shipped end to end by one developer, and live at zilajwl.com.

The brief: selling jewelry people can’t touch

Zila sells everyday imitation jewelry — base metal with a gold-plated or silver-plated finish — to customers across the seven emirates, almost all of whom arrive from Instagram or TikTok on a phone. In that market three things decide whether a sale happens and stays happened. Buyers want to pay cash at the door, not type a card into a shop they found five minutes ago. They want to know how a piece will look on them, because a hoop that looks delicate on a model can look very different on their own face. And they want to know what size to order, because a ring that doesn’t fit becomes a return, a second delivery fee and a lost customer. The store had to answer all three, stay cheap to run, and be something the owner could manage alone without a developer on call.

The shape of the system

Three parts share one small core. The storefront is a single-page application with real URLs — /shop/rings, /product/<id>, /size-guide, /try-on, /track — driven by the History API, with a bag, a wishlist, quick view, search and a checkout layer. A small JSON API exposes three actions: the catalogue, placing an order and tracking one. A password-protected admin panel manages products, categories, orders and settings. Underneath is roughly 4,500 lines of hand-written PHP, JavaScript and CSS — no framework, no build step, no database server — which is why the whole store deploys by unzipping one archive onto shared hosting and keeps running without anything to patch or pay for.

Server-rendered for crawlers, single-page for people

Single-page shops usually trade search visibility for speed; this one refuses the trade. Every URL is answered by one PHP front controller that renders the real content first — the H1, product lists with prices and finishes, specifications, delivery terms and FAQs — together with the page’s title, description, canonical, Open Graph tags and JSON-LD. The same request embeds the full storefront payload (catalogue, categories, specs and store config) directly into the page, so the JavaScript application takes over without a second round-trip and every later navigation is instant. Search engines and AI crawlers get a complete HTML document; shoppers get an app. Renamed products and categories are kept in a redirect map and answered with a 301 before rendering, so changing a slug in the admin never breaks a shared link or an indexed URL.

A virtual try-on that runs entirely on the phone

The try-on page opens the camera (front or back) or accepts an uploaded photo, and places the chosen piece on the shopper in real time. Earrings and necklaces follow the face; rings and bracelets follow the hand. Tracking uses Google’s MediaPipe Tasks Vision — a face landmarker and a hand landmarker — loaded by dynamic import() only when someone actually opens the try-on, so it adds nothing to the rest of the store. Each model is created on the GPU delegate with an automatic CPU fallback, cached once loaded, and switched between video and still-image modes as the source changes. Detection is throttled to roughly 30 frames a second inside a requestAnimationFrame render loop.

Placement is geometry, not guesswork. On the face, the two side-of-face landmarks give the face width and its roll angle, and forehead-to-chin gives its height; the earlobes are projected 13% of face height down the face’s own normal from each side, and a necklace hangs 42% of face height below the chin — so pieces tilt when the head tilts. On the hand, a ring sits 40% of the way between the ring finger’s base joints, sized from the knuckle spacing and rotated to the finger’s angle; a bracelet sits just past the wrist landmark, scaled to 1.12× the palm width. Because everything is derived from the shopper’s own proportions, a 30 mm hoop is drawn as a 30 mm hoop. Positions are smoothed with linear interpolation between frames to kill jitter, and the selfie camera is mirrored correctly.

The jewelry itself is not a pasted product photo: each piece is drawn procedurally on canvas from its try-on style — hoop, stud, pearl drop, chain, pendant, tennis line, beads — with metal gradients per finish, pearl and crystal shading and a soft drop shadow, so it sits on skin convincingly at any angle. If tracking is unavailable on a device, the page falls back to manual placement: drag to move, pinch or scroll to resize, sliders for size, rotation and height. A shutter button exports the composed canvas as a PNG to share before ordering. The camera stream and photos never leave the device — there is no upload endpoint for them at all.

A size finder that prevents returns

Ring sizing is the biggest source of avoidable returns in online jewelry, so the size guide is a guided finder rather than a chart. It works on the standard US / UK / EU (ISO) system, where the EU size is the inside circumference in millimetres: a 17-row table from US 3 to US 11 in half sizes, matched by converting any measurement to circumference and taking the first size within a 0.4 mm tolerance. Shoppers can get there three ways — measure a ring they already own, wrap a strip of paper around the finger and measure the mark, or measure on the screen itself after calibrating it against a standard bank card (85.6 × 53.98 mm), which turns pixels into real millimetres on any display. Each step has its own hand-drawn SVG illustration. The result can be saved, and the product page then pre-selects that size and shows how the ring will fit.

Bracelets and necklaces get the same treatment: a wrist measurement plus a chosen fit — snug, comfort or loose, adding 1.3, 2 or 2.6 cm — maps to XS–XL, and a necklace-length visual walks from collar (33 cm) to opera (75 cm) so the buyer can see where a 45 cm chain actually falls.

A cash-on-delivery checkout that trusts nothing

The checkout asks only for what a courier needs — name, phone, emirate, area, address — and the browser sends nothing but product IDs, sizes and quantities. The server re-reads every item from the catalogue, skips inactive products, rejects sold-out ones with a clear message, clamps quantities to 1–10 and lines to 30, recomputes the subtotal, and applies the delivery rule (free above a threshold, a flat fee below it) itself. A price edited in the browser simply has no effect. Order numbers are sequential (ZL-1001, ZL-1002…) and are allocated inside an exclusive file lock, so two simultaneous orders can never receive the same one.

Abuse is handled without friction for real buyers: a hidden honeypot field silently absorbs bots, request bodies are size-capped, and a per-connection rate limit (eight orders an hour, thirty tracking attempts per ten minutes) is keyed on a SHA-256 hash rather than a stored IP address. Each new order is emailed to the store and, because the region lives on WhatsApp, the confirmation screen offers a one-tap WhatsApp hand-off with the full order pre-written — which also acts as the fallback if the server can’t be reached, so a sale is never lost to a bad connection. Purchase events fire to GA4 and the Meta Pixel when those IDs are configured.

Customers track their own orders with the order number and their phone. Phones are normalised to the UAE format (handling 00, a leading 0 and bare nine-digit mobiles) and matched on the last nine digits, so “050…”, “+971 50…” and “971 50…” are all the same person — and no one can look up an order without knowing both. Each order carries a status history across six states, from New through Out for delivery to Delivered, Cancelled or Returned, shown as a timeline with a plain-language message.

No database, still safe

For a catalogue of tens of products and a steady flow of orders, a database server is cost and maintenance with no benefit, so storage is plain JSON — but done carefully. Reads take a shared flock; writes go through a single update_json() routine that holds an exclusive lock across the whole read-modify-write, which is what keeps order numbers and rate-limit counters correct under concurrent requests. The data directory is denied to the web twice — by its own .htaccess and by a site-wide rule blocking .json files — and the uploads directory is configured so that no script can ever execute from it. On first run the store seeds itself from a bundled catalogue, and settings are merged over the seed defaults, so new settings introduced in a later version appear with sensible values instead of breaking an existing install.

An admin panel the owner actually runs

The owner sets a password on first visit; it is stored with password_hash(), the session ID is regenerated on login, and every state-changing form carries a CSRF token verified in constant time. From there the panel covers products (prices and was-prices, finish, stock, try-on style, ring sizing, specifications and multiple images), categories, orders (status changes logged to the order’s history, private notes, a one-tap button to message the customer on WhatsApp, and CSV export) and settings (delivery fee and free-delivery threshold, exchange window, cities, social links, verification codes, GA4 and Meta Pixel). Uploaded images are validated as real images before they are stored.

SEO is editable without being required: every product and category can take a custom title, description, Open Graph title and a noindex switch, and every image its own alt text and title — but leave them blank and the store generates good ones from the product’s name, finish, type and price. Renaming a product records a 301 automatically, and saving changes pings IndexNow so Bing — and with it ChatGPT search and Copilot — re-crawls the affected pages within minutes.

Built to be found — by Google, by shopping tabs and by AI assistants

Each page type emits its own JSON-LD graph. The store is an Organization + OnlineStore with brand, alternate names (including the Arabic spelling) and an area served of the UAE. Every product is a Product with images, SKU, finish and dimension properties, and an Offer in AED that declares cash on delivery as the accepted payment, OfferShippingDetails (0–1 day handling, 1–3 days transit, the real fee) and a MerchantReturnPolicy — a finite exchange window for most pieces and “no returns” for earrings, exactly as the store’s policy states. Category pages are a CollectionPage with an ItemList, the home page carries an FAQPage, the size guide a HowTo, and the about page an AboutPage, all with breadcrumbs, en-AE hreflang and UAE geo-targeting.

Around that sit four generated endpoints, rewritten to their familiar names: a sitemap.xml, a robots.txt, a feed.xml product feed in Google Merchant format (prices and sale prices, availability, Google product category, material, colour and UAE shipping) that also feeds Meta Commerce Manager and TikTok Catalog for free product listings, and an llms.txt that gives AI assistants the store’s facts, policies, categories and every product with its price — including a note that “Zila” here is a jewelry brand, not the Hindi/Urdu word for a district, so answer engines don’t confuse the two.

Craft

The front end is hand-written vanilla JavaScript: scroll-revealed sections, split-line headline animations, counters that ease up from zero, magnetic buttons, an add-to-bag animation that flies the product image into the bag, and toasts with an inline action — all of it switched off for visitors who set prefers-reduced-motion. The brand mark and every icon are inline SVG; product images are served from a CDN at the width each layout needs; the bag, wishlist and saved ring size persist in the browser across visits. HTML responses send nosniff and a strict referrer policy, and the API never caches.

Stack & scope

PHP 8.1 with no framework and no database — JSON storage with file locking on standard shared hosting; a vanilla-JavaScript single-page storefront with History-API routing over server-rendered pages; MediaPipe face and hand tracking with canvas rendering for the virtual try-on; a calibrated size finder; a server-validated cash-on-delivery order API with rate limiting and order tracking; an admin panel with CSRF protection, image validation, per-item SEO, automatic 301s and CSV export; and a generated layer of JSON-LD, sitemap, robots, llms.txt, Merchant feed and IndexNow — roughly 4,500 lines of application code. Product design, front end, back end, SEO and deployment — end to end by one developer.

See it live at zilajwl.com. Launching an online store for the Gulf — cash on delivery, WhatsApp and search built in? Let’s talk.

Frequently asked questions

How does the virtual try-on work, and are photos uploaded?

The try-on loads Google's MediaPipe face and hand landmarkers in the browser only when the page is opened, running on the GPU with a CPU fallback. Earrings and necklaces are positioned from face landmarks and rings and bracelets from hand landmarks, scaled to the shopper's own proportions and drawn on a canvas. The camera stream and any uploaded photo are processed entirely on the device; there is no endpoint that receives them.

How does the size finder work out a ring size?

It uses the US / UK / EU (ISO) system, where the EU size is the inside circumference in millimetres. The shopper measures a ring they own, a paper strip wrapped around the finger, or a ring held against the screen after calibrating it with a standard 85.6 × 53.98 mm bank card. The measurement is converted to circumference and matched to the first size within 0.4 mm, and the saved size is pre-selected on product pages.

How is a cash-on-delivery order protected from tampering and spam?

The browser only sends product IDs, sizes and quantities. The server re-prices every line from the catalogue, rejects sold-out items, clamps quantities and computes delivery itself, then allocates a sequential order number inside an exclusive file lock. A honeypot field, body-size limits and a hashed per-connection rate limit block abuse, and a pre-filled WhatsApp message is offered as a fallback so no order is lost.

Why no database, and how does the store get found?

For a catalogue of this size a database server adds cost and maintenance without benefit, so data lives in JSON files written under exclusive locks and blocked from the web. Every URL is server-rendered with its own Product, Offer, shipping and return-policy structured data, and the store generates a sitemap, a Google Merchant product feed used by Meta and TikTok catalogs, an llms.txt for AI assistants, and IndexNow pings on every change.