Dolci Eoliani | WaretaGarasuSkip to main content
WaretaGarasu
Language
Theme
Glass
Build notes

Dolci Eoliani

React SPA with Tailwind v4, sharp image pipeline, and WhatsApp ordering for an Aeolian pasticceria.

2026Client workReact 19 · Vite · Tailwind v4 · TypeScriptMay 2026dolci-eoliani.it ↗

Brief

Dolci Eoliani is an artisan pasticceria on Lipari producing traditional Aeolian confections: nacatuli, spicchitedda, sesamini and malvasia pastries, made with recipes handed down across three generations. The client came with a concrete brief: not a vague ask, but a product they wanted online, a Facebook presence they wanted to formalise, and a clear desire to take nationwide shipping orders without running a server.

The site has one job. Not three, not five. One: get a visitor from landing to WhatsApp order in as few decisions as possible. Everything that didn't serve that path was cut before a single component was written.

Stack

React 19 with TypeScript and Vite, styled via Tailwind v4's @theme block in CSS (no tailwind.config.js: token names like --color-mandorla become utility classes automatically). Two typefaces: Playfair Display for headings, Inter for body, all four weights self-hosted via @fontsource and bundled by Vite into content-hashed woff2 files. No CDN request for fonts. No CDN request for anything.

The build pipeline is intentionally layered: sitemap generation, TypeScript compile, Vite build, then a Playwright-based prerender step that spins up a preview server, opens Chromium, and captures the fully rendered HTML for / and /informativa-privacy. This gives crawlers a real DOM without a separate SSR code path. On Cloudflare CI, the prerender step is skipped because Chromium dependencies aren't available there.

The form that isn't a form

The contact form has no action attribute, no HTTP POST, and no backend endpoint. On submit it calls window.open() with a pre-filled wa.me URL. It is a WhatsApp message composer wearing the clothes of a contact form.

// src/utils/whatsapp.ts
function buildOrderWhatsAppMessage(productName: string) {
  return `Ciao, vorrei ordinare ${productName}.\n\nVorrei informazioni sulla spedizione.`
}

function buildContactWhatsAppMessage(name: string, message: string) {
  return `Ciao, vorrei informazioni da Dolci Eoliani.\n\nNome: ${name}\n\nMessaggio:\n${message}`
}

Every product modal has a full-width 'Ordina su WhatsApp' CTA that pre-fills the product name and a shipping inquiry. The primary CTA at the top of the contact section is intentionally blank: it opens WhatsApp cold for general conversations. No confirmation email, no database record, no server-side processing of any kind.

This is the right architecture for this client. The business closes orders conversationally on WhatsApp: a form sending to email would just create a second inbox to manage. No backend means nothing to host, nothing to secure, and nothing to break at 2am.

Performance

A build-time script using sharp produces WebP and JPEG at three widths for every product image and both section backgrounds, served via picture with srcset. The first three product cards above the fold get loading eager and fetchpriority high; the rest are lazy. Four font files via @fontsource: all woff2, Latin only, bundled by Vite. Zero network calls to Google Fonts or any CDN.

JS bundle: 233 KB uncompressed app chunk, 48 KB React vendor chunk with a separate cache lifetime, two lazy route chunks under 3 KB each. CSS: 55 KB. The hero grain texture is a CSS background-image using an inline SVG data URI with an feTurbulence filter: no image file, no network request, generated entirely by the browser's SVG renderer.

The FAQ accordion

The FAQ expands and collapses without measuring height in JavaScript, without max-height hacks, and without layout shift. The outer wrapper transitions grid-template-rows from 0fr to 1fr. An inner div has overflow: hidden. No height measurement, no clipping artefact, clean degradation in older browsers.

.faq-answer {
    display: grid;
    grid-template-rows: 0fr;
    transition: grid-template-rows 300ms ease;
}

.faq-answer--open {
    grid-template-rows: 1fr;
}

/* inner div must have overflow: hidden */
.faq-answer > div {
    overflow: hidden;
}

Playwright as the SSG

Most projects that need prerendered HTML reach for Next.js, Astro, or a dedicated static site generator. Here Playwright acts as the build-time SSG. A script spins up vite preview as a subprocess, opens Chromium, navigates to each route, waits for the main content to appear, captures page.content(), and writes static HTML to dist/. The output is exactly what a real browser produces, all React side effects included, without maintaining a separate server rendering code path. The tradeoff: a heavier build dependency and incompatibility with Cloudflare's CI environment.

Status

Live at dolci-eoliani.it. One commit: 'First release'. The dist/ directory is committed to the repo alongside source, which is deliberate but will create noise on every future build commit. Pending: the privacy policy legal text carries an explicit disclaimer in copy.ts and needs a lawyer's review. The CSP in _headers is Report-Only with no report-to endpoint. Lighthouse has not been automated.

If I could do it over

I would not commit dist/ to the repo. Setting up Cloudflare Pages with a build command takes ten minutes and eliminates the noise from build commits. AGENTS.md shipped with stale path references (app/src/**, docs/cloudflare/) that don't match the actual project structure: a five-minute fix I should have caught before the first commit. And I would have the privacy legal copy reviewed by a lawyer before launch, not flag it as a TODO in source code.