Markdown Converter
Agent skill for markdown-converter
A modern, embeddable podcast player that loads multiple podcasts from a master RSS feed. Features dark/light themes, custom dropdown UI, modal player overlay, and PHP proxy for CORS-free embedding anywhere.
Sign in to like and favorite skills
A modern, embeddable podcast player that loads multiple podcasts from a master RSS feed. Features dark/light themes, custom dropdown UI, modal player overlay, and PHP proxy for CORS-free embedding anywhere.
Tech Stack: Vanilla JS, CSS3 (Material Design), HTML5 Audio API, PHP 7.0+ (proxy only)
The player uses a two-tier feed architecture for performance:
feed.php) contains metadata for ~20 podcasts (title, cover, episode count)// In parseMasterFeed(): Extract ALL podcast metadata instantly // Load first podcast episodes immediately // Background-load remaining podcasts without blocking UI
Critical workflow: Never refetch master feed unnecessarily. Episodes are cached in
state.podcasts[index].episodes after first load.
Feed fetching tries multiple strategies in order:
proxy.php) - preferred, server-side fetchSee
fetchWithFallback() in script.js. Each proxy has different response formats - handle accordingly.
Global
state object tracks:
podcasts[] - full podcast data including episodescurrentPodcast - selected podcast objectcurrentEpisode - loaded episodeisPlaying - audio statecurrentSpeed - playback rateSeparate
modalState for player overlay visibility.
Pattern: Always update state first, then sync UI from state. Don't read DOM as source of truth.
Never run from
protocol. CORS policies block all external requests from file:// origins.file://
# Start local server before testing php -S localhost:8000 # or: python3 -m http.server 8000 # or: npx http-server -p 8000
Check protocol in code:
if (window.location.protocol === 'file:') { showError('Please run from http://localhost...'); }
Never load all podcasts synchronously. User sees frozen UI for 40-60 seconds.
Correct approach:
// 1. Load master feed (1s) // 2. Show first podcast immediately (3s total) // 3. Background-load others in loadRemainingPodcastsInBackground() // 4. Update dropdown as each loads
See
COMPLETE-AUDIT.md for full architectural explanation.
Theme stored in localStorage with key
'podcast-player-theme'. Applied via data-theme attribute on <html>:
:root { --primary: #BB86FC; /* dark mode */ } [data-theme="light"] { --primary: #6750A4; }
Always use CSS custom properties for colors, never hardcoded hex values. Toggle preserves user preference across sessions.
The podcast selector uses a custom-built dropdown, not
<select>:
.dropdown-selected - visible clickable element.dropdown-options - absolutely positioned list (z-index: 10000)<select> remains for form compatibilityWhen updating: Sync both custom dropdown AND hidden select. See
populatePodcastDropdown().
If using master feed system, add to server's
feed.php. If hardcoding:
// In parseMasterFeed() or use static list: const podcasts = [ { url: 'https://example.com/feed.xml', title: 'New Show', id: 0 } ];
http://localhost, not file://proxy.php exists and is accessibleproxy.php:$allowedDomains = [ 'podcast.supersoul.top', 'your-new-domain.com', // Add here ];
Spacing: Use CSS custom properties
--space-1 through --space-6 (4px to 24px scale)
Typography:
font-family: 'Oswald' (600-700 weight, uppercase)font-family: 'Inter' (400-600 weight)Icons: Font Awesome 6.5.1 (CDN loaded in
<head>)
Player auto-detects base URL and uses same-origin
proxy.php:
const baseUrl = window.location.origin + window.location.pathname.replace(/[^/]*$/, ''); const proxyUrl = `${baseUrl}proxy.php?url=${encodeURIComponent(feedUrl)}`;
Deployment requirement: Must upload
proxy.php along with HTML/CSS/JS. Works on any PHP host (7.0+) with cURL enabled.
Testing embeds locally:
<!-- In test-embed.html --> <iframe src="http://localhost:8000?podcast=0&episode=2" width="100%" height="650"></iframe>
?podcast=0 - Load specific podcast by index?episode=5 - Load specific episode by index?podcast=0&episode=5Parsed in
loadFromUrlParams() function.
index.html - Main player interface, sticky header/selector layoutscript.js - All logic: feed parsing, audio controls, state management (~900 lines)styles.css - Material Design dark/light themes, custom dropdown, compact spacingproxy.php - Server-side CORS proxy with domain whitelisttest-embed.html - Iframe embed testing page with theme toggleDocumentation files (read when troubleshooting):
COMPLETE-AUDIT.md - Architecture, performance, lazy loading rationaleplayer-errors.md - CORS troubleshooting, common issuesEMBED-SOLUTION.md - Deployment guide, embed code examplesTHE-REAL-PROBLEM.md - Feed URL validation, testing toolsPlayer opens in fixed-position overlay (
.player-modal) at bottom of screen:
playEpisodeInModal()Important: Audio element is global (
#audio-element), not duplicated in modal. Modal controls sync with same <audio>.
Two progress overlays:
.progress-buffered - shows how much is loaded.progress-filled - shows playback positionBoth animate width as percentages. Slider (
input[type="range"]) is invisible overlay for seeking.
Update in
timeupdate event → updateProgress() function.
Episodes only load when podcast is selected. Check before fetching:
if (state.currentPodcast.episodes.length === 0) { // First time - load now await loadSinglePodcast(state.currentPodcast); }
Prevents loading 20 feeds × 10-50 episodes each on initial page load.
With 20+ podcasts, dropdown uses:
max-height: 400px; overflow-y: auto)Before committing changes:
http://localhost:8000 (not file://)test-embed.htmldecodeHtmlEntities() before displayplayer-errors.md, verify RSS URL is correct (not admin page)COMPLETE-AUDIT.md'podcast-player-theme'const/let, never varfa-solid fa-play)Project Status: Production-ready. Main player works perfectly with standard RSS feeds. Embed system tested and functional. See README.md for deployment guide.