RadioCMS DEV
Getting Started Plugins Themes Reference DE

# 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

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

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.

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

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

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

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

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

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?

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

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:

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"

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

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

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