I vibe-coded a cassette deck for family memory — K7 Mémoire (Sanity Challenge Path 2)
Author: Chaminade (Dondah Chamiande Adjolou) · Lomé, Togo
Challenge: Sanity Challenge Path 2 — Vibe-code something strange (Sep 18–Oct 4, 2026 PDT)
Tag: #sanitychallenge
Live deck: https://k7-memoire.vercel.app
Embedded Studio: https://k7-memoire.vercel.app/studio
Sanity project ID: nh4vx26w · Dataset: production
Why something strange
In Lomé, family history often travels by voice before it travels by page. I did not want another blog layout for that. Path 2 of the Sanity Challenge asks for something strange — so I vibe-coded K7 Mémoire: a Next.js cassette / radio UI that treats Sanity documents as tapes you can tune, filter by mood, and “play.”
The copy is French, Lomé-flavored. The chrome is a deck: FM dial, mood filters, spinning reels, counter, VU meters, rolling transcript, and a kente stripe. No article list. No card grid. Stories are bandes; narrators are voix.
This sits next to earlier work I care about — like a family-tree project for Hacktoberfest — but K7 Mémoire is its own object: oral memory as an instrument, not a CMS demo with a pretty skin.
The product prompt (and why process matters)
Judges for this challenge weigh process as much as the result. I kept a journal in the repo: PROMPT_LOG.md. The brief I gave the agent was essentially one product prompt:
An oral archive for Lomé, stored in Sanity, played by a cassette-deck UI, with Studio, seed stories, README, and this log.
From there, the log records what landed and what broke. Scaffolding with create-next-app (Next.js App Router, TypeScript, Tailwind) into a subdirectory, then moving to the repo root, was more reliable than aiming the CLI at the root. Studio landed at /studio with next-sanity, Sanity v6, Vision, and a custom desk (Bandes / Voix). One GROQ string is shared by the live client and by groq-js over the seed documents — tests assert eight tapes and resolved narrator names.
Failures were part of the craft: no Sanity credentials in the first environment (so the deck shipped a labeled demo mode using the same query, not a second content model); Vitest peer friction (switched to tsx --test); a Turbopack production build that choked on swr’s react-server export until swr was aliased in next.config.ts; Studio pulled into the RSC graph until the config lived behind a client boundary.
Left undone on purpose in that early pass: no audio files on demo tapes (audioUrl optional — Play falls back to the transcript), and no Cursor branding in the public brief.
That honesty is the submission as much as the UI.
Two schemas: tape and voice
Content is not files on the host. Stories stay in Sanity. The model is small on purpose:
| Document | Studio title | Role |
|---|---|---|
story |
Bande | A tape: title, slug, narrator ref, place, era, transcript (portable text), optional audioUrl, tags, mood |
narrator |
Narrateur | A voice: name, short bio (≤280), optional photo with hotspot |
The narrator’s name lives on the narrator document. The story only holds a reference. That keeps voices reusable across bandes and keeps the deck’s display join honest: narrator->name in GROQ, not a duplicated string on every tape.
Mood is a required string from a shared list (the filters on the dial). Era is approximate text (e.g. « vers 1987 ») and is what we sort on. Transcript is required portable text so the machine always has something to roll when there is no audio file.
One GROQ string, two sources
Whether the deck reads the Content Lake or the local seed drawer, the projection is the same:
*[_type == "story"] | order(era asc) {
_id,
title,
"slug": slug.current,
place,
era,
mood,
tags,
audioUrl,
transcript,
"narratorName": narrator->name,
"narratorBio": narrator->bio,
"narratorPhoto": narrator->photo.asset->url
}
That single string is the contract between Studio editors and the strange front end. An empty lake shows an open tray, not a crash. A failed query shows the error and no tapes. Demo mode is labeled — same query shape, seed documents in src/sanity/seed/documents.ts.
Studio at /studio, then the Lake
Studio is embedded in the Next app at /studio (Bandes / Voix desk + Vision). The path from vibe-coded demo to a real Content Lake was deliberate:
- Create project + public
productiondataset in Sanity Manage. - Set
NEXT_PUBLIC_SANITY_PROJECT_ID/NEXT_PUBLIC_SANITY_DATASET(defaults already point atnh4vx26w/production). - Editor write token only for seed — never committed.
-
npm run seed→createOrReplace4 narrators and 8 stories. - Add CORS origins (local +
https://k7-memoire.vercel.app), deploy, open Studio.
Public project ID and dataset are intentional: the deck can read without a token (watch Sanity’s public ACL: path("*") only matches IDs without dots — our seed uses hyphen IDs like story-radio-agoe). Write stays server-side for seed only.
What “strange” looks like on screen
(Screenshots: seeded deck on live — open the app for the full FM dial + kente chrome.)
You do not scroll a feed. You tune. Mood filters narrow the drawer. Play spins the reels and advances a counter while the transcript rolls — or audio plays when audioUrl is set. The on-screen service manual even prints the GROQ, so the “machine” explains how it listens to the Lake.
Stack in short: Next.js (App Router) + TypeScript + Tailwind + Sanity Studio (next-sanity, Sanity v6) on Vercel.
Links for judges and readers
| What | Where |
|---|---|
| Live app | k7-memoire.vercel.app |
| Studio | k7-memoire.vercel.app/studio |
| Sanity project ID | nh4vx26w |
| Dataset | production |
| Seeded content | 4 narrators · 8 stories |
| Origin source of truth | cursor.com/codebase/donchaminade/k7-memoire (private) |
| Process log |
PROMPT_LOG.md in that repo |
| GitHub | Donchaminade |
| chaminadeadjolou |
What I’d vibe next
- Real family recordings behind optional
audioUrl(consent and quiet UX first). - More Lomé places and eras as the Lake grows — still one GROQ contract.
- A small “donate a tape” flow that creates a draft
storyin Studio without breaking the deck metaphor. - Vision-assisted queries from the service manual for editors learning GROQ beside the reels.
Strange does not mean unserious. A cassette deck is a joke until it is how your aunt’s voice finds the next generation. Sanity’s schemas, Studio, and Content Lake made that joke into something I can seed, ship, and keep editing from Lomé.
Thanks to the Sanity Challenge for Path 2 — and to everyone who still presses Play on memory.
#sanitychallenge · nextjs · sanity · africa
Top comments (0)