The brief
VegExpress grows and processes fruit in Jos and sells it frozen, pressed and blended: frozen mango and strawberries, smoothie packs you tip straight into a blender, cold-pressed juices, lollies that are just fruit on a stick. Everything is delivered cold across Abuja, or picked up from their Wuse 2 office. They also supply hotels, supermarkets and events, and do white-label runs for other brands.
They wanted a website that felt as good as the product, that people could actually buy from, and that the team could run themselves afterwards: change a price, add a product, see the orders, without calling anyone. The reference they kept pointing at was a premium smoothie brand’s site, and the instruction was clear. Match the feel, not just the structure.
Two users, two systems
The owner needed an admin they already understood. The shopper needed a page that opens on a phone, on mobile data, in the few seconds before they go back to WhatsApp. One WordPress site can’t be both, so I split it.
WooCommerce holds the products, prices, stock, coupons and orders, on a subdomain shoppers never see. A small WordPress plugin I wrote exposes exactly what the site needs: the catalogue, the business programmes, testimonials, FAQs, recipes, and two endpoints that accept leads and contact messages. The owner edits in the admin they know, and the WooCommerce app on their phone shows new orders as they land.
The storefront is Astro. At build time it pulls the whole catalogue, renders a page per product and per range, generates the sitemap, the structured data and an llms.txt, and ships the lot to Cloudflare Workers as static files. A product page is a file on the edge. No database query, no cache to warm. If WordPress had a bad afternoon, the shop would stay up and only checkout would notice.
Making it feel like the reference
The brief came with a site to match, and the first pass missed it. It had the right sections and none of the feel. So the second pass was about the things you notice without being able to name them: a header that overlays the hero and solidifies on scroll, mega-menu panels that open on hover with the ranges laid out as a grid, a basket that slides in from the side rather than sending you to a cart page, products drawn as pack shapes when a photo was missing, a marquee of product claims, and scroll-driven reveals on Lenis and GSAP that stand down under reduced motion.
Type does a lot of the work. Headings are set in Exposure, a chunky serif that gives the brand its voice, and Inter carries everything else. The palette is the client’s: deep green, cream, a loud orange for the one button that matters on each page.
The hero went through several rounds with the client and ended up as five overlapping photo cards, a fan of packs and bottles with a smoothie pack in the middle, because they wanted the range visible before anyone scrolls. Product photos were the longest-running thread. The client’s shots came on white, so I tried an automated cutout pipeline. It looked clever and was impossible to hand over, so I threw it away. Now the photo simply fills its card, and whatever the owner uploads to WooCommerce is what the site shows.
A basket that knows nothing, a server that checks everything
The basket lives in the browser. Add, remove, change quantity, clear, all in localStorage, with a small event so the drawer and the header count stay in step. It’s fast and it works offline, and it’s also completely untrusted.
When you check out, the browser sends product IDs and quantities and nothing else. The Worker re-prices every line from the catalogue, applies the referral code through WooCommerce’s coupon engine, adds the delivery fee for the zone or sets it to zero for pickup, and creates the order in WooCommerce. WooCommerce’s total is the only total. If WooCommerce drops a line because a product has since been hidden, the order is refused rather than quietly placed short.
Payment stays on the site. The Worker initialises a Paystack transaction server-side with the WooCommerce total and returns an access code that opens Paystack’s popup on the checkout page; the customer never leaves. When the popup reports success, the Worker verifies the reference with Paystack, compares the amount and currency to the order, and only then marks it paid. Paystack’s webhook does the same check independently, with the signature verified, so a customer closing the tab at the wrong moment doesn’t leave a paid order unpaid. The confirmation page says “Your order has reached our sales team” because that’s what the client wanted a customer to feel.
For people who’d rather talk to a human, every product and the basket itself have a WhatsApp route that composes the order into a message.
Delivery, pickup and referral codes
Abuja is three delivery zones with three flat fees, plus free pickup from the office between ten and four. Pickup hides the address fields, zeroes the fee, and puts the collection address and hours on the order and the confirmation page.
The client asked for discount codes, then clarified: they didn’t want discounts, they wanted to know which creators were sending customers. So referral codes are WooCommerce coupons that are allowed to be zero percent. The code is tracked on the order like any coupon, and a Creator Coupons report in the plugin shows each creator’s orders and revenue. Prices stay intact and the owner creates codes in a screen they already have.
Forms that reach a person
The business enquiry and contact forms post to the Worker, which checks a Turnstile token first, then a honeypot, then the fields, and only then hands the message to WordPress. The plugin stores it, emails the sales team from the company’s own address so it actually arrives, and offers a CSV export so leads can go into a spreadsheet. Bots get a quiet 403.
The site rebuilds itself
This is the part that makes the handover real. When anyone saves a product in WooCommerce, the plugin posts to a Cloudflare deploy hook, debounced so five quick edits become one build. A couple of minutes later the site has rebuilt with the new catalogue. The owner changes the mango price in the app on their phone and the website follows, and nobody has to know there’s a repo, a Worker or a build behind it.
Everything the site needs at build time comes from the catalogue, and the build refuses to complete in production if it can’t read it. A site full of stale products was the one outcome I didn’t want to be possible.
Things that bit
Hosts have opinions. The WordPress host redirected the standard REST path to the homepage and blocked PUT and DELETE outright. Every call now goes through the query-string route, and updates are POSTs with a method override header. Not elegant, but it’s the difference between working and not.
Build-time versus run-time. Cloudflare injects runtime variables into a Worker, but a prerendered page only sees what existed when it was built. I learned that twice: once when the catalogue URL wasn’t visible to the build, and once when the live checkout kept shipping a value from an old build. The lesson was to make nothing on the page depend on a build-time secret at all. Paystack’s popup only needs the access code the server returns, so the public key left the page entirely.
The dev server lies. Astro’s dev server caches the catalogue when it starts. After a bulk price change in WooCommerce I stared at wrong prices for a while before remembering that. In production the rebuild pipeline makes it impossible. Locally, restart.
What I’d keep
Split the users. The owner got the admin they already knew plus an app on their phone. The shopper got a site that opens like a landing page. Neither system had to pretend to be the other.
Trust nothing from the browser. The basket is a convenience. Prices, fees, coupons and totals are all decided on the server, and the payment is verified twice.
Make handover the feature. Photos fill their box, prices come from WooCommerce, the site rebuilds on save. Every clever thing that would have needed me to keep running it got cut.
