[ 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.
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
- Dragging is the animation. Slider input paints synchronously — no tween
fighting the thumb. The only tween is the model switch, and under
prefers-reduced-motionit snaps instead. The idle "cave light" drifting across the wheel is pure CSS and dies in the same media query, so reduced-motion users get a fully working instrument with zero idle movement. - Honest without JavaScript. The static SVG markup carries a real bloomy
week-3 state — sensible curves, correct readouts — so the no-JS page shows a truthful
chart, not an empty one. All reveal/intro states are gated behind a
.jsclass set synchronously in<head>. - The chart talks. The SVG
<title>is rewritten on every change ("rind 4.2 millimetres, cellar at 88% humidity…"), the slider carriesaria-valuetext("week 20 of 52"), and the tasting note is a polite live region. Temperature is dashed as well as coloured, so the two lines survive colour-blindness. - Straw needed a chaperone. The brand straw
#d4a53ais only about 1.9:1 against the cream background, so it is reserved for graphics; small text uses a per-theme--accent-text(7:1 on light, 8:1 on dark) and the two chart lines get their own darkened tokens in light mode. - Two cellars, one toggle. Light is the dairy at morning, dark is the aging
cellar — same tokens flipped on
:root[data-theme], persisted tolocalStorage("rennet-theme"), bootstrapped inline before first paint so there is no flash.
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