One of the things I wanted from the start with The Digital Down was to make it easy for people who prefer listening over reading. Smart but lazy, as I like to say. Here is how I set up automated audio narration for every post on this Hugo site.
The Goal Link to heading
Write a post in markdown, run one command, and have a clean audio player appear automatically above the content. No manual work after setup.
The Stack Link to heading
- Hugo — static site generator
- ElevenLabs API — text to speech generation (free tier works for a hobby blog)
- Python script — automation glue between Hugo and ElevenLabs
- Fuse.js — client-side search on the Blog page
Where Everything Lives Link to heading
thedigitaldown.com/
├── generate_audio.py ← the automation script, run this after writing a post
├── start-session.bat ← double-click to start a dev session with API key loaded
├── hugo.toml ← site config
│
├── content/posts/ ← your markdown posts live here
│
├── static/audio/ ← generated MP3 files live here (source of truth)
│ └── post-slug.mp3
│
├── data/
│ └── audio.json ← auto-generated index Hugo uses to find audio files
│
├── layouts/
│ ├── partials/
│ │ └── audio-player.html ← the audio player widget injected into every post
│ ├── posts/
│ │ ├── single.html ← post template (has the partial call added)
│ │ └── list.html ← blog list page with search bar
│ └── _default/
│ └── index.json ← search index template Hugo builds at compile time
│
└── public/ ← Hugo builds into here, do not edit directly
└── audio/ ← MP3s copied here from static/audio/ on hugo build
How It Works Link to heading
1. The Python Script Link to heading
generate_audio.py does the following when you run it:
- Scans
content/posts/for all markdown files - Parses the TOML frontmatter (the
+++block) to get the title - Strips markdown syntax so the voice reads clean prose
- Calls the ElevenLabs API with the cleaned text
- Saves the MP3 to
static/audio/post-slug.mp3 - Updates
data/audio.jsonso Hugo knows the audio exists
2. The Hugo Partial Link to heading
layouts/partials/audio-player.html checks data/audio.json for a matching entry using the post slug. If it finds one it renders the player. If not it renders nothing. This means you never get a broken player on posts without audio.
3. The Post Template Link to heading
layouts/posts/single.html has this line added just above {{ .Content }}:
{{ partial "audio-player.html" . }}
That one line is all Hugo needs to inject the player on every post automatically.
Daily Workflow Link to heading
Starting a session Link to heading
Double-click start-session.bat in the site root. This opens PowerShell in the right directory with your ElevenLabs API key already loaded.
Writing and publishing a post Link to heading
# 1. Write your post in content/posts/my-new-post.md
# 2. Generate audio for any new posts
python generate_audio.py
# 3. Rebuild the site
hugo
# 4. Deploy (sync public/ to your host)
Useful script flags Link to heading
# Generate audio for one specific post by slug
python generate_audio.py --post my-post-slug
# Regenerate all audio (overwrites existing, uses ElevenLabs credits)
python generate_audio.py --all
ElevenLabs Setup Link to heading
- Sign up free at elevenlabs.io
- Choose ElevenCreative platform
- Go to Developers in the sidebar
- Create an API key with Text to Speech access enabled
- Free tier: ~10,000 characters per month (roughly 7-15 average posts)
The voice currently used is George (Voice ID: JBFqnCBsd6RMkjVDRZzb) — a free tier voice that works well for tech and personal content. To change voices, update ELEVENLABS_VOICE_ID at the top of generate_audio.py.
The API Key Link to heading
The API key is set as an environment variable each session. It is stored in start-session.bat so it loads automatically. Never commit this file to a public git repository.
# If you ever need to set it manually
$env:ELEVENLABS_API_KEY="your_key_here"
Rebuilding From Scratch Link to heading
If you ever need to wipe the public folder and rebuild everything cleanly:
Remove-Item -Recurse -Force public
hugo
The MP3 files are safe in static/audio/ and will be copied back into public/audio/ on the next hugo build. You do not need to regenerate audio from ElevenLabs when doing a clean rebuild.
Search Link to heading
The Blog page has full-content search powered by Fuse.js. It reads from public/search.json which Hugo generates automatically at build time from layouts/_default/index.json. No additional setup needed — it just works after every hugo build.
+++