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.

content/plugins/
my-plugin/
my-plugin.php
assets/, includes/ (optional)
Important The main PHP file must match the folder name (e.g., 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.

Done! Your plugin is now running and will display a message in the footer.

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:

my-plugin/
my-plugin.php
blocks/
AlertBlock.php

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:

TypeDescriptionExample
stringText content['type' => 'string', 'default' => '']
integerNumeric value['type' => 'integer', 'default' => 1]
booleanTrue/false['type' => 'boolean', 'default' => false]
arrayList 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);
});
Default blocks VoidForge includes 15 built-in blocks: paragraph, heading, list, quote, code, table, image, gallery, video, columns, spacer, separator, button, html, and embed.

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')
);
Security Always use prepared statements. Never interpolate user input directly.

All Hooks

VoidForge provides 90+ hooks. Here are the most commonly used:

Core System

HookTypeDescription
initActionAfter CMS initializes
plugins_loadedActionAfter all plugins load
shutdownActionBefore script ends

Content

HookTypeDescription
pre_insert_postFilterModify post before creation
post_insertedActionAfter post created
post_updatedActionAfter post updated
post_status_changedActionWhen status changes
the_contentFilterModify post content
the_titleFilterModify post title
anvil_register_blocksActionRegister custom Anvil blocks

Users

HookTypeDescription
user_logged_inActionAfter successful login
user_login_failedActionAfter failed login
authenticateFilterCustom auth methods

Admin

HookTypeDescription
admin_initActionWhen admin loads
admin_menuActionWhen building menu
admin_headActionInside admin head
admin_footerActionBefore admin body close
admin_noticesActionDisplay admin notices
admin_enqueue_scriptsActionEnqueue page assets

Theme/Template

HookTypeDescription
vf_headActionInside head tag
vf_footerActionBefore body close
body_classFilterModify body classes
template_includeFilterOverride template

REST API

HookTypeDescription
rest_api_initActionRegister routes
rest_pre_dispatchFilterBefore handling request
rest_post_dispatchFilterModify response

SEO

HookTypeDescription
seo_titleFilterModify page title
seo_descriptionFilterModify meta description
seo_robotsFilterModify robots meta
seo_canonicalFilterModify canonical URL
seo_og_tagsFilterModify Open Graph tags
seo_twitter_tagsFilterModify Twitter Card tags
seo_schemaFilterModify JSON-LD schema
seo_sitemap_urlsFilterAdd 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
More resources See the Theme Development Guide for frontend customization.