Listen to this article
0:00 / 0:00

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:

  1. Scans content/posts/ for all markdown files
  2. Parses the TOML frontmatter (the +++ block) to get the title
  3. Strips markdown syntax so the voice reads clean prose
  4. Calls the ElevenLabs API with the cleaned text
  5. Saves the MP3 to static/audio/post-slug.mp3
  6. Updates data/audio.json so 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.


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. +++