---
title: Giving my blog a voice
date: '2026-01-14T14:25:50.000Z'
author: Rich Tabor
summary: Explore how to add audio to your blog with 5 simple steps. Enhance your content and connect with your audience. See the guide.
tags:
  - AI
published: true
type: blog
url: /listen
canonical: https://richtabor.com/listen/
id: 7402
---

I’ve found myself choosing audio more often, like in last week’s experiment, [interviews.now](/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](https://richtabor.com/listen), read in my voice.

## How it works

I have an [experimental version](https://richtabor.com/listen) 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](https://elevenlabs.io)) reads the post to you. The audio gets cached in [Vercel Blob](https://vercel.com/docs/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.

```
//ListenButton.tsx
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](https://elevenlabs.io/blog/introducing-turbo-v25%20) model with [SSML](https://learn.microsoft.com/en-us/azure/ai-services/speech-service/speech-synthesis-markup) (Speech Synthesis Markup Language), which lets me control _how_ text gets spoken.

Like adding [pauses](https://elevenlabs.io/docs/overview/capabilities/text-to-speech/best-practices#pauses) around headings:

```
//text-extractor.ts
parts.push('<break time="0.75s"/>');
parts.push(decodeHtmlEntities($el.text().trim()));
parts.push('<break time="0.5s"/>');
```

Or fixing pronunciation with a phoneme tag with the IPA spelling, like here for my last name:

```
//text-extractor.ts
`<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](https://elevenlabs.io/app/voice-lab/share/83698de998ea1df8370d5dea6b6e8e13e1e44e2073009c7fa6ff133bca8656c6/UL7YtIO1odGIVqCrjI0U) 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.

```
//richtabor-helper.php
$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](https://code.claude.com/docs/en/overview). A bit of planning, a handful of follow-ups, some detailed design tweaks, and a circle-about to clean up loose ends.

[Listen](https://richtabor.com/listen) to any of my posts, and [let me know](https://x.com/richtabor) what you think.