Developer Documentation
Everything you need to build plugins, themes and custom modules for RadioCMS Community Edition. The system is inspired by WordPress, actions, filters and widgets, but much lighter.
Plugins
Extend RadioCMS with new features, without touching the core.
Modules
Disable core features and replace them with your own plugins.
Themes
Custom designs via CSS, no PHP, no fear of breaking pages.
Safe by default
Plugin crashes never take down the site, errors are caught.
Overview
RadioCMS Community Edition is an open-source CMS tailored for internet radio stations, MIT-licensed. Free to use, modify and redistribute, commercially too.
The extension system has three pieces:
- Plugins, PHP code that hooks into actions and filters
- Modules, core features that can be toggled on/off
- Themes, CSS-based designs that are simply uploaded
Plugins and themes are meant to be shared. When you build something nice, publish it on the community Discord or GitHub.
Getting Started
A minimal plugin needs only two files:
{
"id": "my-plugin",
"name": "My Plugin",
"version": "1.0.0",
"description": "Short description.",
"author": "Your Name",
"main": "index.php",
"enabled": false,
"requires": "1.0.0"
}
<?php
// Print something in the footer
radiocms_add_action('footer', function() {
echo '<!-- My plugin is active -->';
});
That's it. Upload the folder, activate under Plugins in the admin, done.
Architecture
RadioCMS loads plugins during initialization in config/config.php. Each plugin is loaded inside a try/catch, a broken plugin cannot take down the site.
config.php
├─ database.php (getDB)
├─ themes.php (theme helpers)
└─ plugins.php
└─ radiocms_load_plugins()
├─ plugins/a/plugin.json → try: require index.php
├─ plugins/b/plugin.json → catch: log, skip
└─ plugins/c/plugin.json → try: require index.php
Once loaded, all registered actions, filters and widgets are in RAM. When hook points are hit in the frontend, callbacks are executed (also in try/catch).
What is a plugin?
A plugin is a folder under /plugins/ containing at least a plugin.json and a main PHP file (usually index.php).
Plugin structure
plugins/
└── my-plugin/
├── plugin.json # Metadata (required)
├── index.php # Entry point (required)
├── assets/ # CSS/JS/images (optional)
│ └── style.css
└── includes/ # Own helpers (optional)
└── helper.php
plugin.json, fields
| Field | Type | Required | Description |
|---|---|---|---|
id | string | ✓ | Unique ID, [a-z0-9-]+ |
name | string | ✓ | Display name |
version | string | ✓ | SemVer (e.g. 1.0.0) |
author | string | ✓ | Author |
description | string | - | Short description |
main | string | - | Entry file (default: index.php) |
enabled | bool | - | Default status (used on install) |
requires | string | - | Minimum RadioCMS version |
author_url | string | - | Author website |
Actions Action
Actions are event hooks. You register a callback that runs at a specific point (e.g. in the footer, after the player, …).
Register
radiocms_add_action(string $hook, callable $callback, int $priority = 10): void
Lower $priority = earlier. Default is 10.
radiocms_add_action('footer', function() {
echo '<script>console.log("Plugin loaded");</script>';
});
Fire (core only)
radiocms_do_action(string $hook, mixed ...$args): void
The core calls these at defined points, you normally don't fire them yourself.
Available actions
| Hook | Location | Args |
|---|---|---|
wp_head | Before </head> | - |
footer | Before </body> | - |
home_top | Homepage, before hero | - |
home_after_player | Homepage, after hero | - |
home_bottom | Homepage, before footer | - |
We extend the hook list continuously. Need a specific hook? Let us know, sensible ones get added.
Filters Filter
Filters transform a value. They receive an input, your callback returns a (possibly modified) value.
radiocms_add_filter(string $hook, callable $callback, int $priority = 10): void
radiocms_add_filter('station_name', function(string $name): string {
return $name . ' 🎵';
});
radiocms_add_filter('navbar_items', function(array $items): array {
$items[] = [
'label' => 'Blog',
'url' => '/blog.php',
'slug' => 'blog',
];
return $items;
});
Available filters
| Hook | Input | Description |
|---|---|---|
station_name | string | Radio station name |
navbar_items | array | Additional navbar entries |
Widgets
Widgets are UI blocks admins can freely place at positions (footer, homepage, …). Your plugin registers them, the admin decides where they appear.
radiocms_register_widget(
string $id,
string $name,
callable $render,
array $options = []
): void
radiocms_register_widget(
id: 'weather-widget',
name: 'Weather',
render: function() {
echo '<div class="card p-3">';
echo '<h5>🌤️ 22°C in Berlin</h5>';
echo '<p class="text-muted">Sunny, light breeze</p>';
echo '</div>';
},
options: [
'description' => 'Shows current weather.',
'icon' => 'bi-cloud-sun',
'plugin' => 'weather-plugin',
]
);
The render callback can output any HTML, Bootstrap, custom CSS, JS snippets, whatever.
Widget positions
| Position | Where it renders |
|---|---|
home_top | Homepage, top |
home_after_player | Homepage, after player |
home_bottom | Homepage, before footer |
footer | Footer (every page) |
admin_dashboard | Admin dashboard |
Error handling
You don't need to worry about try/catch, the core catches all errors. If your plugin throws an exception, it gets logged and skipped.
Error log: /logs/plugin_errors.log
[2026-04-16 14:32:01] [PLUGIN:action:footer] Undefined variable $foo
In the admin under Plugins you can see plugin errors directly.
Core modules
Core modules are the built-in features: schedule, news, DJs, team, partners, song requests, podcasts, chat, DJ applications, contact.
Admins can enable/disable each under Admin → Modules. A disabled module:
- Is hidden from the frontend navigation
- Is hidden from the admin sidebar
- Returns 404 when its URL is hit directly
- Is hidden from footer links
Replace a module with a plugin
Want to replace the built-in schedule with your own?
- In the admin under Modules, disable the
schedulemodule - In your plugin, provide your own endpoint, e.g.
/my-schedule.phpor via a plugin router - Add a custom nav entry via the
navbar_itemsfilter
<?php
// Custom navbar entry
radiocms_add_filter('navbar_items', function(array $items): array {
if (!radiocms_module_active('schedule')) {
// Only if core module is disabled
$items[] = [
'label' => 'Schedule',
'url' => '/plugins/my-schedule/view.php',
'slug' => 'schedule',
];
}
return $items;
});
All core modules
| Module ID | Feature |
|---|---|
schedule | Schedule |
news | News |
djs | DJs |
team | Team |
partners | Partners |
requests | Song requests |
podcasts | Podcasts |
chat | Chat |
applications | DJ applications |
contact | Contact |
Theme development
Themes in RadioCMS are intentionally minimalistic: only CSS and images, no PHP. That way themes can never break the site.
Theme structure
my-theme.zip
├── theme.json # Metadata (required)
├── style.css # CSS overrides (required)
├── screenshot.png # Preview image (optional, 16:9 recommended)
└── assets/ # Images, fonts (optional)
├── logo.png
└── background.jpg
theme.json
{
"id": "my-theme",
"name": "My Theme",
"version": "1.0.0",
"author": "Your Name",
"author_url": "https://example.com",
"description": "Dark theme with orange accents.",
"requires": "1.0.0",
"tags": ["dark", "orange", "modern"]
}
CSS variables
RadioCMS uses CSS custom properties for all colors. Your theme just overrides them:
:root {
--primary: #ff6b35; /* primary color (e.g. buttons) */
--primary-dark: #e0551d; /* hover color */
--secondary: #ffd23f; /* accent color */
--bg-primary: #0d0d1a; /* main background */
--bg-card: #1a1a2e; /* card background */
--text-main: #ffffff; /* main text color */
--text-muted: #94a3b8; /* secondary text */
--border-color: #2a2f42; /* borders */
}
/* Navbar logo in theme color */
.navbar-brand {
color: var(--primary) !important;
}
/* Hero section with gradient */
.hero-section {
background: linear-gradient(135deg, var(--primary) 0%, var(--secondary) 100%);
}
Upload & validation
Admins upload your theme as a ZIP under Admin → Themes. RadioCMS validates strictly and shows a clear error when something is off:
| Check | Error message on violation |
|---|---|
Only .zip | "File must be a .zip" |
| Max 5 MB | "ZIP too large: 12 MB. Max: 5 MB" |
theme.json present | "theme.json missing in ZIP archive" |
| Valid JSON | "theme.json is not valid JSON: …" |
| Required fields | "theme.json missing required field: author" |
| ID pattern | "Theme ID 'My Theme!' is invalid" |
style.css present | "style.css missing in ZIP archive" |
| No PHP files | "Disallowed file in ZIP: evil.php" |
No .htaccess | "Disallowed file in ZIP: .htaccess" |
| No path traversal | "Suspicious file path in ZIP: ../../../etc/passwd" |
PHP files are blocked for security. The /themes/ folder also has an .htaccess that disables PHP execution, defense in depth.
API Reference
Plugin functions
| Function | Description |
|---|---|
radiocms_add_action($hook, $cb, $priority=10) | Register an action hook |
radiocms_do_action($hook...$args) | Run all actions for a hook (core) |
radiocms_add_filter($hook, $cb, $priority=10) | Register a filter hook |
radiocms_apply_filters($hook, $value...$args) | Apply filter (core) |
radiocms_register_widget($id, $name, $render, $options=[]) | Register a widget |
radiocms_render_widgets($position) | Render all widgets at a position (core) |
radiocms_plugin_loaded($id): bool | Is a given plugin active? |
radiocms_log_plugin_error($context, $msg) | Log your own error |
Module functions
| Function | Description |
|---|---|
radiocms_module_active($id): bool | Is a core module active? |
radiocms_require_module($id) | 404 if module is disabled (for page guard) |
radiocms_get_core_modules(): array | List of all core modules |
Theme functions
| Function | Description |
|---|---|
radiocms_get_active_theme(): string | ID of the currently active theme |
radiocms_get_theme_url(): string | URL path to active theme |
radiocms_get_all_themes(): array | List of installed themes |
Useful core functions
| Function | Description |
|---|---|
getSetting($key, $default=''): string | Read a setting from the settings table |
getDB(): PDO | Database connection (PDO) |
t($key): string | Translation (only inside callbacks, not at plugin load time!) |
Hook overview (cheatsheet)
Actions
// ── Frontend ──
radiocms_add_action('wp_head', fn() => /* CSS/JS in head */);
radiocms_add_action('footer', fn() => /* Scripts at end */);
radiocms_add_action('home_top', fn() => /* Top of homepage */);
radiocms_add_action('home_after_player', fn() => /* After player */);
radiocms_add_action('home_bottom', fn() => /* Before footer */);
Filters
radiocms_add_filter('station_name', fn(string $n) => $n);
radiocms_add_filter('navbar_items', fn(array $items) => $items);
Tutorial: My first plugin
Let's build a plugin that prints a greeting in the footer.
1. Create folder
plugins/
└── greetings/
├── plugin.json
└── index.php
2. Plugin manifest
{
"id": "greetings",
"name": "Greetings",
"version": "1.0.0",
"author": "You",
"description": "Shows a greeting in the footer.",
"main": "index.php"
}
3. Logic
<?php
radiocms_add_action('footer', function() {
$hour = (int)date('H');
if ($hour < 12) $greet = '🌅 Good morning';
elseif ($hour < 18) $greet = '☀️ Good afternoon';
else $greet = '🌙 Good evening';
echo '<div class="alert alert-info text-center m-3">';
echo htmlspecialchars($greet) . ', glad you\'re here!';
echo '</div>';
});
4. Activate in admin
Go to Admin → Plugins, find "Greetings", click Activate. Done.
Tutorial: Build a widget
Let's build a listener-stats widget showing the current listener count.
<?php
radiocms_register_widget(
id: 'listener-stats',
name: 'Listener stats',
render: function() {
// Read listener count from Icecast status
$status = @file_get_contents('http://localhost:8000/status-json.xsl');
$data = @json_decode($status, true);
$listeners = $data['icestats']['source']['listeners'] ?? '?';
?>
<div class="card bg-dark text-white p-3 mb-3">
<div class="d-flex align-items-center gap-3">
<i class="bi bi-people-fill fs-2 text-primary"></i>
<div>
<div class="small text-muted">Listening now</div>
<div class="fw-bold fs-4"><?= htmlspecialchars((string)$listeners) ?></div>
</div>
</div>
</div>
<?php
},
options: [
'description' => 'Shows current listener count.',
'icon' => 'bi-people-fill',
'plugin' => 'listener-stats',
]
);
The admin goes to Plugins → Widgets, selects the widget, picks a position (e.g. footer), and saves.
Tutorial: Custom theme
Let's build a simple "Sunset" theme with orange accents.
1. Create files
sunset/
├── theme.json
├── style.css
└── screenshot.png
2. theme.json
{
"id": "sunset",
"name": "Sunset",
"version": "1.0.0",
"author": "You",
"description": "Warm theme with orange and yellow tones.",
"tags": ["warm", "orange", "sunset"]
}
3. style.css
:root {
--primary: #ff6b35;
--primary-light: #ff9b7a;
--secondary: #ffd23f;
--bg-primary: #1a0f0a;
--bg-card: #2d1810;
--border-color: #4a2817;
}
/* Hero with sunset gradient */
.hero-section {
background:
linear-gradient(180deg, rgba(26, 15, 10, 0.4), rgba(26, 15, 10, 0.9)),
linear-gradient(135deg, #ff6b35, #ffd23f);
}
/* Warm buttons */
.btn-primary-radio.btn-hero-primary {
background: linear-gradient(135deg, var(--primary), var(--secondary));
color: #1a0f0a;
font-weight: 700;
}
/* Orange logo */
.brand-icon i { color: var(--primary); }
/* Player accents */
.player-play-btn {
background: linear-gradient(135deg, var(--primary), var(--secondary));
color: #1a0f0a;
}
4. Zip & upload
- Zip the
sunset/folder contents (important: zip the contents, not the folder itself) - Upload in admin under
Themes - Activate & done
A screenshot.png (1280×720 or 16:9) makes your theme look much better in the admin.
FAQ
Is RadioCMS Community really completely free?
Yes, MIT license. You can use, modify and distribute it for free, including commercially.
Can I sell plugins?
Absolutely. Your plugins are yours, you decide on the license.
Who builds custom plugins on request?
The RadioCMS author offers this as a paid service, see radio.dgnshop.com. The community is growing too.
Can I have multiple themes at once?
Installed: yes. Active: only one. You can switch anytime.
What happens when my active theme breaks?
RadioCMS automatically falls back to the default theme. Your site stays online.
Can I define hooks in my own templates?
Yes, use radiocms_do_action('my_hook') in your plugin templates. Other plugins can hook in.
Why no PHP files in themes?
Security. A broken PHP theme could crash the site or become an attack vector. CSS alone covers 99% of design needs, for complex logic, use a plugin.
Help & support
GitHub
File issues, send pull requests, browse the source.
Community Discord
Chat with other developers & radio operators. (soon)
Custom plugins
Need a specific feature? Custom plugins built on request.
Sample plugin
In the install: plugins/hello-world/, starter to copy.
Starter templates for download
Ready-to-use boilerplates with example code and comments, download the zip, customize, ship.
Plugin template
plugin.json + index.php with action, filter and widget examples as comments.
plugin-template.zipTheme template
theme.json + style.css with all CSS variables and commented examples.
theme-template.zipReady to start?
Download RadioCMS, grab a starter template and build your first plugin in 10 minutes.
Download RadioCMS