Mai is a portfolio and agency site I am building under Frisson. Like Racket Tuning, it is not released yet, but the setup is interesting enough to preview now.
The site shows case studies: a home page with a work grid, individual project
pages at /works/[slug] with next-project navigation, and an about page. Prospective
clients browse it; the studio edits it.
The CMS lives inside the site
The front end is Astro 5, server-rendered on Vercel, with React 19 islands for the interactive parts. The twist is that Sanity Studio is not a separate deployment: it is embedded at /studio via @sanity/astro, so editing happens on the same domain the site runs on. One repo, one deploy, no CORS dance.
The Studio itself is customized: a bespoke dashboard and docs tool, a grid editor for the home page, and a curated structure with Site Settings, About, and Home Grid as singletons next to the Projects list. Sanity's Vision GROQ playground is enabled in development only.
Blocks all the way down
Pages are assembled from Sanity content blocks (intro, text, text-plus-image, image, video, project data, logo wall) mapped to Astro components by a single sections renderer. Adding a block type is a documented three-step pattern: write the schema, build the component, register it in the renderer. Rich text goes through Portable Text, images through Sanity's urlFor() pipeline.
Motion and constraints
GSAP drives split-title and entry animations, with Three.js available for 3D work, all kept SSR-safe. The tooling is strict on purpose: ESLint enforces kebab-case filenames, Stylelint bans hex and named colors in favor of custom properties and requires alphabetical property order, and commits go through Commitlint. One fun wrinkle: astro.config.mjs runs before Vite loads env files, so the config ships a tiny hand-rolled .env loader.
The site has been through a steady stream of iterations: animations, styling, schema fixes, a move to fully server-rendered pages, and is getting close. When it launches, this post gets a link. Coming soon.