Adding a Custom Arc Audio Player to Your Article Body Chain
This guide shows how to add the audio player as a custom feature component from scratch (not reusing a shared bundle’s component) and wire it into a custom article body chain. It walks through the four things the player needs at runtime, then covers integration and code-quality considerations.
The <arc-audio-player> / <koi-card-audio-player> custom elements need:
- The audio player script loaded on the page (once)
- An Arc audio token (
arc-tokenattribute) - A media ID (
media-idattribute), typically pulled from the Arc Native Specification (ANS) audio element - Org and env config (
configattribute), JSON-encoded
1. Load the audio player script once per page
The audio player ships as an ES module at:
https://{org}-{env}.audio.arc-cdn.net/player/koi/v0.2.17/koi-player.min.jsInject it once per page and de-duplicate by src so many player instances on the same article do not double-load it:
const scriptUrlCache = new Set();
function ensureAudioPlayerScript(src) { if (typeof document === "undefined" || !src) return; if (scriptUrlCache.has(src)) return; if (document.querySelector(`script[src="${src}"]`)) { scriptUrlCache.add(src); return; } const script = document.createElement("script"); script.type = "module"; script.src = src; script.setAttribute("data-audio-player", "1"); document.head.appendChild(script); scriptUrlCache.add(src);}Build the URL from AUDIO_ORG / AUDIO_ENV env values so org/env live in one place:
const AUDIO_ORG = "yourorg";const AUDIO_ENV = "sandbox";
export default { AUDIO_ORG, AUDIO_ENV, AUDIO_PLAYER_SCRIPT_URL: `https://${AUDIO_ORG}-${AUDIO_ENV}.audio.arc-cdn.net/player/koi/v0.2.17/koi-player.min.js`,};2. Give an Arc audio token
The player calls audio APIs that require an Arc token. Without it, the player loads, but every request fails in the content delivery network (CDN) environment. Two methods give it:
Method A: static ARC_AUDIO_TOKEN env var
Manually get a token (from the Developer Center or by calling the delivery API once) and add it to your bundle environment:
export default { ARC_AUDIO_TOKEN: "arc-public-sandbox-...", // ...other env values};Then read it inside your component:
import { ARC_AUDIO_TOKEN } from "fusion:environment";// ...<arc-audio-player arc-token={ARC_AUDIO_TOKEN} ... />Pros
- Simplest setup: one value in one env file.
- No runtime API call to fetch the token, so no extra latency or failure mode at render time.
- Works even if the delivery API becomes unreachable from the bundle (locked-down networks, local dev with no Arc creds).
- The token shows up in resolved env, simplifying debugging.
Cons
- Manual rotation: when you rotate the token or it expires, you must update env config and redeploy.
- Per-environment files (
sandbox.js,prod.js, …) each need a separate value. - Risk of stale tokens silently breaking playback between rotations.
- Long-lived secret in version-controlled config, harder to audit and rotate.
Method B: delivery-keys content source
Write a small content source that calls Arc’s /delivery-api/v1/access/keys and selects the audio key for your org/env. The following example shows an implementation. Before it will work, you’ll need to follow the earlier directions to generate a key and add it to an audio collection.
import { ARC_ACCESS_TOKEN, CONTENT_BASE, ENVIRONMENT } from "fusion:environment";
const resolveEnv = () => { const [orgId = "yourorg", env = "sandbox"] = (ENVIRONMENT || "").split("-"); return { orgId, env };};
async function fetchFunction() { const { orgId, env } = resolveEnv(); const collectionName = `arc-client-${orgId}-${env}-audio`;
const response = await fetch(`${CONTENT_BASE}/delivery-api/v1/access/keys`, { method: "GET", headers: { "content-type": "application/json", ...(ARC_ACCESS_TOKEN && { Authorization: `Bearer ${ARC_ACCESS_TOKEN}` }), }, });
const data = await response.json().catch(() => null);
if (!response.ok || !Array.isArray(data)) { return { status: "error", errorCode: response.status, errorMessage: "delivery-keys fetch failed" }; }
const audioKey = data .find((k) => k?.key_details?.collections?.some((c) => c?.collectionName === collectionName)) ?.key_details?.keyValue;
return audioKey ? { audioKey } : { status: "error", errorCode: 404, errorMessage: "no audio key" };}
export default { fetch: fetchFunction, params: {}, schemaName: "delivery-keys" };Then consume it in your component:
import { useContent } from "fusion:content";import { ARC_AUDIO_TOKEN } from "fusion:environment";
const { audioKey } = useContent({ source: "delivery-keys", query: {} }) || {};const arcToken = audioKey || ARC_AUDIO_TOKEN; // fall back if the API is downThe content source must have these env values available:
| Variable | Purpose |
|---|---|
ARC_ACCESS_TOKEN | Bearer credential for the delivery API. Server-side only: never exposed to the browser. |
CONTENT_BASE | Base URL for the delivery API (e.g. https://api.{org}.arcpublishing.com). |
ENVIRONMENT | Used to derive org/env so the source can find the arc-client-{org}-{env}-audio collection. |
Pros
- Token fetched at request time: rotating the underlying audio key takes effect without a bundle redeploy.
- Org/env values come from a single
ENVIRONMENTvalue, not duplicated per env file. - Centralized secret: only
ARC_ACCESS_TOKEN(the delivery-api credential) lives in env config. The audio token stays out of version control. - Cached via Fusion’s content-source caching, so requests amortize the lookup cost.
Cons
- Adds a runtime dependency: if the delivery API is slow or unavailable, the source errors. Keep
ARC_AUDIO_TOKENset as a fallback if you want graceful degradation. - Requires
ARC_ACCESS_TOKENprovisioned with delivery-api access in every environment. - More moving parts to debug (network, content-source cache TTL, collection naming).
- Naming dependency: assumes a key in
arc-client-{org}-{env}-audio. Orgs with a different convention need to adjust the lookup.
3. Resolve the media ID from the ANS element
The article body chain hands each content_elements entry to a renderer. For an audio element, the playable ID is _id (sometimes additional_properties.audio_id):
case "audio": { const mediaId = item?._id || item?.additional_properties?.audio_id; return mediaId ? <YourAudioPlayer key={`audio_${index}_${item._id}`} mediaId={mediaId} /> : null;}Without an ID, render nothing: do not render an empty player.
4. Your custom feature component
Putting it together, the smallest viable component:
import React, { useEffect, useMemo } from "react";import PropTypes from "@arc-fusion/prop-types";import { useContent } from "fusion:content";import { ARC_AUDIO_TOKEN, AUDIO_ORG, AUDIO_ENV, AUDIO_PLAYER_SCRIPT_URL,} from "fusion:environment";
const scriptUrlCache = new Set();
function ensureAudioPlayerScript(src) { if (typeof document === "undefined" || !src) return; if (scriptUrlCache.has(src)) return; if (document.querySelector(`script[src="${src}"]`)) { scriptUrlCache.add(src); return; } const script = document.createElement("script"); script.type = "module"; script.src = src; document.head.appendChild(script); scriptUrlCache.add(src);}
const YourAudioPlayer = ({ mediaId, orgId = AUDIO_ORG, env = AUDIO_ENV }) => { const { audioKey } = useContent({ source: "delivery-keys", query: {} }) || {}; const arcToken = audioKey || ARC_AUDIO_TOKEN;
useEffect(() => ensureAudioPlayerScript(AUDIO_PLAYER_SCRIPT_URL), []);
const config = useMemo(() => JSON.stringify({ orgId, env }), [orgId, env]);
if (!mediaId || !arcToken) return null;
return <arc-audio-player config={config} media-id={mediaId} arc-token={arcToken} />;};
YourAudioPlayer.propTypes = { mediaId: PropTypes.string.isRequired, orgId: PropTypes.string, env: PropTypes.string,};
export default YourAudioPlayer;Then in your article body chain, call it from the case "audio" branch as shown in step 3.
Quality considerations
A few things worth getting right up front so this does not bite you in production.
Keep the player a feature, not chain-inlined
Define the player as a standalone feature component (as described earlier) and have the chain dispatch to it. That way the player maintains dedicated propTypes, tests, and lifecycle. You can drop it anywhere: in a PageBuilder block, an article body chain, or a related-content rail, without copy-pasting custom-element JSX.
Server-side rendering (SSR) safety
<arc-audio-player> registers as a custom element through client-side JS, so its first render on the server produces an unknown tag. This causes no problems: the script upgrades it on the client. But you must guard all script-injection logic with typeof document === "undefined" (as described earlier) or keep it inside a useEffect. Do not reach for window or document at module scope.
Render nothing when invariants fail
Without a mediaId, or if you cannot resolve the token, return null rather than render a broken player. Avoid throwing: an unhandled exception in one element can blow up the whole article.
Keep org/env values in one place
Derive both AUDIO_PLAYER_SCRIPT_URL and the config payload from the same AUDIO_ORG / AUDIO_ENV env values. Avoid hardcoding the script URL alongside a separate org/env constant: they will drift.
De-duplicate the script tag
Many audio elements in one article (or PageBuilder page) must not load the script more than once. The scriptUrlCache set plus the querySelector check handles both cases: the same React tree, and re-mounts across pages in a single-page application (SPA).
Memoize JSON-encoded attributes
config={JSON.stringify({orgId, env})} re-creates the string on every render. The player only reads config once, in connectedCallback, so this does not affect the mounted player. But a changing prop identity still wastes work and produces an unstable attribute value in the rendered HTML. Wrap it in useMemo.
Do not expose ARC_ACCESS_TOKEN to the client
Content sources run server-side in Fusion, so the bearer credential stays on the server and only the resolved audioKey ships to the browser. Never import ARC_ACCESS_TOKEN into a feature component or pass it through props: that token has broader scope than the audio token and should not appear in HTML or client JS bundles.
Test the chain switch with no audio element
The most common regression after changing the chain: breaking articles that do not have audio. Add a chain test that renders an article whose content_elements contain no audio entries and assert that no player renders.
Pin the audio player version
The script URL shown earlier pins v0.2.17. Treat it like any other dependency: review changelogs before bumping, and consider environment-by-environment promotion (sandbox first, then prod).