← Blog · 27 October 2026 · Lire en français

Article audio in your own player: keeping your design system and your security policy

Your front-end team already has an audio component and a no-third-party-scripts rule. Here is how to get narrated articles, chapters and word-level highlighting into it: the copyable render toolkit, the manifest your server builds, and the trade-offs you accept.

Most text-to-speech vendors sell you a player. That is fine for a blog, and it is exactly what a mature publisher does not want. If your newsroom has a design system, an accessibility team that audits every component, and a content security policy that blocks third-party scripts, a vendor widget is a negotiation you will lose internally before it even starts.

So the real question is not "which player looks nicest". It is: can we take the audio, plus everything that makes it usable, and render it with our own components? This post answers that for our product, with the parts that are your job spelled out.

The hard part is not the play button

Any front-end developer can wire an MP3 to an <audio> element before lunch. What takes weeks is the rest: a progress bar that works with a keyboard and a screen reader, chapters you can jump to, word-by-word highlighting that stays in sync, resuming where the listener left off, and listening analytics that do not count a scrub to the end as a completed listen.

Our render toolkit exists so that refusing our player does not mean rebuilding all of that. It ships as source files you copy into your own repository, not as a package and not as a script loaded from our domain. You review it, version it, change it, delete it. Nothing runs on your pages that you did not commit.

Pick the file that matches where you start

  • A self-contained component. wedispatch-player.js defines <wedispatch-player>, rendered inside a Shadow DOM so that neither side's CSS leaks into the other. You theme it with custom properties such as --wd-accent and --wd-rayon, and reach deeper through a few ::part() hooks.
  • Logic only, for your own React or Vue rendering. reference-web.js holds the same behaviour with no markup at all.
  • An adapter for the player you already have. If your design system includes an audio component, wedispatch-sipaui.js drives it rather than replacing it. It adds zero CSS and creates no elements in your bar. It wires play and pause, skipping, progress, time, closing, resume and listening milestones, and, if you point it at containers, word highlighting and chapters.

For native apps there are two reference implementations, Swift (with lock-screen playback) and Kotlin. They are starting points to fold into your codebase, not dependencies to track.

Your server builds the manifest

One rule shapes the whole design: the browser never talks to our API. An API key does not belong in a web page or a mobile binary. Your backend calls our endpoints and hands the client a small JSON manifest: article id, title, audio URL, duration, summary, chapters, and an optional list of words with start and end times.

Two calls produce it. GET /api/v1/podcasts/{id} returns the audio record; /words returns the word-level alignment, and you skip it entirely if you do not display highlighted text. If you have never called the API, the minimal first request shows the record you will be reshaping.

Two details catch people out, so here they are up front:

  • Protected files expire. If your account protects its audio, the URL is signed and short-lived (two hours today, stated in url_expires_in). Cache it for less than that and never store it in a database.
  • Alignment may be missing. Audio produced before word-level alignment existed answers 409 on /words. That is not an outage; it means the article needs regenerating. The player still works, it simply hides the Text tab. More generally, a tab with no content is never shown: no chapters, no Chapters button.

What you get without writing it

  • A progress control with a slider role, keyboard shortcuts (space, arrows, Home, End), screen-reader announcements and correct aria-pressed and aria-expanded states, plus respect for prefers-reduced-motion.
  • Resume with sensible bounds: under fifteen seconds there is nothing to resume, and within ten seconds of the end the article counts as finished.
  • Playback speed and reading-comfort settings remembered across articles.
  • Listening milestones at 25, 50, 75 and 100 percent, each sent once, and not triggered by seeking.

Your own code stays in charge through plain methods (play, pause, seek, setRate, hide, show, destroy) and DOM events such as wd:ready, wd:chapter and wd:milestone. hide covers a specific case: an ad takes over the screen, playback carries on, the card disappears. destroy is there for single-page apps that never truly reload.

One caveat on accessibility. When the adapter drives your component, your component carries the audit. The adapter sets ARIA states; it cannot make an unlabeled button accessible. What we check on our own player, and what is still missing, is listed in our player against accessibility criteria.

Privacy, stated precisely

The only request the component makes on its own is the listening event, and it carries three fields: article id, event type, origin. No listener id, no cookie, no fingerprint. Set no-events to switch it off, or events-url to send it to your own collector instead of ours. Local storage holds three things: playback position, speed and comfort settings. None of it leaves the browser.

The trade-offs you accept

Copying code instead of loading a script has a cost, and it is better to name it:

  • No automatic updates. A fix on our side reaches you only when you pull the new file. Changes are announced in the release notes.
  • The manifest is yours to maintain. That is the price of keeping your key on your server.
  • No ad logic. The audio record exposes midroll_s, the cleanest paragraph break in the track, so a break never lands mid-sentence. What you insert there is your business.

If your platform renders pages server-side and refuses even copied scripts, this toolkit is the wrong tool; the server-composed block is the right one, and in-page player versus iframe explains the difference.

A ten-minute test

The folder includes exemple.html, a page that runs straight from disk with a hard-coded demo manifest and makes no server call. Give it to your front-end lead. Within ten minutes they will know whether the logic fits your components, with no account and no key. If it does, the remaining work is one backend route that returns the manifest.

And if you have no design system and no front-end team, none of this is needed: the standard player is one line of script, or zero lines with the WordPress plugin.

Give your articles a voice with WeDispatch

This blog is itself voiced by WeDispatch. Curious how it sounds on your content?

Book a demo

Read next