Build With Moenu buildwithmoenu.com
Case Study

Kaffa Espresso Bar — Live Client Site

Live

A neighborhood espresso bar's first real web presence — vanilla site, real deploy pipeline, HTTPS that renews itself.

Overview

Project framing

A neighborhood espresso bar with no web presence and no in-house technical capacity. The job was a real site, a real domain, and a deploy pipeline the owner could keep running without ever opening a terminal. Vanilla code. Boring infrastructure. HTTPS that renews itself.

Problem

Most small-business sites are either over-built (Shopify + Wix + booking add-ons for a shop with no online ordering) or under-built (a Squarespace template that goes stale the day after launch). For a coffee shop that mostly needs to show hours, location, and menu, and that doesn't want a monthly platform fee, the right answer is a tiny vanilla site on its own infrastructure.

Role

Designer, engineer, operator. One-month build, single client, hand-off-ready.

Why now

Web standards are good enough that vanilla HTML + CSS Grid + a small amount of JS produce a faster site than most SaaS templates. Let's Encrypt + Certbot's webroot ACME flow makes HTTPS truly hands-off. Together, those mean a shop can have a real online presence without paying a platform tax.

Topline

Constraints

  • No in-house technical capacity at the shop — every deploy primitive had to be operable by the client or trivially handed off.
  • Legacy subdomain hosted off another artist's portfolio, with email and external integrations still pointed at the old DNS — cutover had to be surgical.

Outcomes

  • Live at the custom domain, primary online presence for the shop — production-stable since Mar 9, 2026.
  • Zero-downtime HTTPS renewal since launch.
  • ~2.3 MB total page weight including 18 hero / menu / gallery photos.
Architecture

Push to live

Owner-grade deploy pipeline: every release is versioned, atomic, and rolls back in one symlink swap.

01
Step 1

Edit + commit

Content or markup change in the local repo; git commit on the main branch.

02
Step 2

Push to droplet

git push to the droplet's bare remote triggers a post-receive hook.

03
Step 3

Stage release

Hook writes the working tree into /var/www/releases/<timestamp>, leaving the previous releases untouched.

04
Step 4

Atomic cutover

Single symlink swap (/var/www/current → new release dir). Nginx serves the new content instantly; old releases stay around for rollback.

05
Step 5

Renew (in background)

Certbot's webroot ACME flow renews the Let's Encrypt cert without taking Nginx offline.

Rollback

5-release rollback window — point the symlink at a prior release dir, reload Nginx, done.

OutcomeZero-downtime deploys and zero-downtime cert renewals from a single shell command.

Architecture narrative

A single-page site — hero, about, menu, gallery, hours, location, contact — built with HTML5, CSS Grid, custom properties, vanilla JS. Mobile hamburger nav, smooth scroll, no framework tax. 18 WebP photos optimized to ~2.3 MB total page weight. Teal accent (#3FBFBF), Pacifico script logo. Deployment is a release-versioned pipeline: a bootstrap script that sets up the droplet (Nginx, deploy user, SSH config), a deploy script that pushes a new release dir with an atomic symlink cutover, and a 5-release rollback window. Let's Encrypt HTTPS via Certbot's webroot ACME flow, so cert renewal never takes the site down. The cutover from the legacy subdomain to the apex `.com` walled off MX / TXT / nameservers so email and external integrations on the old DNS kept working through the transition.

System shape

Static site, real deploy pipeline. No CMS, no platform fee, no platform lock-in.

Source

Git repo

Owner-edited HTML, CSS, JS, WebP images. No build step.

Deploy

Post-receive hook

Stages a new release dir, then swaps the /var/www/current symlink atomically.

5-release window

Old release directories are preserved; rollback is a single symlink swap.

Serve

Nginx

Serves the symlinked release. Apex + www + legacy subdomain all redirect to the canonical apex.

Let's Encrypt (webroot)

Cert renewal happens without taking the site down.

DNS

Apex cutover

Walled off MX / TXT / nameservers during cutover — email and external integrations on the old DNS kept working.

Webroot ACME, not standalone

Standalone takes port 80 to renew. Webroot lets Nginx keep serving traffic. That's why renewals are silent.

Surgical DNS work

The apex cutover only touched A / CNAME records. Email and third-party integrations on the legacy DNS were never paused.

Key Decisions
1 / 3
Pull off S3, rebuild as a git-driven droplet deploy

Drop the AWS S3 static-site setup and run the site on its own droplet with Nginx.

S3 is the easy answer for a static site. But I wanted Nginx-level redirect control (for the apex cutover), real release versioning, and the same deploy primitives I use across other client work. Once you want any of those, S3 stops being the easy answer.

More infrastructure to maintain than a managed static host. Worth it for the redirect control and the reusable deploy pipeline.
Webroot ACME, not standalone

Use Certbot's webroot challenge for HTTPS, not the standalone one.

Standalone ACME briefly takes port 80 to renew, which on a busy site means a tiny renewal-window outage. Webroot lets Nginx keep serving traffic during renewal.

Slightly more Nginx config to wire up. Worth it for zero-downtime renewals indefinitely.
Vanilla JS, no framework

Skip React / Vue / anything else; write the page in HTML + CSS + a small amount of JS.

The site is a coffee shop's hours, menu, and location. A framework is overhead with no payoff. Vanilla loads faster, deploys simpler, and is debuggable by anyone.

If the shop ever wants client-side state (online ordering, etc.), that's a different project. Fine — this one is hours, menu, location.
Struggles

Domain migration. Legacy domain was a subdomain of another artist's portfolio site. Client wanted to move to a real `.com` apex while keeping email and external integrations pointed at the old DNS untouched.

Walled off MX / TXT / nameservers explicitly. Wrote `finalize_primary_domain_cutover.sh` to handle `www` → apex and old-subdomain → apex redirects. Kept the legacy cert working through the cutover. No outage, no broken email, no third-party integration broke.
Learnings
The boring choices are the right ones for client work — they age well and they hand off cleanly.
DNS cutovers fail when they touch records they didn't need to touch. Be surgical.
A 'small' client site is still worth a real deploy pipeline; the alternative is editing files on the server at 11pm.
Demo