
I wanted a way to listen to my posts while driving, without recording myself reading every single one. So the first version of this is fully automated: write the post, run a script, get an mp3 read by an AI voice. Here's how it works.
The obvious way to automate this is a Sanity Function that fires on publish, or a custom Studio action that calls an API route. I skipped both. A Sanity Function means setting up Blueprints and deploying a serverless function just to call an API. A Studio action means a new authenticated API route on the Next.js app, plus a secret shared between Studio and the app. Both are more infrastructure than a personal blog needs โ especially since the plan is to swap AI narration for real recordings eventually anyway.
So it's a plain Node script: write the post in the Studio, run `pnpm generate-audio <slug>`, done. No deploy, no secrets to share across apps, no server to keep running.
The post schema got three new fields: `audio` (a file field, audio only), and two hidden fields โ `audioSource` (`ai` or `manual`) and `audioSourceHash`. The hash is a fingerprint of the post body text. If it matches what's already stored, the script skips regenerating audio that hasn't gone stale. The source flag exists so a manually uploaded recording is never silently overwritten by a re-run of the AI script โ once `audioSource` is `manual`, the script refuses to touch it unless you pass `--force`.
Sanity stores the post body as Portable Text โ an array of typed blocks, not a plain string. Text-to-speech needs a string, so the script filters the array down to `block`-type entries (skipping inline images) and joins each block's `children[].text` into paragraphs.
That plain text gets POSTed to ElevenLabs' text-to-speech endpoint with an `xi-api-key` header and a voice ID, using the `eleven_multilingual_v2` model. The response body is the raw mp3 bytes.
The mp3 buffer gets uploaded as a Sanity asset, then a patch sets the post's `audio` field to reference it, sets `audioSource` to `ai`, and stores the new hash โ so the next run knows this post is up to date.
After drafting a post in the Studio: `pnpm generate-audio my-post-slug`. Edit the post later and run it again โ it only regenerates if the text actually changed, or if you pass `--force`.
AI narration is the placeholder, not the destination. The plan is to record real audio myself for posts that matter, and the schema already supports that without any code changes โ just upload a file in the Studio and set the source to manual.