Plugin Development
Learn to extend VoidForge CMS with custom plugins. Build powerful features using hooks, filters, shortcodes, and REST APIs.
Introduction
VoidForge CMS features a powerful plugin system that lets you extend functionality without modifying core files. Plugins can add new features, modify behavior, and integrate with external services.
🪝 Hooks & Filters
90+ hooks to extend every part of the CMS
⚡ Shortcodes
Embed dynamic content in posts and pages
🔌 REST API
Register custom API endpoints easily
⚙️ Settings API
Built-in configuration management
Plugin Structure
Each plugin lives in its own folder within /content/plugins/. The folder name becomes the plugin's unique identifier.
my-plugin/my-plugin.php).
Your First Plugin
Step 1: Create the Folder
Create a new folder in /content/plugins/:
content/plugins/hello-world/
Step 2: Create the Main File
Create hello-world.php with a plugin header:
<?php
/**
* Plugin Name: Hello World
* Description: A simple example plugin
* Version: 1.0.0
* Author: Your Name
*/
defined('CMS_ROOT') or die('Direct access not allowed');
// Add a greeting to the footer
Plugin::addAction('vf_footer', function() {
echo '<p>Hello from my plugin!</p>';
});
Step 3: Activate
Go to Admin → Plugins and click Activate.
Actions
Actions execute custom code at specific points in the CMS lifecycle.
Plugin::addAction('hook_name', $callback, $priority = 10);
// Example: Send email when post is published
Plugin::addAction('post_status_published', function($post, $oldStatus) {
mail('[email protected]', 'New Post', $post['title'] . ' was published');
});
Triggering Actions
Plugin::doAction('my_custom_action', $arg1, $arg2);
Filters
Filters modify data as it passes through the CMS. Unlike actions, filters must return a value.
Plugin::addFilter('filter_name', $callback, $priority = 10);
// Example: Add reading time to content
Plugin::addFilter('the_content', function($content, $post) {
$words = str_word_count(strip_tags($content));
$minutes = ceil($words / 200);
return "<p>📖 {$minutes} min read</p>" . $content;
});
Shortcodes
Embed dynamic content using simple tags like [shortcode].
Self-Closing
Plugin::addShortcode('year', function() {
return date('Y');
});
// [year] → 2025
With Attributes
Plugin::addShortcode('button', function($atts) {
$atts = array_merge([
'url' => '#',
'text' => 'Click Me'
], $atts);
return "<a href=\"{$atts['url']}\" class=\"btn\">{$atts['text']}</a>";
});
// [button url="/signup" text="Sign Up"]
Enclosing
Plugin::addShortcode('highlight', function($atts, $content) {
$color = $atts['color'] ?? 'yellow';
return "<mark style=\"background:{$color}\">{$content}</mark>";
});
// [highlight color="lime"]Important[/highlight]
Settings API
Plugin::registerSettings('my-plugin', [
'api_key' => [
'type' => 'text',
'label' => 'API Key',
'default' => ''
],
'enabled' => [
'type' => 'checkbox',
'label' => 'Enable Feature',
'default' => true
]
]);
// Get/Set
$key = Plugin::getSetting('my-plugin', 'api_key');
Plugin::setSetting('my-plugin', 'api_key', 'new-value');
AJAX Handlers
Plugin::addAjax('my_action', function() {
if (!verifyCsrf($_POST['csrf'] ?? '')) {
vf_send_json_error(['message' => 'Invalid token']);
}
$result = processData($_POST['data']);
vf_send_json_success(['result' => $result]);
}, true); // true = require login
Admin Pages
Plugin::addAdminPage('my-settings', [
'page_title' => 'My Plugin Settings',
'menu_title' => 'My Plugin',
'capability' => 'admin',
'icon' => 'settings',
'parent' => 'plugins',
'callback' => function() {
include __DIR__ . '/views/settings.php';
}
]);
REST API
Plugin::addAction('rest_api_init', function() {
Plugin::registerRestRoute('my-plugin/v1', 'items', [
'methods' => 'GET',
'callback' => function($request) {
return ['items' => getItems()];
},
'permission_callback' => function() {
return User::isLoggedIn();
}
]);
});
// GET /api/my-plugin/v1/items
Custom Blocks
Anvil is VoidForge's block-based content editor. You can create custom blocks by extending the AnvilBlock base class.
Block Structure
Each block is a separate PHP class that defines its settings, attributes, and rendering logic:
Creating a Block
Create a class extending AnvilBlock:
<?php
/**
* Alert Block - Display alert/notice boxes
*/
defined('CMS_ROOT') or die();
class AlertBlock extends AnvilBlock
{
// Block identifier (used in JSON)
protected static string $name = 'alert';
// Display name in editor
protected static string $label = 'Alert';
// Description shown in block picker
protected static string $description = 'Display an alert or notice box';
// Category: text, media, layout, embed
protected static string $category = 'layout';
// Lucide icon name
protected static string $icon = 'alert-triangle';
// Block attributes with types and defaults
protected static array $attributes = [
'content' => ['type' => 'string', 'default' => ''],
'type' => ['type' => 'string', 'default' => 'info'],
'dismissible' => ['type' => 'boolean', 'default' => false],
];
// Supported features
protected static array $supports = ['className'];
// Render the block HTML
public static function render(array $attrs, array $block): string
{
$classes = self::buildClasses($attrs, 'alert');
$classes[] = 'alert-' . ($attrs['type'] ?? 'info');
if (!empty($attrs['dismissible'])) {
$classes[] = 'is-dismissible';
}
return sprintf(
'<div class="%s">%s</div>',
esc(self::classString($classes)),
self::processInlineContent($attrs['content'] ?? '')
);
}
}
Registering Blocks
Register your block on the anvil_register_blocks hook:
<?php
/**
* Plugin Name: Custom Blocks
* Description: Adds custom Anvil blocks
*/
defined('CMS_ROOT') or die();
// Load the block class
require_once __DIR__ . '/blocks/AlertBlock.php';
// Register on the anvil_register_blocks hook
Plugin::addAction('anvil_register_blocks', function() {
Anvil::registerBlockClass(AlertBlock::class);
});
Block Attributes
Each attribute needs a type and default value:
| Type | Description | Example |
|---|---|---|
string | Text content | ['type' => 'string', 'default' => ''] |
integer | Numeric value | ['type' => 'integer', 'default' => 1] |
boolean | True/false | ['type' => 'boolean', 'default' => false] |
array | List of items | ['type' => 'array', 'default' => []] |
Adding Categories
You can also register custom block categories:
Plugin::addAction('anvil_register_blocks', function() {
// Register a new category
Anvil::registerCategory('commerce', [
'label' => 'Commerce',
'icon' => 'shopping-cart',
'order' => 50,
]);
// Then register blocks in that category
Anvil::registerBlockClass(ProductBlock::class);
});
Styles & Scripts
// Frontend
Plugin::addAction('vf_head', function() {
Plugin::enqueueStyle('my-css', Plugin::url('my-plugin') . '/style.css');
});
Plugin::addAction('vf_footer', function() {
Plugin::enqueueScript('my-js', Plugin::url('my-plugin') . '/script.js');
});
// Admin (page-specific)
Plugin::addAction('admin_enqueue_scripts', function($page) {
if ($page === 'plugins') {
Plugin::enqueueScript('admin-js', Plugin::url('my-plugin') . '/admin.js');
}
});
Database
$pdo = Database::getInstance();
$users = Database::query(
"SELECT * FROM " . Database::table('users') . " WHERE role = ?",
['admin']
);
$count = Database::queryValue(
"SELECT COUNT(*) FROM " . Database::table('posts')
);
All Hooks
VoidForge provides 90+ hooks. Here are the most commonly used:
Core System
| Hook | Type | Description |
|---|---|---|
init | Action | After CMS initializes |
plugins_loaded | Action | After all plugins load |
shutdown | Action | Before script ends |
Content
| Hook | Type | Description |
|---|---|---|
pre_insert_post | Filter | Modify post before creation |
post_inserted | Action | After post created |
post_updated | Action | After post updated |
post_status_changed | Action | When status changes |
the_content | Filter | Modify post content |
the_title | Filter | Modify post title |
anvil_register_blocks | Action | Register custom Anvil blocks |
Users
| Hook | Type | Description |
|---|---|---|
user_logged_in | Action | After successful login |
user_login_failed | Action | After failed login |
authenticate | Filter | Custom auth methods |
Admin
| Hook | Type | Description |
|---|---|---|
admin_init | Action | When admin loads |
admin_menu | Action | When building menu |
admin_head | Action | Inside admin head |
admin_footer | Action | Before admin body close |
admin_notices | Action | Display admin notices |
admin_enqueue_scripts | Action | Enqueue page assets |
Theme/Template
| Hook | Type | Description |
|---|---|---|
vf_head | Action | Inside head tag |
vf_footer | Action | Before body close |
body_class | Filter | Modify body classes |
template_include | Filter | Override template |
REST API
| Hook | Type | Description |
|---|---|---|
rest_api_init | Action | Register routes |
rest_pre_dispatch | Filter | Before handling request |
rest_post_dispatch | Filter | Modify response |
SEO
| Hook | Type | Description |
|---|---|---|
seo_title | Filter | Modify page title |
seo_description | Filter | Modify meta description |
seo_robots | Filter | Modify robots meta |
seo_canonical | Filter | Modify canonical URL |
seo_og_tags | Filter | Modify Open Graph tags |
seo_twitter_tags | Filter | Modify Twitter Card tags |
seo_schema | Filter | Modify JSON-LD schema |
seo_sitemap_urls | Filter | Add URLs to sitemap |
Examples
Social Share Buttons
<?php
/**
* Plugin Name: Social Share
* Version: 1.0.0
*/
defined('CMS_ROOT') or die();
Plugin::addFilter('the_content', function($content, $post) {
if ($post['post_type'] !== 'post') return $content;
$url = urlencode(Post::permalink($post));
$buttons = '<div class="share">
<a href="https://twitter.com/intent/tweet?url=' . $url . '">Tweet</a>
</div>';
return $content . $buttons;
}, 20);
View Counter
<?php
/**
* Plugin Name: View Counter
* Version: 1.0.0
*/
defined('CMS_ROOT') or die();
Plugin::addAction('template_redirect', function() {
global $post;
if (!$post) return;
$views = (int)Post::getMeta($post['id'], 'views', 0);
Post::setMeta($post['id'], 'views', $views + 1);
});
Plugin::addShortcode('views', function() {
global $post;
return number_format(Post::getMeta($post['id'], 'views', 0)) . ' views';
});
Best Practices
Security
- Always verify CSRF tokens on form submissions
- Use prepared statements for all database queries
- Escape output with
esc()to prevent XSS - Check user capabilities before actions
Performance
- Cache expensive operations when possible
- Use appropriate hook priorities (default: 10)
- Only load code when needed
Code Quality
- Prefix all functions with your plugin name
- Include
defined('CMS_ROOT')check - Document hooks for other developers