Giving my blog a voice
I’ve found myself choosing audio more often, like in last week’s experiment, interviews.now. It’s nice, especially when I’m walking, driving, or just stepping away from a screen.
I wanted to explore adding audio to my blog in a way that stays simple and doesn’t add any friction to how I publish—at all.
So yea, you can listen to my posts, read in my voice.
How it works
I have an experimental version of my blog running on a headless WordPress setup where WP handles the writing and a Next.js frontend handles the rest.
Every post on the blog now has a small “Listen” button.
Click it, and a voice clone of me (via ElevenLabs) reads the post to you. The audio gets cached in Vercel Blob, so it’s instant after the first generation.
The flow is simple: click the button, it checks if audio exists, plays it if it does, generates it if it doesn’t.
const checkResponse = await fetch(`/api/generate-audio?slug=${slug}`);
const checkData = await checkResponse.json();
if (checkData.exists && checkData.url) {
setAudioUrl(checkData.url);
return;
}
// Generate if it doesn't exist yet
setAudioState("generating");
const generateResponse = await fetch("/api/generate-audio", {
method: "POST",
body: JSON.stringify({ slug }),
});Before sending the text to ElevenLabs, I strip out everything that doesn’t make sense to read aloud: code blocks, embedded videos, image captions, and the like.
The rest gets processed using the ElevenLabs Turbo v2.5 model with SSML (Speech Synthesis Markup Language), which lets me control how text gets spoken.
Like adding pauses around headings:
parts.push('<break time="0.75s">');
parts.push(decodeHtmlEntities($el.text().trim()));
parts.push('<break time="0.5s">');</break></break>Or fixing pronunciation with a phoneme tag with the IPA spelling, like here for my last name:
`<phoneme alphabet="ipa" ph="ˈteɪbɝ">Tabor</phoneme>`I’ve capped off the text at 5,000 characters and truncate at a sentence boundary if needed. If a post goes over that, the narration ends with “This post continues on my blog at
I made a studio-quality voice clone so that my posts are narrated by a version of me that never actually recorded the words. True, it’s a little uncanny, but it sounds natural most of the time. Even my wife, Jesse, thinks so.
Publishing
When a post is published, or updated, in WordPress, a utility plugin hooks in and invalidates the cached audio.
This way, the next time someone goes to listen to a post, it’s grabbing the latest and generating, then caching, that version for everyone else.
$response = wp_remote_post($api_url . '/api/invalidate-audio', [
'body' => json_encode([
'slug' => $post->post_name,
'secret' => REVALIDATE_SECRET
])
]);I’m pleased with how well it turned out, and how quickly I was able to get it to production using Claude Code. A bit of planning, a handful of follow-ups, some detailed design tweaks, and a circle-about to clean up loose ends.
Listen to any of my posts, and let me know what you think.