Podcast-Player selbst hosten
Jedes Publish-Ziel bekommt bereits ohne jedes Setup einen voll funktionsfähigen nativen Audio-Player. Diese Seite ist für eine reine WordPress-Site (ohne mylandingpage.ai-Plugin), die den moderneren Custom-Player möchte: größere Bedienelemente, Skip -10s/+20s, eine Seekbar.
Was das macht
Blog-Maker veröffentlicht eine Podcast-Episode als einfaches <figure class="bm-podcast-player">, das ein natives <audio controls>-Element und einen Fallback-Link enthält. Das funktioniert bereits überall, ohne jedes Setup.
Die Snippets unten legen eine moderne Bedienleiste darüber: Play/Pause, Skip -10s/+20s, eine Seekbar mit verstrichener/verbleibender Zeit und ein Mute-Button. Das ist Progressive Enhancement: Fügst du diese Dateien nie hinzu, bekommen Besucher weiter den einfachen nativen Player. In beiden Fällen bricht nichts.
Warum selbst gehostet, nicht von blog-maker.com verlinkt
Wir könnten das als einzelnes <script src="https://blog-maker.com/podcast-enhancer.js"> anbieten, aber dann macht der Browser jedes Besuchers bei jedem Seitenaufruf eine Anfrage an unsere Server und legt dabei dessen IP-Adresse offen, ohne dass er das erwarten würde. Unter der DSGVO ist das ein Drittanbieter-Datenfluss, den du offenlegen und oft per Consent einholen müsstest, nur für ein UI-Upgrade.
Stattdessen: beide Dateien auf deinen eigenen Server kopieren. Null externe Anfragen, nichts offenzulegen, nichts an deinem Cookie-/Consent-Setup zu ändern. Das JS selbst macht keine eigenen Netzwerk-Aufrufe (kein Analytics, kein Tracking, kein Rückruf zu uns), es hört nur auf Events am Audio-Element, das ohnehin schon auf deiner Seite steht.
Die Audio-Datei selbst: bist du auf mylandingpage.ai oder lädt Blog-Maker die .mp3 bereits in deine eigene Mediathek, ist sie schon first-party. Auf einer reinen WordPress-Site ohne das streamt das Audio weiterhin von blog-maker.com's CDN, sobald ein Besucher auf Play drückt (nicht vorher, preload="none", siehe Markup unten). Das ist eine bewusste, nutzer-ausgelöste Anfrage, dasselbe Vertrauensmodell wie beim Einbetten jedes anderen Drittanbieter-Media-Players.
Das Markup, das du siehst (hier musst du nichts bauen)
Das schreibt Blog-Maker automatisch in deinen Beitragsinhalt. Du schreibst das nicht selbst, es steht hier, damit du weißt, worauf die Snippets unten zielen.
<figure class="bm-podcast-player" data-bm-podcast="2"> <figcaption>Podcast episode: Show Name</figcaption> <audio controls preload="none" src="https://blog-maker.com/audio/podcasts/..."></audio> <p><a href="https://blog-maker.com/audio/podcasts/..." rel="noopener">Play the episode as audio, right in your browser</a></p> </figure>
1. Als CSS-Datei auf deinem Server speichern
Feste Farbwerte statt eigener Custom Properties deines Themes: ein Copy-Paste-Snippet muss eigenständig funktionieren. Die Hex-Werte gerne an dein Branding anpassen.
.bm-podcast-player {
margin: 1.75rem 0;
padding: 1rem 1.25rem;
border: 1px solid rgba(10, 57, 78, 0.16);
border-radius: 12px;
background: #eef1f3;
box-shadow: 0 1px 3px rgba(10, 57, 78, 0.08);
min-height: 132px;
}
@media (max-width: 640px) {
/* 132px was measured against the desktop (single-row) control bar. At
mobile widths the 6-item control bar wraps to two rows: live-measured
215px at 375px AND 320px viewport width (design-reviewer finding,
2026-08-24). 240px, not 220px: that measurement predated the
.bm-podcast-mute touch-target fix in this same commit, which grows
the wrapped row by another 8px -- two independent re-reviews both
live-measured the combined real height at 223px. */
.bm-podcast-player { min-height: 240px; }
}
/* Video gets no card wrap: the Bunny iframe already brings its own
modern controls, a bordered/padded/background box would just double
up on chrome the iframe already has. */
.bm-podcast-video { margin: 1.75rem 0; }
.bm-podcast-player figcaption,
.bm-podcast-video figcaption {
margin: 0 0 0.5rem;
font-size: 16px;
font-weight: 600;
letter-spacing: 0.01em;
color: #4a6363;
}
.bm-podcast-player audio {
display: block;
width: 100%;
height: 40px;
}
.bm-podcast-player audio[data-bm-enhanced] { display: none; }
.bm-podcast-video iframe {
display: block;
width: 100%;
height: auto;
aspect-ratio: 16 / 9;
border: 0;
border-radius: 8px;
}
.bm-podcast-player p,
.bm-podcast-video p { margin: 0.625rem 0 0; }
.bm-podcast-player p a,
.bm-podcast-video p a {
font-size: 14px;
font-weight: 500;
color: #c2410c;
text-decoration: underline;
text-decoration-thickness: 1px;
text-underline-offset: 2px;
}
.bm-podcast-player p a:hover,
.bm-podcast-video p a:hover { color: #9a3412; }
.bm-podcast-player p a:focus-visible,
.bm-podcast-video p a:focus-visible {
outline: 2px solid #c2410c;
outline-offset: 2px;
border-radius: 4px;
}
.bm-podcast-controls { display: flex; align-items: center; gap: 8px; margin-top: 4px; flex-wrap: wrap; }
.bm-podcast-btn {
display: inline-flex; align-items: center; justify-content: center;
flex: 0 0 auto; width: 36px; height: 36px; padding: 0; border: none;
border-radius: 999px; background: #e8edf0; color: #0a394e; cursor: pointer;
}
.bm-podcast-btn:hover { background: #dde4e8; }
.bm-podcast-btn:active { background: #ccd6db; }
.bm-podcast-btn:disabled { opacity: 0.7; cursor: default; }
.bm-podcast-btn:focus-visible { outline: 2px solid #c2410c; outline-offset: 2px; }
.bm-podcast-play {
width: 44px; height: 44px;
background: linear-gradient(to bottom right, #c2410c, #9a3412);
color: #ffffff;
}
.bm-podcast-play:hover { background: linear-gradient(to bottom right, #c2410c, #9a3412); filter: brightness(0.93); }
.bm-podcast-icon { width: 20px; height: 20px; fill: currentColor; }
.bm-podcast-play .bm-podcast-icon-pause { display: none; }
.bm-podcast-play.is-playing .bm-podcast-icon-play { display: none; }
.bm-podcast-play.is-playing .bm-podcast-icon-pause { display: block; }
.bm-podcast-mute .bm-podcast-icon-muted { display: none; }
.bm-podcast-mute.is-muted .bm-podcast-icon-vol { display: none; }
.bm-podcast-mute.is-muted .bm-podcast-icon-muted { display: block; }
.bm-podcast-skip { width: auto; min-height: 44px; padding: 0 10px; gap: 2px; }
.bm-podcast-skip .bm-podcast-icon { width: 18px; height: 18px; }
.bm-podcast-skip-n { font-size: 10px; font-weight: 700; }
.bm-podcast-time { flex: 0 0 auto; min-width: 32px; font-size: 12px; font-variant-numeric: tabular-nums; color: #4a6363; }
.bm-podcast-seek { flex: 1 1 auto; min-width: 60px; height: 4px; accent-color: #c2410c; cursor: pointer; }
.bm-podcast-seek::-webkit-slider-runnable-track { background: rgba(10, 57, 78, 0.18); border-radius: 2px; height: 4px; }
.bm-podcast-seek::-moz-range-track { background: rgba(10, 57, 78, 0.18); border-radius: 2px; height: 4px; }
.bm-podcast-seek::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 14px; height: 14px; border-radius: 50%; background: #c2410c; cursor: pointer; margin-top: -5px; }
.bm-podcast-seek::-moz-range-thumb { width: 14px; height: 14px; border: none; border-radius: 50%; background: #c2410c; cursor: pointer; }
.bm-podcast-seek:disabled { cursor: default; opacity: 0.7; }
.bm-podcast-seek:focus-visible { outline: 2px solid #c2410c; outline-offset: 2px; }
.bm-podcast-mute { margin-left: 4px; min-width: 44px; min-height: 44px; }2. Als JS-Datei auf deinem Server speichern
Unverändert übernehmen, kein Build-Schritt nötig. Mit defer-Attribut (oder im Footer) laden, damit es das Rendern der Seite nie blockiert.
/*
* Blog-Maker podcast player Enhancer.
*
* Progressive enhancement, vanilla, no dependencies: the base markup
* already works as a plain <audio controls> without this script. This
* script REPLACES the native controls with a custom UI: play/pause,
* seekbar, elapsed/remaining time, and skip -10s/+20s buttons.
*
* iOS Safari quirks handled explicitly:
* - Skip buttons and the seek input stay disabled until loadedmetadata
* fires, instead of quietly failing a seek against NaN/Infinity.
* - play() is only ever called from within a real user-gesture handler
* (the play button's own click listener), never programmatically.
*/
(function () {
"use strict";
function fmtTime(seconds) {
if (!isFinite(seconds) || seconds < 0) seconds = 0;
var m = Math.floor(seconds / 60);
var s = Math.floor(seconds % 60);
return m + ":" + (s < 10 ? "0" : "") + s;
}
// Icon visibility toggles on the BUTTON's own state class (is-playing,
// is-muted), never via `hidden` on the <svg> itself: SVGElement's
// `hidden` IDL reflection is inconsistent across engines. A class on an
// HTMLButtonElement is universally reliable.
var ICON_PLAY =
'<svg viewBox="0 0 24 24" class="bm-podcast-icon bm-podcast-icon-play" aria-hidden="true"><path d="M8 5v14l11-7z"/></svg>';
var ICON_PAUSE =
'<svg viewBox="0 0 24 24" class="bm-podcast-icon bm-podcast-icon-pause" aria-hidden="true"><path d="M6 5h4v14H6zM14 5h4v14h-4z"/></svg>';
var ICON_BACK =
'<svg viewBox="0 0 24 24" class="bm-podcast-icon" aria-hidden="true"><path d="M12 5V1L7 6l5 5V7c3.31 0 6 2.69 6 6s-2.69 6-6 6-6-2.69-6-6H4c0 4.42 3.58 8 8 8s8-3.58 8-8-3.58-8-8-8z"/></svg>';
var ICON_FWD =
'<svg viewBox="0 0 24 24" class="bm-podcast-icon" aria-hidden="true"><path d="M12 5V1l5 5-5 5V7c-3.31 0-6 2.69-6 6s2.69 6 6 6 6-2.69 6-6h2c0 4.42-3.58 8-8 8s-8-3.58-8-8 3.58-8 8-8z"/></svg>';
var ICON_VOL =
'<svg viewBox="0 0 24 24" class="bm-podcast-icon bm-podcast-icon-vol" aria-hidden="true"><path d="M3 9v6h4l5 5V4L7 9H3z"/></svg>';
var ICON_MUTED =
'<svg viewBox="0 0 24 24" class="bm-podcast-icon bm-podcast-icon-muted" aria-hidden="true"><path d="M3 9v6h4l5 5V4L7 9H3zm13.59 3l2.7-2.71-1.42-1.41L15.17 12l-2.7-2.71-1.41 1.41L13.76 13l-2.7 2.71 1.41 1.41L15.17 14l2.7 2.71 1.42-1.41L16.59 12z"/></svg>';
function enhance(audio) {
if (audio.dataset.bmEnhanced) return;
audio.dataset.bmEnhanced = "1";
var figure = audio.closest(".bm-podcast-player");
if (!figure) return;
audio.removeAttribute("controls");
var bar = document.createElement("div");
bar.className = "bm-podcast-controls";
bar.innerHTML =
'<button type="button" class="bm-podcast-btn bm-podcast-play" aria-label="Play">' +
ICON_PLAY + ICON_PAUSE + "</button>" +
'<button type="button" class="bm-podcast-btn bm-podcast-skip bm-podcast-skip-back" aria-label="Back 10 seconds" disabled>' +
ICON_BACK + '<span class="bm-podcast-skip-n">10</span></button>' +
'<button type="button" class="bm-podcast-btn bm-podcast-skip bm-podcast-skip-fwd" aria-label="Forward 20 seconds" disabled>' +
ICON_FWD + '<span class="bm-podcast-skip-n">20</span></button>' +
'<span class="bm-podcast-time bm-podcast-current">0:00</span>' +
'<input type="range" class="bm-podcast-seek" min="0" max="0" step="1" value="0" disabled aria-label="Seek position">' +
'<span class="bm-podcast-time bm-podcast-duration">0:00</span>' +
'<button type="button" class="bm-podcast-btn bm-podcast-mute" aria-label="Mute">' +
ICON_VOL + ICON_MUTED + "</button>";
// Insert right after the (now-hidden) audio, not appended to the end
// of the figure: the base markup already carries a <p><a> fallback
// link AFTER the audio, so appending here would put the bar below
// that link instead of above it (found live, 2026-08-26).
audio.insertAdjacentElement("afterend", bar);
var playBtn = bar.querySelector(".bm-podcast-play");
var backBtn = bar.querySelector(".bm-podcast-skip-back");
var fwdBtn = bar.querySelector(".bm-podcast-skip-fwd");
var curEl = bar.querySelector(".bm-podcast-current");
var durEl = bar.querySelector(".bm-podcast-duration");
var seek = bar.querySelector(".bm-podcast-seek");
var muteBtn = bar.querySelector(".bm-podcast-mute");
var scrubbing = false;
playBtn.addEventListener("click", function () {
if (audio.paused) {
// iOS Safari: preload="none" + no prior load() rejects the first
// play() silently. load() is synchronous, calling it right before
// play() keeps both in the same gesture.
if (audio.readyState === 0) audio.load();
audio.play().catch(function () {});
} else {
audio.pause();
}
});
audio.addEventListener("play", function () {
playBtn.classList.add("is-playing");
playBtn.setAttribute("aria-label", "Pause");
});
audio.addEventListener("pause", function () {
playBtn.classList.remove("is-playing");
playBtn.setAttribute("aria-label", "Play");
});
audio.addEventListener("ended", function () {
playBtn.classList.remove("is-playing");
playBtn.setAttribute("aria-label", "Play");
});
audio.addEventListener("loadedmetadata", function () {
var d = audio.duration;
if (isFinite(d) && d > 0) {
seek.max = String(Math.floor(d));
durEl.textContent = fmtTime(d);
seek.disabled = false;
backBtn.disabled = false;
fwdBtn.disabled = false;
}
});
audio.addEventListener("timeupdate", function () {
if (scrubbing) return;
curEl.textContent = fmtTime(audio.currentTime);
if (Number(seek.max) > 0) seek.value = String(Math.floor(audio.currentTime));
});
seek.addEventListener("input", function () {
scrubbing = true;
curEl.textContent = fmtTime(Number(seek.value));
});
seek.addEventListener("change", function () {
audio.currentTime = Number(seek.value);
scrubbing = false;
});
backBtn.addEventListener("click", function () {
audio.currentTime = Math.max(0, audio.currentTime - 10);
});
fwdBtn.addEventListener("click", function () {
var d = isFinite(audio.duration) ? audio.duration : audio.currentTime;
audio.currentTime = Math.min(d, audio.currentTime + 20);
});
muteBtn.addEventListener("click", function () {
audio.muted = !audio.muted;
muteBtn.classList.toggle("is-muted", audio.muted);
muteBtn.setAttribute("aria-label", audio.muted ? "Unmute" : "Mute");
});
audio.addEventListener("error", function () {
bar.remove();
delete audio.dataset.bmEnhanced;
audio.setAttribute("controls", "");
});
}
function scan(root) {
var scope = root || document;
var audios = scope.querySelectorAll(".bm-podcast-player audio");
for (var i = 0; i < audios.length; i++) enhance(audios[i]);
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", function () { scan(); });
} else {
scan();
}
})();3. Beide Dateien laden (WordPress-Beispiel)
Füge das in die functions.php deines Themes ein und passe die Pfade an, wo du die beiden Dateien gespeichert hast. Die Bedingung lädt die Assets nur auf einem Artikel, der wirklich einen Player hat, jede andere Seite bleibt komplett unberührt.
// functions.php
add_action('wp_enqueue_scripts', function () {
global $post;
// This condition only checks for the AUDIO player marker. If you also
// publish podcast video episodes (figure.bm-podcast-video), broaden it to
// "|| strpos($post->post_content, 'bm-podcast-video') !== false" -- the
// CSS snippet above styles both, this JS only enhances audio (video
// embeds already have their own modern player controls).
if (!$post || strpos($post->post_content, 'bm-podcast-player') === false) return;
wp_enqueue_style(
'bm-podcast-player-styles',
get_stylesheet_directory_uri() . '/css/podcast-player.css',
[],
filemtime(get_stylesheet_directory() . '/css/podcast-player.css')
);
wp_enqueue_script(
'bm-podcast-enhancer',
get_stylesheet_directory_uri() . '/js/podcast-enhancer.js',
[],
filemtime(get_stylesheet_directory() . '/js/podcast-enhancer.js'),
['strategy' => 'defer', 'in_footer' => true]
);
});Aktuell bleiben
Das sind Kopien von Blog-Makers eigener kanonischer Quelle. Wir ändern nicht stillschweigend die CSS-Klassennamen oder das Player-Markup (das würde jede Seite brechen, die dieses Snippet kopiert hat), aber liefern wir eine Verbesserung aus, schau hier noch mal vorbei und kopiere neu. Diese Seite zeigt immer den aktuellen Stand.