770 commits later: what we rebuilt, deleted and abandoned on a watercolour artist's shop
On 17 February 2026 I made the first commit of a portfolio site for Nadia Poe, a Surrey south-west London based watercolour artist. Eight months and 770 commits later it's a shop with checkout, a kiosk mode for market st
On 17 February 2026 I made the first commit of a portfolio site for Nadia Poe, a Surrey south-west London based watercolour artist. Eight months and 770 commits later it's a shop with checkout, a kiosk mode for market stalls, 36 architecture decision records, and a button that hangs a painting on your wall through your phone's camera.
If you've read my earlier post on the stack and what it costs, this is the other half: the story of how it got that way. Most of that wasn't planned. This post follows the decisions that changed the site, and the ones we reversed. I've put screenshots of the old and new versions next to each other, because that's the most honest way to show what changed.
Day one and day two
The first commit was an Astro 5 site with a gallery, a shop and a hand-rolled Stripe Checkout endpoint. It had eight placeholder paintings. The next day I added a Medusa v2 backend, so the shop's source of truth would be a real commerce engine and not markdown files.
Here are the two ends of the timeline:
The photographs are a cheat, and I should say so up front. When I cleaned the repo's git history to shrink it, I stripped old image blobs, so no old checkout has its painting photos. For these screenshots I put today's photo of the same painting into each old build. The layouts, copy and components are the real ones from each date.
The February hero is a title over a photo. The October one puts two paintings on a wall (one framed original, one unframed print) with a button under each. That's the whole story of the site in one picture: it started as a portfolio and became a shop.
One painting has survived the whole journey: Sunset in Langdale Valley. It's the only work from the first week still on the site, so it's the through-line for every comparison below.
The graveyard: hosting and video
The part I'd least like to repeat was the hosting. Medusa started on Render's free tier: 512 MB of RAM, and a 30-60 second cold start after 15 minutes idle. That is a bad experience for someone about to pay for a painting.
I tried three things before settling:
| Attempt | What happened |
|---|---|
AWS EC2 t4g.micro
|
Ran out of memory under real checkout load. The infrastructure around it (CloudFormation, Nginx, a custom deploy script) was out of proportion for a shop this size. |
| Oracle Cloud Always Free | Free ARM instances, in theory. An automated retry loop ran for days and never got one in the London region. |
| Hetzner CAX11 | 2 ARM cores, 4 GB, dedicated resources at a predictable price. It worked, and it's still there. |
The lesson is in the Oracle ADR: a free tier with no capacity guarantee cannot be planned around. I'd written migration tooling before checking I could get a machine.
Video went the same way. I put short process clips on ImageKit because of its free tier. The free tier limits got reached almost immediately, so everything moved to Cloudflare R2, where egress is free.
Neither of these shows up in a screenshot, but they cost more of my time than anything that does. Both are recorded as decisions that were rejected, with the reason. Whoever picks this up in a year, probably me, will thank me for that.
Delete the page nobody needed
The original gallery was a masonry grid of paintings with filters and sorting. The shop was the same paintings with prices. Every change had to land twice, on a page I'd already decided was less important.
In August I replaced the gallery with a kiosk slideshow: one painting at a time, crossfading, with a tombstone-style label showing the title, medium, size and price. It's for a tablet on a market stall. You can try it yourself at /gallery/ (the URL kept its old name), and press Start.
The interesting engineering was all in the details of running unattended:
-
Screen Wake Lock is released silently when the tab loses focus, so it's re-acquired on
visibilitychange. - Fullscreen and Wake Lock both need a user gesture. A "Start slideshow" screen provides it, and doubles as the no-JS fallback.
- QR codes are generated at build time. A kiosk in a marquee shouldn't depend on venue wifi to draw a QR code.
- The page is
noindex, with no JSON-LD. Googlebot never presses "Start", so structured data would describe content it can't see.
Removing a feature turned out to be one of the most satisfying commits. The shop got simpler, and the market stall got something the browsable grid could never have been.
One visual grammar
By September the site spoke two visual languages. The shop had settled into a calm one: cream page, paintings at their natural aspect on a hairline plate, serif titles, right-aligned prices. The home page, redesigned a month earlier, spoke another: white cards with shadows, centred headings, hover lifts, and a filled green button.
Both were fine on their own, and together they looked like two different shops.
I wrote the rules down as an ADR, and the whole site followed. The short version:
- Cream surface only. No white cards, no shadows on in-flow content.
- Radii no bigger than 4px.
- Paintings always at natural aspect, never cropped in a grid.
- Left-aligned headings. Two button shapes. One kind of text link.
- Sage green is structural only: rules, active states. Never body text, never a filled button.
The rule list is short on purpose. A design system a one-person team can hold in their head beats a long one nobody consults.
Splitting one page into two
For months, every painting had one product page with a selector: Original, A4 print, A3 print. It looked fine, but it forced one page to serve two different buyers. One is after a one-of-a-kind £120-700 painting, and the other wants a £30 reproduction. When the original sold, the page became a dead end for the print.
On 18 October I split every painting into an original page and an art print page, each with its own images, copy and structured data. The old /shop/ URLs all became 301 redirects, and the combined page is gone.
The rollout plan was a build flag and a six-painting pilot. I dropped both the same day. On a site this size a two-to-three-week pilot would never produce a usable signal, and keeping the combined page alive next to the split ones made the code harder to follow than either version alone. So I shipped it to all of them, and wrote down why.
The same week I also folded the painting images into a single pipeline: one registry, one generator, descriptive file names, and alt text written per image role.
The strip below shows the product page on mobile at five points in the year. Look at the breadcrumbs, the heart, and the "View on your wall" link that appears on the last one:
Collections and places: planning SEO with no data
In October I had a problem. The site had been stable for barely a month, so there was no search data to learn from. Keyword tools only echo what's already popular.
So I planned from intent instead. Someone who searches for "Lake District watercolour" has an obvious question, and the site had no page that answered it. Every link to a collection pointed at a filtered /shop/ that canonicalised back to the shop root.
I built landing pages for three collections and seven places:
- A place hub like the Lake District lists every painting made there, with originals and prints priced side by side.
- A collection hub like British Landscapes groups places into a theme.
- Each hub has a short essay that links into individual paintings, such as the Langdale Pikes at sunset and the prints.
The hubs are data, not code. A Markdown file with kind: place, a few match rules and an essay is a new page. The River Thames and Richmond Park pages were each a file and an afternoon.
I don't know yet if it works. The plan is to review it in mid-January with real search data. What I do know is that the site finally has an answer to the question people were already asking.
The part where a painting hangs on your wall
The most recent week has been about augmented reality. It started as a trial on one painting on 21 October, and there was a custom WebXR placement system two days later.
The idea is simple. A buyer wants to know whether a 10 x 14 inch painting will look right above their sofa. Pictures can't tell them. The phone's camera can.
I started with Google's <model-viewer> and it went wrong fast:
-
Scene Viewer crashed outright on my Pixel 8a with a
StackOverflowError, about 100 ms after launch. It crashed the same way when I launched Google's own astronaut demo model fromadb, with no browser and no site involved. - model-viewer's WebXR wall mode floated while it waited for ARCore to detect a plain painted wall, jumped whenever it re-read the wall's angle while dragging, and drifted because it didn't use anchors.
So I wrote my own. It uses three.js and WebXR, with no surface detection at all. Aiming at walls and then at furniture were both slow and erratic on real walls, while ARCore's tracking of the phone itself is dependable. So the interaction became physical: hold the phone flat, touch its top edge to the wall where the middle of the painting should go, and tap Set.
iPhones needed a separate fix. Quick Look hung paintings by their bottom edge, sticking them out of the wall instead of flat against it. The cause was a convention mismatch in the generated USDZ file, fixed with a quarter-turn.
Desktop visitors get a QR code that carries them to the same page on their phone, and the home page has a "See it on your wall" section so the feature isn't hidden on product pages. The phrase I'm proudest of on that page is a plain one: the wall view works on "phones and tablets that support augmented reality". It names a capability and never mentions how old the device is.
If you're reading this on a phone, you can try it now: open the home page and scroll to "See it on your wall".
What I'd tell my February self
- Write the decision down when you make it, especially the rejected ones. The ADRs for the AWS and Oracle attempts are the most useful files in the repo. They stop me re-trying them.
- Delete the duplicate page. The gallery was a copy of the shop. Removing it simplified everything else.
- Cut the pilot when it can't tell you anything. A rollout plan is worth following only if it produces a signal.
- Write the rules down once. The visual grammar is a one-page Markdown file, and it settled arguments I'd otherwise have had with myself every week.
- Check a free tier can actually be provisioned before building on it.
The paintings are Nadia's, and the commit history is mine. You can see how it all ended up at nadiapoe.co.uk.
Questions about any of the decisions above? Ask in the comments. Several of them are recorded as ADRs, and I'm happy to go into more detail on the ones people care about.
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.





