Narration
A "Listen to this page" player that reads each page aloud and highlights the sentence being read, using the reader's own browser voices or neural voices generated at build time.
Narration adds a Listen to this page player under each page’s description. Readers press play, and the page is read aloud from the title down while the sentence being read is highlighted and kept in view. It’s opt-in:
export default defineConfig({
narration: true,
});
With true, pages are read with the reader’s own browser voices: no key, no build step, and it works on any host, static or not. Add a provider to use generated voices instead.
What it reads
Narration reads the page title, the description, headings, paragraphs, list items, and card text, in order. Some components are introduced with a short spoken cue so the listener knows what kind of content comes next:
| Component | Cue |
|---|---|
| Callouts | “Note.”, “Tip.”, “Warning.”, and so on |
| Steps | “Step 1.”, “Step 2.” |
| Tabs | “macOS tab.” Every tab is read, not just the open one |
| Accordions and Expandable | “Expandable section.” |
When narration reaches a closed section or a tab that isn’t showing, it opens it, so the highlight always lands on text the reader can see.
Code blocks, tables, images, video, diagrams, math, type tables, file trees, and live component previews are skipped. They don’t make sense read aloud.
The player only appears on pages with enough prose to be worth listening to, roughly 50 words. Very short pages, and pages that are mostly code or API fields, don’t get one.
Listening
While a page plays, the player stays pinned under the header with play and pause, previous and next sentence, a progress slider, the time read against the page’s length, and a speed control from 0.8× to 2×. The speed is remembered for the next page.
The page scrolls to keep the sentence being read in view. If the reader scrolls away, following stops and the audio carries on; scrolling back to the sentence, or pressing Follow along, picks it up again. Opening another page stops the narration.
Browser voices
narration: true reads the page with the Web Speech API and the voices on the reader’s device, picking one for the page’s language. The player stays hidden on a browser with no voice for that language rather than reading the page in the wrong accent.
This needs nothing from you and costs nothing, but how it sounds depends on the device: the system voices on macOS, iOS, and Windows are good, while some Linux browsers ship few or none.
Generated voices
Pass a provider to generate audio with a neural speech model at build time. The provider is the same gateway() adapter the assistant uses, pointed at a speech model:
import { defineConfig } from "blume";
import { gateway } from "blume/ai";
export default defineConfig({
narration: {
provider: gateway({
model: "openai/tts-1-hd",
voice: "alloy",
}),
},
});
blume build reads each built page exactly as the player will, splits it into sentences, and generates one clip per sentence through the Vercel AI Gateway. The clips and a small manifest per page are written into your build as static files under /blume-narration/, so replays cost nothing and nothing runs on a server.
Before it generates anything, the build logs how many new clips it needs and how many characters they cover, so the cost is visible up front:
Generating narration: 214 new clip(s), 15,880 characters, with openai/tts-1-hd
| Option | Default | Description |
|---|---|---|
model |
openai/tts-1-hd |
A gateway speech model: openai/tts-1, openai/tts-1-hd, fish-audio/s2.1-pro, spacexai/grok-tts, and others the gateway lists. |
voice |
alloy |
The model’s voice. |
instructions |
How the voice should sound, for models that take instructions (“Read calmly, like a teacher”). | |
apiKeyEnv |
AI_GATEWAY_API_KEY |
The env var holding the gateway key. On Vercel, the build’s OIDC token works too. |
headers |
Static headers sent with every request. | |
providerOptions |
Passed to the AI SDK’s generateSpeech as-is, for model settings Blume doesn’t name. |
Caching
Clips are cached in node_modules/.cache/blume/narration by what decides their sound: the sentence, its language, the model, the voice, and the instructions. A rebuild only pays for sentences that changed, and a sentence shared by many pages is generated once. Vercel and Netlify restore node_modules from their build caches, so a deploy there regenerates only what changed. On other CI, cache that directory between runs.
Fallbacks
Generated voices fall back to browser voices wherever no clips exist:
- In
blume dev, which never generates audio. Runblume buildandblume previewto hear the generated voice locally. - When the key isn’t set at build time. The build warns and skips generation.
- When generation fails. The build stops at the first failed clip, since the AI SDK has already retried it, and keeps every clip it has cached.
Languages
Narration reads each page in its content language, so on an internationalized site every locale is read in its own voice. The spoken cues and the player’s labels are translated in every built-in UI language. Override them per locale under narration in i18n.ui.
Turning it off for a page
Set narration: false in a page’s frontmatter to leave that page without a player:
---
title: Changelog
narration: false
---
Keeping content out
Add data-blume-narration="skip" to an element to leave it, and everything inside it, out of narration. This is how Blume skips its own type tables and previews, and it works the same on your own components and islands:
<div data-blume-narration="skip">
<PricingCalculator />
</div>
Analytics
The player sends a narration_play event, with the engine (audio or browser) and the page’s path, when a reader starts listening, and narration_complete when a page plays to the end. Both go through your analytics adapters like the other custom events.