← Back to Rennet

[ Parable build guide ]

How Rennet was built

A farmhouse creamery where the product is mostly time. The signature is an aging-cave data viz: an SVG wheel of cheese that matures as you drag a week slider — rind thickening and darkening, a humidity×temperature chart tracking, tasting notes turning over — with three rind models to switch between. Vanilla JS, no chart library.

Astro 5 · static outputinline SVG onlyCormorant / Karla / Space Monolight + darkreduced-motion aware

The idea

Every creamery site says "patience" and shows a photo of a cellar. Rennet demonstrates it: the one thing an affineur actually controls is time × humidity × temperature, so that trio became the interface. You drag the weeks and watch the wheel take on the cave — the same slow transaction the copy describes, made manipulable. The slider is the brand.

The stack

Astro 5 with zero client framework — the page ships as static HTML plus one inline vanilla script. Content (cheeses, stockists) lives as arrays in the page frontmatter and renders at build time; the cave gets its three rind models directly in the script. Type is Cormorant (a serif with wheel-stamp elegance), Karla for body, Space Mono for the ledger voice — weeks, millimetres, humidity — that runs through the whole site.

Signature technique — the aging cave

The wheel is a cross-section built from three nested rounded rectangles: rind, creamline, core. "Aging" is just insetting the inner rectangles and re-mixing their colours. Each rind style is a small model — functions of the week w — and one snapshot function turns (model, week) into everything the SVG needs:

const snapshot = (mk, w) => {
  const m = MODELS[mk], f = w / m.weeks;
  return {
    rindPx:  m.rindMm(w)  * PX_PER_MM,   // rind thickens with age
    creamPx: m.creamMm(w) * PX_PER_MM,   // bloomy: creamline advances
    rc: mix(hexRgb(m.rindCol[0]), hexRgb(m.rindCol[1]), f),  // rind darkens
    oc: mix(hexRgb(m.coreCol[0]), hexRgb(m.coreCol[1]), f),  // paste deepens
    mop: m.mottle(w),                    // crust speckle density
  };
};

// dragging IS the animation — paint directly on input
slider.addEventListener('input', () => {
  week = +slider.value;
  paintWheel(snapshot(modelKey, week));
  paintMeta();   // readouts, chart playhead, tasting note
});

The humidity and temperature curves are the same models plotted: 48 samples of m.rh(w) and m.temp(w) concatenated into two SVG path strings, with a dashed playhead line at the slider's week. Switching rind styles redraws the curves, resets the slider's range (bloomy peaks at 8 weeks, natural runs to 52) and tweens the wheel to its new state over half a second. The mottled crust is a dot pattern masked to the rind ring, so it thickens with the rind for free.

Details that matter

The order form is a demo — it validates and confirms in place but sends nothing. Wire it to your own endpoint (Formspree, a serverless function, an email service) before taking real orders. Raw-milk cheese also has real rules — check yours.

Ship it on GitHub Pages

Astro builds static HTML, so Pages serves it straight from the repo. Two config lines do the work:

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

Every internal link goes through import.meta.env.BASE_URL so nothing 404s under the /rennet/ prefix, and a .nojekyll in public/ keeps Pages from mangling the output. Then:

npm run build                      # emits ./docs
git add -A && git commit -m "wheel no. 214"
gh repo create bswxyz/rennet --public --source . --push
gh api --method POST /repos/bswxyz/rennet/pages \
  -f 'source[branch]=main' -f 'source[path]=/docs'
← Back to Rennet