A one-page festa-junina (Brazilian June festival) themed RSVP website for Otto's 1st birthday. Mobile-first, all copy in Brazilian Portuguese (caipirΓͺs), short-lived (it lives for a few weeks around the event).
π Live: ottok.com.br (also www.ottok.com.br)
Event: Saturday 2026-06-27, 12:00β18:00, Arena Bombinhas β Rua AraΓ§Γ‘, 551, SertΓ£ozinho.
Built end-to-end with Claude Code as a platform β project memory + skill-invocation conventions in
CLAUDE.md;.claude/settings.jsonwires thefrontend-design/cloudflare/playwrightplugins; visual self-check viascripts/shots.mjs(Playwright + system Chromium) so visual changes get verified during the build loop.
A single, full-screen, scroll-driven invitation that travels through a day: the sky animates from noon blue β golden hour β dusk β bonfire night as you scroll, with an illustrated Otto in every scene, varied festa separators, and a group RSVP form that writes to a database β all on Cloudflare's free tier ($0/month).
- Dynamic sky (
Sky.jsx) β dayβnight gradient, a rotating warm sunburst that sets, drifting Simpsons-style clouds (random per session), a full moon + twinkling stars at the bonfire end, and a WebGL ember particle field (Embers.jsx, Three.js/R3F, lazy-loaded and code-split). - Full-screen scenes with gentle scroll-snap; 2-column on desktop, compact on mobile.
- Group RSVP β a responsΓ‘vel confirms for the whole household (add/remove acompanhantes); one row
per person sharing a
group_id. Honeypot anti-spam (Turnstile wired but disabled). Confetti on success. - Rustic touches β wood-textured plaques/frames, garland separators (lanterns, balloons, corn, candy apples) built from single generated elements tiled/hung via CSS.
- Browser chrome follows the sky β
theme-color+ body background animate with scroll (nice on Android Chrome). - Social card β a generated Open Graph image (
public/og.jpg) with Otto + balloons + wood frame. - Countdown, add-to-calendar (
.ics+ Google), Google Maps + Waze, all respectingprefers-reduced-motion.
- Frontend: Vite + React 19 + Tailwind CSS v4,
motion(idle/reveal helpers),three+@react-three/fiber(embers),canvas-confetti. - Hosting: Cloudflare Pages.
- Backend: Cloudflare Pages Function (
POST /api/rsvp). - Database: Cloudflare D1 (SQLite), table
rsvps. - Anti-spam: hidden honeypot. (Cloudflare Turnstile is wired but currently disabled.)
- Package manager: pnpm.
.
βββ index.html # Vite entry; <head> OG/Twitter meta, fonts, theme-color
βββ src/
β βββ App.jsx # composes Sky + the full-screen scenes + FloatingCTA
β βββ index.css # Tailwind v4 @theme tokens + CSS keyframes (sky, garlands, reveal)
β βββ components/ # Sky, Sun, CloudField, Embers, Scene, Hero, Detalhes, Programacao,
β β # Mapa, Rsvp, Footer, Divider, WoodSign, WoodFrame, Bunting,
β β # Countdown, Button, OttoImage, FloatingCTA, Turnstile, ErrorBoundary
β βββ data/
β β βββ event.js # event facts + caipirΓͺs copy (single source of truth)
β β βββ clouds.js # Simpsons-style cloud SVG silhouettes
β βββ lib/
β βββ calendar.js # .ics + Google Calendar links
β βββ links.js # Google Maps / Waze links
β βββ confetti.js # festa-colored confetti burst
βββ functions/
β βββ api/rsvp.js # Pages Function: validate β honeypot β Turnstile β D1 batch insert
βββ public/
β βββ img/ # generated WebP assets (Otto scenes, wood, separators) + og.jpg
βββ scripts/
β βββ make-og.mjs # builds public/og.jpg (pnpm og)
β βββ rsvps.mjs # prints the RSVP list from D1 (pnpm rsvps)
β βββ shots.mjs # Playwright screenshots for visual self-checks
βββ migrations/
β βββ 0001_add_group_id.sql
βββ docs/ # SPEC.md, RSVP-HANDOFF.md, IMAGES.md, IMAGE-PROMPTS.md, DEPLOY.md
βββ schema.sql # D1 table definition
βββ wrangler.jsonc # Pages config + D1 binding
βββ CLAUDE.md # project memory / build decisions (read this!)
βββ package.json
Prerequisites: Node 22+ and pnpm.
pnpm installpnpm dev # http://localhost:5173You can see and exercise the whole UI here, but submitting the RSVP errors β the /api/rsvp
Function and the database don't run under Vite.
# one-time: create the local table
pnpm exec wrangler d1 execute rsvp-db --local --file ./schema.sql
pnpm build
pnpm pages:dev # http://localhost:8788 (serves dist/ + functions/ + a local D1)Read the local rows:
pnpm rsvps --local| Command | What it does |
|---|---|
pnpm dev |
Vite dev server (frontend only). |
pnpm build |
Production build to dist/. |
pnpm preview |
Preview the built dist/. |
pnpm pages:dev |
wrangler pages dev β full stack (Functions + local D1). Build first. |
pnpm og |
Regenerate the social card public/og.jpg (resvg + sharp). |
pnpm rsvps |
Pretty-print the RSVP list grouped by household (--local for the dev DB). |
node scripts/shots.mjs [url] screenshots the running dev/preview at several scroll fractions using
the system Chromium via Playwright β used to verify visual changes.
POST /api/rsvp accepts JSON:
It validates, drops honeypot hits, then writes one row per person (companions are attending = 1),
all sharing a group_id (crypto.randomUUID(), NULL when solo), via DB.batch. Returns
{ ok, count }. The old single-person payload still works.
CREATE TABLE rsvps (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
attending INTEGER NOT NULL, -- 1 = vem, 0 = nΓ£o vem
dietary TEXT,
group_id TEXT, -- shared per household submission; NULL = solo
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);Otto's scenes are generated externally on the Picsart web app (the gen-ai API rejects real-child
face references; the web app allows them) from the prompts in docs/IMAGES.md, using
Otto's real photo as the face reference. Hero/footer are transparent cutouts composited over the
live sky; the 4 programaΓ§Γ£o scenes are framed cards. Decorative assets (wood texture, separator
elements) were generated via the Picsart gen-ai CLI and keyed/optimized with sharp. Everything
is committed as optimized WebP under public/img/.
wrangler.jsonc already points at the production resources (Pages project arraia-do-otto, D1
rsvp-db with its database_id), so on a fresh machine you only need to authenticate wrangler
with the Cloudflare account that owns them:
pnpm install
pnpm exec wrangler login # opens the browser β authorize the account (rkuesters@gmail.com)
pnpm exec wrangler whoami # confirm the email + account idAfter that, deploy and the reports work as usual:
pnpm build && pnpm exec wrangler pages deploy dist --project-name arraia-do-otto
pnpm rsvps # read production RSVPsNotes:
- Secrets are not in git. Nothing is secret while Turnstile is disabled. If you re-enable it, put
the site key in
.env(.env.exampleβ.env, baked into the build) and the secret key in.dev.varsfor local dev / a Pages secret (wrangler pages secret put TURNSTILE_SECRET_KEY) for prod β and remember both gates must move together (a secret with no widget 403s every submission). - The local D1 lives under
.wrangler/(gitignored), per machine. For local full-stack dev, recreate it:pnpm exec wrangler d1 execute rsvp-db --local --file ./schema.sql(then anymigrations/*.sql). - For CI/headless (no browser for OAuth), set a
CLOUDFLARE_API_TOKENenv var with Pages (edit) + D1 (edit) permissions instead ofwrangler login.
Cloudflare Pages, project arraia-do-otto, served on the custom domain ottok.com.br.
pnpm build
pnpm exec wrangler pages deploy dist --project-name arraia-do-ottoFull walkthrough (D1 create, remote schema, Turnstile, custom domain): docs/DEPLOY.md.
Read RSVPs in production with pnpm rsvps.
This site was designed and built collaboratively in Claude Code. Skills/tools that shaped it:
frontend-designβ the distinctive, non-generic visual direction.cloudflare/wrangler/cloudflare-email-serviceβ Pages + D1 + Functions, and exploring email notifications.gen-ai-use(Picsart gen-ai CLI) β generated the wood texture and the separator elements.humanizerβ available for the guest-facing caipirΓͺs copy (kept manual by the owner's call).- Playwright + system Chromium β screenshot-driven visual verification (
scripts/shots.mjs).
See CLAUDE.md for build decisions and a couple of gotchas (notably: the scroll-linked sky
is driven by a plain rAF state, not motion MotionValues, because scroll-bound opacity didn't
flush to the DOM in this motion v12 + React 19 setup).
- CSV export of the guest list for the caterer (headcount) β closer to the event.
- Turnstile anti-spam β wired but disabled (2026-06-03): the managed widget blocked legit guests on Safari / Samsung Internet. Honeypot stays active. Revisit only with a non-blocking config (e.g. invisible mode) if spam becomes a problem.
- Email-on-RSVP notification β deferred; Pages Functions can't use the
send_emailbinding, so it'd need a companion Worker or a transactional API. - Optional
www β apexredirect for a canonical URL. - Refresh the OG art / swap in newer Otto renders if desired.
Feito com muuuuito amΓ΄ pela mamΓ£e e pelo papai do Otto β€οΈ
{ "name": "JoΓ£o", "attending": 1, // 1 = coming, 0 = not "dietary": "sem glΓΊten", "companions": [{ "name": "Maria", "dietary": "" }], // only used when attending = 1 "fax": "", // honeypot β must stay empty "turnstileToken": "..." // verified only if TURNSTILE_SECRET_KEY is set }