DEV Community

Cover image for I vibe-coded a cassette deck for family memory — K7 Mémoire (Sanity Challenge Path 2)
Chaminade Dondah ADJOLOU
Chaminade Dondah ADJOLOU

Posted on

I vibe-coded a cassette deck for family memory — K7 Mémoire (Sanity Challenge Path 2)

Sanity Challenge Path Two Submission

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
}
Enter fullscreen mode Exit fullscreen mode

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:

  1. Create project + public production dataset in Sanity Manage.
  2. Set NEXT_PUBLIC_SANITY_PROJECT_ID / NEXT_PUBLIC_SANITY_DATASET (defaults already point at nh4vx26w / production).
  3. Editor write token only for seed — never committed.
  4. npm run seed → createOrReplace 4 narrators and 8 stories.
  5. 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

K7 Mémoire live deck

(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
LinkedIn 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 story in 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)