RadioCMS DEV

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:

Community first

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:

plugins/my-plugin/plugin.json
{
 "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"
}
plugins/my-plugin/index.php
<?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

FieldTypeRequiredDescription
idstringUnique ID, [a-z0-9-]+
namestringDisplay name
versionstringSemVer (e.g. 1.0.0)
authorstringAuthor
descriptionstring-Short description
mainstring-Entry file (default: index.php)
enabledbool-Default status (used on install)
requiresstring-Minimum RadioCMS version
author_urlstring-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.

Example, inject something into the footer
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

HookLocationArgs
wp_headBefore </head>-
footerBefore </body>-
home_topHomepage, before hero-
home_after_playerHomepage, after hero-
home_bottomHomepage, before footer-
More hooks coming

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
Example, extend the station name
radiocms_add_filter('station_name', function(string $name): string {
 return $name . ' 🎵';
});
Example, add a navbar entry
radiocms_add_filter('navbar_items', function(array $items): array {
 $items[] = [
 'label' => 'Blog',
 'url' => '/blog.php',
 'slug' => 'blog',
 ];
 return $items;
});

Available filters

HookInputDescription
station_namestringRadio station name
navbar_itemsarrayAdditional 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
Example, weather widget
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

PositionWhere it renders
home_topHomepage, top
home_after_playerHomepage, after player
home_bottomHomepage, before footer
footerFooter (every page)
admin_dashboardAdmin 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:

Replace a module with a plugin

Want to replace the built-in schedule with your own?

  1. In the admin under Modules, disable the schedule module
  2. In your plugin, provide your own endpoint, e.g. /my-schedule.php or via a plugin router
  3. Add a custom nav entry via the navbar_items filter
plugin.php, example: custom schedule
<?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 IDFeature
scheduleSchedule
newsNews
djsDJs
teamTeam
partnersPartners
requestsSong requests
podcastsPodcasts
chatChat
applicationsDJ applications
contactContact

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

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:

style.css, simplest theme
: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:

CheckError 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"
Security

PHP files are blocked for security. The /themes/ folder also has an .htaccess that disables PHP execution, defense in depth.

API Reference

Plugin functions

FunctionDescription
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): boolIs a given plugin active?
radiocms_log_plugin_error($context, $msg)Log your own error

Module functions

FunctionDescription
radiocms_module_active($id): boolIs a core module active?
radiocms_require_module($id)404 if module is disabled (for page guard)
radiocms_get_core_modules(): arrayList of all core modules

Theme functions

FunctionDescription
radiocms_get_active_theme(): stringID of the currently active theme
radiocms_get_theme_url(): stringURL path to active theme
radiocms_get_all_themes(): arrayList of installed themes

Useful core functions

FunctionDescription
getSetting($key, $default=''): stringRead a setting from the settings table
getDB(): PDODatabase connection (PDO)
t($key): stringTranslation (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

plugins/greetings/plugin.json
{
 "id": "greetings",
 "name": "Greetings",
 "version": "1.0.0",
 "author": "You",
 "description": "Shows a greeting in the footer.",
 "main": "index.php"
}

3. Logic

plugins/greetings/index.php
<?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.

plugins/listener-stats/index.php
<?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

  1. Zip the sunset/ folder contents (important: zip the contents, not the folder itself)
  2. Upload in admin under Themes
  3. Activate & done
Tip

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.zip

Theme template

theme.json + style.css with all CSS variables and commented examples.

theme-template.zip

Ready to start?

Download RadioCMS, grab a starter template and build your first plugin in 10 minutes.

Download RadioCMS