Skip to content

robotetdragon/badal-cms

Badal

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.

Version PHP License


Homepage: https://robotetdragon.com/badal/


Features

  • Zero database — episodes stored as .md files 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 up for local development with demo data
  • Publish animation — micro SVG draw animation when publishing an episode

Installation

Requirements

  • PHP 7.4+ (tested on 8.4)
  • Apache with mod_rewrite (or Nginx equivalent)
  • ffprobe recommended (for auto duration detection)
  • No database required

1. Upload files to your server

your-domain.com/badal/
├── setup.php
├── .htaccess
├── admin/
├── core/
├── public/
└── ...

2. Run setup

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.

Capture d’écran 2026-03-24 à 11 41 02

3. Delete setup.php

rm setup.php

4. Access admin

https://your-domain.com/badal/admin/

Capture d’écran 2026-03-24 à 11 41 48

Project structure

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

Episode format

---
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.
Capture d’écran 2026-03-24 à 11 42 27

Public URLs

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
Capture d’écran 2026-03-24 à 11 41 26

Security

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
Capture d’écran 2026-03-24 à 11 42 45

Internationalization

Code Language
fr Français
en English
es Español
pt Português

Changelog

0.6

  • 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:active press 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 countcount((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)

0.52

  • Accurate listen stats — listen counting moved from the audio proxy to a dedicated JS endpoint (stats-record.php), triggered on actual play events. Bots and preload="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

0.51

  • Import bugfixes — XSS protection in import logs, crash fix on malformed XML (libxml_get_last_error returning false), slug collision avoidance (episodes with duplicate titles no longer overwrite each other)
  • RSS feed: description field — imported episodes now include a description in 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 to false
  • Cover import improved — added fallback to media:thumbnail and media:content (Spotify, Audioboom, etc.) when itunes:image is missing
  • Episode ordering — import now resets any custom order (episodes_order.json) so episodes sort by publication date
  • Dynamic .htaccesssetup.php now generates .htaccess with the correct RewriteBase derived from the detected base_url, fixing broken episode links and missing cover images on non-/badal/ deployments

0.5

  • 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 headersthead sticks to top with background on scroll for better readability

0.4

  • 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 fixaddslashes() 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

0.3

  • 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)

0.2

  • Theme system: visual themes as separate JSON files, create/duplicate/delete from admin
  • Home config separated into config/home.json (new HomeManager class)
  • 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-url and podcast: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)

0.13

  • 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

0.12

  • 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

0.11

  • 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

0.1

  • Initial release

Credits

Created by Robot & Dragon

Development assisted by AI (Claude). All code is reviewed and validated by the maintainer.

License

GPL v2 or later — see LICENSE

Packages

 
 
 

Contributors

Languages