← Loop & Larder THE GUIDE · HOW IT WAS BUILT
Design showcase · build notes

Ink, grain, and a box that does the math.

Loop & Larder is a warm light-theme storefront for a fictional coffee roastery, built with Astro and exactly one photograph. The headline is Playfair Display; the human touches — the "fresh this week" script, the arrows, the roast meters — are SVG ink that draws itself on as you scroll; and the subscription builder is a small vanilla-JS island that keeps a running total honest in real time.

Freshly roasted coffee spilling from a kraft bag beside a filled cup and brass scoop on linen — the site's only photograph
THE ONLY RASTER IMAGE ON THE SITE · BATCH LL-118 · 3:2 · COMPRESSED JPEG

The idea

A specialty roastery sells two things at once: a product (this week's four single origins, each with an origin, an elevation, a process and a roast level) and a habit (a subscription you trust enough to leave running). So the site is an editorial magazine page and a commerce configurator wearing the same clothes. The voice is warm and specific — "Roasted Tuesday. At your door Thursday. Gone by Sunday." — and the design keeps a crisp editorial grid, then lets a hand break into it with ink: a circled note, a drawn arrow, a signature. The commerce patterns underneath are borrowed from shipped DTC apps (Blue Apron's live recurring total, Walmart's subscribe-vs-one-time cards, Cleo's "save 15%" framing) and dressed in the roastery's own hand.

The stack — why Astro

Signature #1 — ink that letters itself

The "fresh this week" script, the little arrows, the underlines and the roastery signature are all hand-written SVG paths. Each visible stroke carries pathLength="1", which normalizes its length to 1 so CSS can animate the draw without JavaScript ever measuring anything:

<path class="p" pathLength="1" d="M31 30 C25 25 17 28 17 39 L17 65"/>

.js .ink .p        { stroke-dasharray: 1; stroke-dashoffset: 1; }
.js .ink.is-in .p  { stroke-dashoffset: 0;
                     transition: stroke-dashoffset 1.6s var(--ease); }

A single IntersectionObserver adds .is-in when a drawing scrolls into view, and each letter's stroke draws in sequence — so "fresh" appears the way a hand would write it. The same observer fills the five-dot roast meters and runs the stat counters. The easing is the site's signature cubic-bezier(.2,.7,.15,1) — "the pour": a slow tip, then a smooth stream. Under prefers-reduced-motion the dashes are stripped and every stroke, dot and number simply arrives final.

Signature #2 — the subscription builder

The builder is three radio groups — coffee, grind, cadence — plus a subscribe/one-time toggle, with a dark summary card that recomputes on every change. The groups are real <input type="radio"> elements (visually hidden, fully keyboard-operable with arrow keys); the selected-card look and focus ring come from CSS :has(). All the logic is one function that reads the checked inputs and writes the summary:

function update() {
  const subscribe = checked('mode').value === 'subscribe';
  const base = parseFloat(checked('origin').dataset.price);
  const perBag = subscribe ? base * 0.85 : base;      // subscribe = 15% off
  const per = parseInt(checked('cadence').dataset.per, 10); // 4 / 2 / 1 per month

  if (subscribe) {
    total.textContent = money(perBag * per);          // live monthly total
    saveText.textContent = `${money(perBag)} a bag — you save ${money(base-perBag)}`;
  } else {
    total.textContent = money(perBag);                // one bag, ships once
    cadenceStep.classList.add('dim');                 // cadence not relevant
  }
}

Why it reads as trustworthy: shipped subscription apps never hide the math. The summary restates every choice in plain language and shows the per-bag price and the monthly total, so there's no surprise at a checkout that, in this demo, doesn't exist. The whole card lives inside an aria-live="polite" region, so screen-reader users hear the total change as they build.

Details that matter

Ship it on GitHub Pages

Astro builds static HTML, so Pages serves it straight from the repo — no Actions workflow needed. Three config lines do the work:

// astro.config.mjs
export default defineConfig({
  site: 'https://bswxyz.github.io',
  base: '/loop-and-larder',   // project-site path prefix
  outDir: './docs',           // Pages serves main + /docs
});

Every internal link and asset routes through import.meta.env.BASE_URL so nothing 404s under the /loop-and-larder/ prefix, a .nojekyll in public/ stops Pages from mangling the build, and deploying is:

npm run build                      # emits ./docs
git add -A && git commit -m "batch LL-118"
gh repo create bswxyz/loop-and-larder --public --source . --push
# then: Pages → main + /docs

Loop & Larder is a design-showcase concept — the coffees, farms, prices and numbers are fictional, and there is no cart, checkout or subscription backend. The repository README maps exactly what a production version would need.

← Back to the roastery