❯ Web · Creative · 2026
BrewCraft Coffee
A scroll-storytelling marketing site giving a local coffee brand its first online presence, with inquiry-based ordering via contact form, WhatsApp, and phone.
The problem
A local coffee roaster had no online presence. No site, no menu, no way to order except a phone number passed by word of mouth. The story of the brand (small batch, local, worth the detour) had nowhere to live, and every menu change meant asking someone technical. They needed a shop window that could take inquiries, and they needed to edit it themselves after handover.
The approach
I built the site in Astro as a static export with strict TypeScript and
Tailwind v4 CSS-first @theme tokens, one theming system with light and dark
pairs. Motion runs off a single scroll driver: Lenis smoothing feeds GSAP
ScrollTrigger, which drives 0.3/0.6/1.0 parallax layers and ~150vh pinned
story sections. Reduced motion is a first-class path, not a fallback. Content
lives in Sanity with an embedded Studio and an idempotent seed script, so the
owner edits products, story, and photos without a developer. Inquiries go
through Web3Forms with confirmation states, WhatsApp deep links, click-to-call,
and availability badges.
Architecture
- Brand Owner
- Sanity CMS
- Astro Static Site
- GSAP + Lenis Driver
- Web3Forms
FLOWS
- Brand Owneredits via StudioSanity CMS
- Astro Static SiteGROQ queriesSanity CMS
- Astro Static Sitescroll progressGSAP + Lenis Driver
- Astro Static Siteinquiry submitWeb3Forms
The site is generated at build time: Astro queries Sanity over GROQ, renders statically, and ships to Vercel. There is no client framework for the content path; the interactivity is the scroll driver and the form states, both small. The whole page fits under a 1.5MB payload budget with WebP srcset imagery, and the scroll work holds 60fps. Vitest covers the unit logic; Playwright plus axe-core covers E2E and accessibility.
Key decisions
One motion source
Lenis owns smoothing, ScrollTrigger owns positions. Two libraries, one pipeline, no fighting over scroll events.
Sanity over hardcoded content
The owner edits copy and photos. The alternative was a developer dependency for every menu change.
Static-first
No SSR for the content path. Cheaper to run, faster to load, easier to reason about.
Performance as a gate
The 1.5MB payload and 60fps targets were budgets the build had to respect, which forced real decisions about imagery.
What I'd do differently
Accessibility testing came later than it should have. axe-core found focus and contrast issues that were cheap to fix once surfaced but would have been free to avoid, so I would run it from the first component. The form states also took more iteration than expected; I would spec the confirmation and error paths before writing any markup.