The anonymized sample site already in the portfolio, and the post about scrubbing it clean, both cover the same underlying project: a real pitch, built for a real metalworking and locksmithing business, that's still sitting private behind a password while they decide whether to go ahead. That post was about what got deleted before anything went public. This one is about the build itself — no names, no village, nothing that points back to anyone real, just how the thing actually got made.
Starting from a brief, not a blank page
Before writing a line of HTML, I wrote myself a brief: static site only, hand-written HTML and plain CSS, no framework, no build step, no CDN requests at runtime — self-host everything, because this is a German site and DSGVO makes that non-negotiable, not a nice-to-have. Never invent a fact — prices, certificates, opening hours, staff count, service radius, all of it only from what the client's own material actually supported. Anything missing gets logged as an open question and marked [TBD] in the HTML, never quietly papered over with confident-sounding filler.
Writing the constraints down first, before any design decision, is what keeps an AI-assisted build honest. It's much easier to catch "this doesn't belong on a real business's site" against a written rule than against a vague sense that something feels off.
Phase 0: look at every photo before deciding anything
The client had supplied a folder of real photos — the workshop, finished work, an owner portrait. Before any layout got sketched, every single one got opened and logged: pixel size, orientation, what's actually in the frame, described concretely enough to tell two similar shots apart, plus a quality rating from A (hero-capable) down to X (don't use — blurry, a stranger's face in frame, a visible number plate, a cluttered background). Anything that needed a number plate or a passer-by cropped out got flagged right there.
The rule that mattered most: only photos actually looked at could appear in the plan. If a section of the site had no real photo to back it, the section either got redesigned as pure type and line art, or it didn't exist — never a placeholder grey box waiting to be filled in later. Three pages with real content beat eight thin ones with stock-photo energy.
Rejecting the AI defaults
The design plan had to name an actual palette and typeface pairing before any CSS got written — and it had to explicitly reject a specific list of what I'd started calling "AI defaults": cream background with a high-contrast serif and a terracotta accent, near-black paired with acid green, a generic broadsheet look with hairline rules and zero border-radius anywhere, card grids with drop shadows as the entire visual idea, purple-to-blue gradients, icon fonts. All immediately recognizable, all the kind of thing that shows up by default when nobody's pushed back on the first plausible answer.
Instead the palette came from the trade's own material world — steel, the different finishes stainless steel actually takes, weathering, the stone the region is built from. Before presenting the plan, the gut-check question was: would I have produced this exact thing for any craftsman's website? Where the answer was yes, that part got changed.
A palette that changed after the client saw it
The material-derived palette was the plan. It wasn't the final word. After seeing it, the client asked for a second option sampled directly from their own workshop's physical signage — navy and white, nothing to do with the design brief's steel-and-stone logic, just what's already painted on their building.
Both palettes shipped as a "Stil 1 / Stil 2" switcher on every page, and technically they're nothing more than two sets of values for the same CSS custom properties. No component's markup or component-level CSS differs between them — every card, button, and section just re-skins automatically depending on which set of variables is active. That's the payoff of building the design as tokens from the start: a real client preference that had nothing to do with the original plan still cost zero redesign work, just a second value for each variable and a toggle. It did push the nav script a little over its own self-imposed 3 KB budget — a deliberate, requested exception, not scope quietly creeping.
The image pipeline
Every photo that made it past the Phase 0 review went through a repeatable script: responsive widths at 480, 960, 1600, and 2400 pixels, WebP with a JPEG fallback, art-directed crops per placement rather than a centered default — cutting out the parked car or the empty sky instead of just letting the crop fall wherever. EXIF and GPS data stripped from everything, faces and plates cropped or blurred out. Target: no single image over 250 KB.
The real numbers came in under that: the largest image on the finished site is 245 KB, and the homepage's entire first load — HTML, CSS, JS, an icon sprite, all seven self-hosted font files, and the eager-loaded hero image — comes to about 305 KB, most of which is the one-time, then-cached cost of the fonts.
What building actually looked like
One CSS file, deliberately structured in the same order every time — tokens, then reset, then base, layout, components, utilities, media queries — so specificity fights don't creep in between a generic element selector and a component class fighting over the same padding. Icons and the section-divider motif as hand-written inline SVG, kept to one consistent stroke language rather than mixing icon sets.
The legal pages got built against the client's real address, with [TBD] markers everywhere only a lawyer or the client themselves could actually fill in — VAT ID, trade chamber registration, professional title — plus an explicit note that a lawyer needs to review both pages before anything goes live, regardless of how complete they look. Same discipline on the contact form: full working markup, a honeypot field, a consent checkbox linking to the privacy page, and the form's action left as a documented [TBD], because a static demo has nowhere to actually deliver a submission yet.
Two real bugs a browser caught that static checks missed
No headless browser was available in the environment this got built in, so the actual verification step was asking a real person to open it in a real browser — and it caught two bugs that every static check had missed. The header navigation disappeared completely at desktop widths: a <details>-plus-display: contents technique that looked reasonable on paper doesn't actually override the browser's native closed-state hiding. Fixed by going back to a plain, always-visible <nav> that JavaScript progressively enhances into a collapsible button-and-menu only below a certain width — visible with or without JS, at every width. Two photos on one interior page were also getting forced into a fixed aspect-ratio frame that didn't match their actual shape, leaving a strip of visible empty background; removing the forced ratio (and, while in there, fixing a related object-fit: cover bug that had been silently doing nothing everywhere it was used) let each photo show at its own true proportions.
That's the same principle now written into how the site talks about AI-assisted work on the Experiment page: AI drafts and speeds the work up, but nothing ships without an actual human review catching what static analysis alone doesn't.
The rest of the self-critique pass was quieter but just as real. The W3C validator caught an unencoded space inside an inline SVG favicon's data URI and a few skipped heading levels — all eleven pages now run clean with zero errors and zero warnings. Every text-and-background color pairing got checked by computing the actual WCAG relative-luminance contrast ratio rather than eyeballing it; every body-text pairing came out at 6:1 or better, well past the 4.5:1 minimum. And all 123 unique internal links and asset references across the site got crawled against a local server to confirm every one actually resolves.
What's still open
Most of the client's open questions from early in the build — exactly how their business name should appear, their real service radius, which phone numbers to list, how to mark the site clearly as a sample rather than the real thing — got resolved along the way. What's genuinely still open, and stays marked as such rather than quietly assumed: the legal-page fields waiting on a lawyer's review, a contact form with no backend yet since a static host can't send mail on its own, and a placeholder .example domain standing in until there's a real one to point at.
If you're weighing whether your own business's site is worth building properly, I'd like to hear about it.