Lightweight, database-free podcast CMS.
Pure PHP, flat files, RSS feed, stats, customizable themes.
From the Occitan badal [baˈðal] — the opening — the gesture of opening one's mouth wide to speak, to sing, or to stand in awe of a story. Rooted in the Latin batare.
In a world where podcasting is increasingly locked inside proprietary ecosystems and opaque algorithms, Badal is born from a simple idea: what if we gave you back the keys?
No fussy database. No obscure dependencies. No monthly subscription that silently creeps up. Just your files, your server, your voice. Old-school podcasting — with a CMS that isn't.
Homepage: https://robotetdragon.com/badal/
- Zero database — episodes stored as
.mdfiles with YAML frontmatter - RSS feed — fully compatible with Apple Podcasts, Spotify, Pocket Casts, Overcast
- Admin UI — clean dark/light interface with live preview
- Theme system — swappable visual themes as JSON files, create/duplicate/delete from admin
- Home config — tagline, CTA, socials, logo, layout separated from themes (
home.json) - Episode reordering — drag-and-drop in admin, auto-updates RSS feed
- SEO — JSON-LD structured data, Open Graph, Google Podcasts and Podcasting 2.0 tags
- Feed redirect — move your podcast without losing subscribers (
itunes:new-feed-url) - Stats — play counts per episode, interactive charts, CSV/PDF export
- Import — migrate from any existing podcast via RSS feed
- Transcriptions — clickable timestamps and speaker detection
- Tools page — export ZIP, feed redirect, podcast deletion from a dedicated page
- Security — bcrypt, CSRF, rate-limiting, session fingerprinting, CSP headers
- 4 languages — Français, English, Español, Português
- Auto duration — ffprobe extracts audio duration on upload
- Update checker — popup notification when a new version is available on GitHub
- Chapters — Podcasting 2.0 chapters with timestamps, served as JSON for compatible players
- Push notifications — Web Push (VAPID) to notify subscribers of new episodes
- Security dashboard — real-time audit of HTTP headers, file permissions, auth log, rate-limit status, and active sessions
- Password reset — email-based password recovery with rate-limited, hashed tokens (30 min expiry)
- Accurate listen stats — play counts triggered by actual JS play events (not page loads), with IP deduplication (1 listen/IP/episode/24h)
- Docker — ready-to-use
docker compose upfor local development with demo data - Publish animation — micro SVG draw animation when publishing an episode
- PHP 7.4+ (tested on 8.4)
- Apache with
mod_rewrite(or Nginx equivalent) ffproberecommended (for auto duration detection)- No database required
your-domain.com/badal/
├── setup.php
├── .htaccess
├── admin/
├── core/
├── public/
└── ...
Open https://your-domain.com/badal/setup.php in your browser.
- Choose your language (FR / EN / ES / PT)
- Set your admin username and password
- Badal auto-detects your base URL
No config file to edit.
rm setup.phphttps://your-domain.com/badal/admin/
badal/
├── .htaccess Apache rewrite rules
├── setup.php Installation wizard
├── index.php Fallback router
├── config/
│ ├── config.php Generated by setup
│ ├── home.json Home page config (tagline, socials, layout)
│ └── stats.json Play counts
├── themes/ Visual themes (JSON files)
│ ├── sombre.json
│ ├── nuit-bleue.json
│ ├── papier.json
│ └── ...
├── core/
│ ├── Auth.php Login, sessions, password reset
│ ├── EpisodeParser.php Parse/write Markdown + YAML episodes
│ ├── HomeManager.php Home page config (home.json)
│ ├── RssGenerator.php RSS 2.0 feed generation
│ ├── SitemapGenerator.php XML sitemap
│ ├── StatsManager.php Per-episode listen tracking
│ ├── ThemeManager.php JSON theme management
│ ├── ChaptersManager.php Podcasting 2.0 chapters
│ ├── TranscriptManager.php Episode transcripts
│ ├── WebPush.php Web Push (VAPID) notifications
│ ├── Security.php CSRF, rate-limit, CSP, audit
│ ├── AudioDuration.php ffprobe duration extraction
│ ├── Telemetry.php Anonymous usage stats
│ ├── Version.php Update checker (GitHub)
│ ├── Lang.php i18n (FR, EN, ES, PT)
│ └── bootstrap.php Autoloader, config, sessions, headers
├── lang/ fr.php, en.php, es.php, pt.php
├── admin/ Admin interface (episodes, stats, tools, security, push)
├── public/ Public pages (home, episode, RSS, sitemap, stats-record)
├── content/episodes/ Episode Markdown files
└── audio/ Uploaded audio files and media
---
title: My First Episode
date: 2026-01-08
duration: 45:30
description: Short description for RSS.
guest: Jane Doe
audio: my-first-episode/audio.mp3
cover: my-first-episode/cover.jpg
---
## Show notes
Full **Markdown** content here.
| URL | Description |
|---|---|
/ |
Home — featured latest + episode list |
/episodes/{slug} |
Episode page with audio player |
/rss.xml |
RSS feed |
/sitemap.xml |
XML sitemap |
/chapters/{slug}.json |
Podcasting 2.0 chapters (JSON) |
/admin/ |
Admin dashboard |
/admin/security |
Security audit dashboard |
| Feature | Implementation |
|---|---|
| Password | bcrypt cost 12 |
| Session | 8h expiry, UA fingerprint |
| CSRF | Token on all POST forms |
| Rate limiting | 5 attempts / 15 min lockout |
| Upload | finfo MIME validation |
| Headers | CSP, HSTS, X-Frame-Options |
| Directories | Blocked by .htaccess |
| Audio proxy | Path traversal protection |
| Password reset | Email token, SHA-256 hashed, 30 min expiry |
| Security dashboard | Real-time audit (headers, permissions, auth log, rate-limit) |
| Stats dedup | 1 listen / IP / episode / 24h |
| Code | Language |
|---|---|
fr |
Français |
en |
English |
es |
Español |
pt |
Português |
- AJAX import — rewritten for large podcasts (100+ episodes). One request per episode, no timeout, progress bar with elapsed timer, auto-resume
- Import completion popup — animated popup with step-by-step RSS feed redirect instructions
- Feed redirect guide — detailed 6-step migration procedure in the Tools page
- Telemetry: country detection — GeoIP resolution from requester IP, country distribution chart in dashboard
- Telemetry opt-in by default — enabled on new installations
- Enhanced UI feedback —
:activepress states,:focus-visible, hover glow, card lift, input focus ring, custom scrollbar across all pages - Show notes editor — replaced EasyMDE with plain textarea matching transcript style (full-width, monospace)
- Import bugfix: episode count —
count((array)$channel->item)counted XML children of the first item instead of total episodes - Import bugfix: output buffer — AJAX responses no longer corrupted by PHP warnings
- Config write escaping — apostrophes and backslashes in podcast titles no longer corrupt
config.php - Codebase cleanup — all 35+ files reformatted (4-space indent, section dividers, PHPDoc, aligned variables)
- Accurate listen stats — listen counting moved from the audio proxy to a dedicated JS endpoint (
stats-record.php), triggered on actual play events. Bots andpreload="metadata"no longer inflate stats - IP deduplication — max 1 listen per IP per episode per 24-hour window, with automatic deduplication file purge
- Larger show notes editor — EasyMDE height increased from 340px to 600px in both create and edit pages
- Import bugfixes — XSS protection in import logs, crash fix on malformed XML (
libxml_get_last_errorreturning false), slug collision avoidance (episodes with duplicate titles no longer overwrite each other) - RSS feed: description field — imported episodes now include a
descriptionin frontmatter, fixing empty<description>tags in the generated feed - RSS feed: content:encoded —
<content:encoded>now uses the full show notes HTML instead of the short description - RSS feed: per-episode explicit —
<itunes:explicit>now respects each episode's explicit flag instead of being hardcoded tofalse - Cover import improved — added fallback to
media:thumbnailandmedia:content(Spotify, Audioboom, etc.) whenitunes:imageis missing - Episode ordering — import now resets any custom order (
episodes_order.json) so episodes sort by publication date - Dynamic .htaccess —
setup.phpnow generates.htaccesswith the correctRewriteBasederived from the detectedbase_url, fixing broken episode links and missing cover images on non-/badal/deployments
- Draft episodes — save episodes as drafts before publishing, with draft/publish toggle in editor and "BROUILLON" badge in episode list
- Email social link — new Email field in social networks, renders as
mailto:link with envelope icon - Mobile responsive redesign — episode cards switch to vertical 2-column grid on mobile (home + episode pages), sticky table headers, scrollable sidebar, single-column account layout
- Sticky table headers —
theadsticks to top with background on scroll for better readability
- Unit tests — PHPUnit test suite (107 tests) covering all core classes
- English codebase — all comments translated from French to English
- Mobile stats layout — episode KPIs wrap below cover and title on small screens
- YAML frontmatter fix —
addslashes()replaced with proper YAML escaping (no more\'in descriptions) - Stats default to 7 days — period selector defaults to 7-day view instead of 30
- Sidebar footer reorder — My Account, GitHub, Logout
- Update popup link — now points to the correct GitHub repository URL
- Chapters (Podcasting 2.0): timestamped chapters per episode, JSON endpoint
- Push notifications: Web Push (VAPID) for new episode alerts
- Docker Compose: dev environment with PHP 8.2, Apache, ffmpeg, demo podcast
- Publish animation: micro SVG draw effect + "L'épisode est en ligne"
- Stats auto-backup: automatic restore if stats.json is lost during update
- Episode covers in edit page
- Fix Docker RewriteBase for any subdirectory
- New routes: chapters JSON, push subscribe, push notification
- All 4 language files updated (FR, EN, ES, PT)
- Theme system: visual themes as separate JSON files, create/duplicate/delete from admin
- Home config separated into
config/home.json(newHomeManagerclass) - Episode drag-and-drop reordering with automatic RSS feed update
- SEO: JSON-LD structured data (Article, AudioObject, WebSite, ItemList, BreadcrumbList)
- RSS: Google Play and Podcasting 2.0 namespaces, per-episode cover, generator tag
- Feed redirect support via
itunes:new-feed-urlandpodcast:previousUrl - New dedicated tools page (export, feed redirect, podcast deletion)
- Update popup: checks GitHub for new versions, modal notification on all admin pages
- Sidebar: "View site" at top, GitHub link in footer, better mobile spacing
- RSS builder: Apple validation above metadata, responsive two-column layout
- About page updated with all new features
- All 4 language files updated (FR, EN, ES, PT)
- Share buttons (Web Share API on mobile, clipboard copy on desktop)
- Open Graph & Twitter Card meta tags for link previews
- Social networks: added Website, LinkedIn, TikTok, Pocket Casts
- Full i18n for public pages (home, episode, about)
- Fix telemetry: cURL instead of fsockopen, no more silent failures
- Fix font weight not saving (PHP strict comparison bug)
- Fix episode page using hardcoded theme colors
- Fix cover image and OG URLs in subdirectory installs
- Fix footer logo visibility on light themes
- Fix stats counting logo/favicon as episode plays
- ZIP export of podcast (episodes, audio, covers, transcripts, config)
- Delete podcast with double-confirmation modal
- Setup success page with Badal logo SVG stroke animation
- Fix RSS feed: XML-escape all URLs and attributes
- Fix RSS feed: encode audio path segments individually (subdirectory paths)
- Fix RSS feed: normalize base_url (no more double slashes)
- Fix RSS feed: escape duration and episode number fields
- Fix RSS feed: skip episodes with missing audio files
- Fix image upload (allowed extensions parameter)
- Fix footer RSS/Admin links visible with custom footer text
- Telemetry endpoint and interval updated
- License changed from MIT to GPL v2 or later
- Badal calligraphic logo (SVG) in footer and admin sidebar
- Favicon on all pages
- About page with etymology and project description
- Fix setup.php crash (escaped apostrophes)
- Fix content/audio directory paths
- Fix .htaccess RewriteBase
- Graceful redirect to setup when config is missing
- Image MIME types in audio proxy
- Robot & Dragon logo with dark/light theme support
- Initial release
Created by Robot & Dragon
Development assisted by AI (Claude). All code is reviewed and validated by the maintainer.
GPL v2 or later — see LICENSE