I’ll build this as a full WordPress theme “hook builder” module: admin UI, saved hook locations, priorities, conditional display, shortcode/PHP-safe callbacks, import/export, and frontend rendering.
Absolutely, my dear friend. Below is an advanced inc/builders/hook-builder.php file for your Rx Theme. It adds a powerful hook builder system with:
- Admin panel under Appearance → RX Hook Builder
- Add unlimited hook blocks
- Enable/disable each block
- Select WordPress/theme hook location
- Priority control
- HTML / shortcode / template-part output
- Device targeting: desktop, tablet, mobile
- Login targeting: everyone, logged-in, logged-out
- User role targeting
- Page/post/category/archive/search/home/404 conditions
- Date start/end scheduling
- Import/export JSON
- Safe sanitization
- Frontend rendering
- Shortcode support
- Manual theme hook output support
1. Create this file
Path:
inc/builders/hook-builder.php
Paste this full code:
<?php
/**
* RX Theme Hook Builder
*
* File: inc/builders/hook-builder.php
*
* Advanced hook builder for RX Theme.
* Allows adding custom content into WordPress/theme hooks with conditions.
*
* @package RX_Theme
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
if ( ! class_exists( 'RX_Theme_Hook_Builder' ) ) :
final class RX_Theme_Hook_Builder {
/**
* Option name.
*
* @var string
*/
const OPTION_NAME = 'rx_theme_hook_builder_items';
/**
* Nonce action.
*
* @var string
*/
const NONCE_ACTION = 'rx_theme_hook_builder_action';
/**
* Nonce name.
*
* @var string
*/
const NONCE_NAME = 'rx_theme_hook_builder_nonce';
/**
* Admin page slug.
*
* @var string
*/
const PAGE_SLUG = 'rx-theme-hook-builder';
/**
* Singleton instance.
*
* @var self|null
*/
private static $instance = null;
/**
* Get instance.
*
* @return self
*/
public static function instance() {
if ( null === self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Constructor.
*/
private function __construct() {
add_action( 'init', array( $this, 'register_frontend_hooks' ), 20 );
add_action( 'admin_menu', array( $this, 'register_admin_page' ) );
add_action( 'admin_init', array( $this, 'handle_admin_actions' ) );
add_action( 'admin_enqueue_scripts', array( $this, 'admin_assets' ) );
add_shortcode( 'rx_hook_area', array( $this, 'shortcode_hook_area' ) );
add_shortcode( 'rx_hook_block', array( $this, 'shortcode_hook_block' ) );
}
/**
* Get default hook locations.
*
* You can add your own theme hooks here.
*
* @return array
*/
public function get_hook_locations() {
$hooks = array(
'wp_head' => 'WordPress: wp_head',
'wp_body_open' => 'WordPress: wp_body_open',
'wp_footer' => 'WordPress: wp_footer',
'loop_start' => 'WordPress: loop_start',
'loop_end' => 'WordPress: loop_end',
'the_content_before' => 'RX Virtual: Before Post Content',
'the_content_after' => 'RX Virtual: After Post Content',
'rx_before_site' => 'RX Theme: Before Site',
'rx_after_site' => 'RX Theme: After Site',
'rx_before_header' => 'RX Theme: Before Header',
'rx_header' => 'RX Theme: Header',
'rx_after_header' => 'RX Theme: After Header',
'rx_before_nav' => 'RX Theme: Before Navigation',
'rx_after_nav' => 'RX Theme: After Navigation',
'rx_before_main' => 'RX Theme: Before Main',
'rx_after_main' => 'RX Theme: After Main',
'rx_before_content' => 'RX Theme: Before Content',
'rx_after_content' => 'RX Theme: After Content',
'rx_before_sidebar' => 'RX Theme: Before Sidebar',
'rx_after_sidebar' => 'RX Theme: After Sidebar',
'rx_before_footer' => 'RX Theme: Before Footer',
'rx_footer' => 'RX Theme: Footer',
'rx_after_footer' => 'RX Theme: After Footer',
'rx_before_single' => 'RX Theme: Before Single Post',
'rx_after_single' => 'RX Theme: After Single Post',
'rx_before_page' => 'RX Theme: Before Page',
'rx_after_page' => 'RX Theme: After Page',
'rx_before_archive' => 'RX Theme: Before Archive',
'rx_after_archive' => 'RX Theme: After Archive',
'rx_before_search' => 'RX Theme: Before Search',
'rx_after_search' => 'RX Theme: After Search',
'rx_before_404' => 'RX Theme: Before 404',
'rx_after_404' => 'RX Theme: After 404',
'rx_inside_article_top' => 'RX Theme: Inside Article Top',
'rx_inside_article_bottom' => 'RX Theme: Inside Article Bottom',
'rx_before_comments' => 'RX Theme: Before Comments',
'rx_after_comments' => 'RX Theme: After Comments',
);
return apply_filters( 'rx_theme_hook_builder_locations', $hooks );
}
/**
* Register frontend actions dynamically.
*
* @return void
*/
public function register_frontend_hooks() {
$items = $this->get_items();
if ( empty( $items ) ) {
return;
}
foreach ( $items as $item ) {
if ( empty( $item['status'] ) || 'enabled' !== $item['status'] ) {
continue;
}
$hook = ! empty( $item['hook'] ) ? sanitize_key( $item['hook'] ) : '';
$priority = isset( $item['priority'] ) ? absint( $item['priority'] ) : 10;
if ( empty( $hook ) ) {
continue;
}
if ( 'the_content_before' === $hook || 'the_content_after' === $hook ) {
add_filter( 'the_content', array( $this, 'filter_the_content' ), $priority );
continue;
}
add_action(
$hook,
function () use ( $item ) {
$this->render_item( $item );
},
$priority
);
}
}
/**
* Add virtual content hooks around the_content.
*
* @param string $content Content.
* @return string
*/
public function filter_the_content( $content ) {
if ( is_admin() || ! in_the_loop() || ! is_main_query() ) {
return $content;
}
$before = '';
$after = '';
$items = $this->get_items();
foreach ( $items as $item ) {
if ( empty( $item['status'] ) || 'enabled' !== $item['status'] ) {
continue;
}
if ( empty( $item['hook'] ) ) {
continue;
}
if ( ! $this->item_can_display( $item ) ) {
continue;
}
ob_start();
$this->render_item( $item, false );
$output = ob_get_clean();
if ( 'the_content_before' === $item['hook'] ) {
$before .= $output;
}
if ( 'the_content_after' === $item['hook'] ) {
$after .= $output;
}
}
return $before . $content . $after;
}
/**
* Register admin page.
*
* @return void
*/
public function register_admin_page() {
add_theme_page(
esc_html__( 'RX Hook Builder', 'rx-theme' ),
esc_html__( 'RX Hook Builder', 'rx-theme' ),
'manage_options',
self::PAGE_SLUG,
array( $this, 'render_admin_page' )
);
}
/**
* Admin assets.
*
* @param string $hook Current admin hook.
* @return void
*/
public function admin_assets( $hook ) {
if ( false === strpos( $hook, self::PAGE_SLUG ) ) {
return;
}
wp_enqueue_style( 'wp-color-picker' );
wp_enqueue_script( 'wp-color-picker' );
$css = '
.rx-hook-wrap {
max-width: 1250px;
}
.rx-hook-card {
background: #fff;
border: 1px solid #dcdcde;
border-radius: 8px;
padding: 18px;
margin: 18px 0;
box-shadow: 0 1px 2px rgba(0,0,0,.04);
}
.rx-hook-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 16px;
}
.rx-hook-grid-3 {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 16px;
}
.rx-hook-card label {
font-weight: 600;
display: block;
margin-bottom: 6px;
}
.rx-hook-card input[type="text"],
.rx-hook-card input[type="number"],
.rx-hook-card input[type="date"],
.rx-hook-card select,
.rx-hook-card textarea {
width: 100%;
max-width: 100%;
}
.rx-hook-card textarea {
min-height: 160px;
font-family: Consolas, Monaco, monospace;
}
.rx-hook-mini {
color: #646970;
font-size: 12px;
}
.rx-hook-badge {
display: inline-block;
padding: 3px 8px;
background: #f0f6fc;
border: 1px solid #c5d9ed;
border-radius: 999px;
font-size: 12px;
}
.rx-hook-danger {
color: #b32d2e;
}
.rx-hook-actions {
display: flex;
gap: 8px;
align-items: center;
flex-wrap: wrap;
}
@media(max-width: 782px) {
.rx-hook-grid,
.rx-hook-grid-3 {
grid-template-columns: 1fr;
}
}
';
wp_add_inline_style( 'wp-admin', $css );
$js = '
jQuery(function($){
$(".rx-hook-confirm").on("click", function(e){
if(!confirm("Are you sure?")) {
e.preventDefault();
}
});
$(".rx-hook-copy").on("click", function(e){
e.preventDefault();
var target = $(this).data("target");
var text = $(target).val();
if(navigator.clipboard){
navigator.clipboard.writeText(text);
alert("Copied!");
}
});
});
';
wp_add_inline_script( 'jquery-core', $js );
}
/**
* Render admin page.
*
* @return void
*/
public function render_admin_page() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
$items = $this->get_items();
$editing_id = isset( $_GET['edit'] ) ? sanitize_text_field( wp_unslash( $_GET['edit'] ) ) : '';
$editing_item = $editing_id ? $this->get_item( $editing_id ) : array();
$hook_locations = $this->get_hook_locations();
$roles = wp_roles()->roles;
$export_json = wp_json_encode( $items, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES );
?>
<div class="wrap rx-hook-wrap">
<h1><?php esc_html_e( 'RX Theme Hook Builder', 'rx-theme' ); ?></h1>
<p>
<?php esc_html_e( 'Create reusable hook blocks and display them in theme or WordPress hook locations with conditions.', 'rx-theme' ); ?>
</p>
<?php $this->admin_notices(); ?>
<div class="rx-hook-card">
<h2>
<?php
echo $editing_item
? esc_html__( 'Edit Hook Block', 'rx-theme' )
: esc_html__( 'Add New Hook Block', 'rx-theme' );
?>
</h2>
<form method="post" action="">
<?php wp_nonce_field( self::NONCE_ACTION, self::NONCE_NAME ); ?>
<input type="hidden" name="rx_hook_builder_action" value="save_item">
<input type="hidden" name="item_id" value="<?php echo esc_attr( $editing_item['id'] ?? '' ); ?>">
<div class="rx-hook-grid">
<p>
<label for="rx_title"><?php esc_html_e( 'Title', 'rx-theme' ); ?></label>
<input type="text" id="rx_title" name="title" value="<?php echo esc_attr( $editing_item['title'] ?? '' ); ?>" placeholder="Header ad, Footer CTA, schema script...">
</p>
<p>
<label for="rx_status"><?php esc_html_e( 'Status', 'rx-theme' ); ?></label>
<select id="rx_status" name="status">
<?php
$status = $editing_item['status'] ?? 'enabled';
$this->option( 'enabled', 'Enabled', $status );
$this->option( 'disabled', 'Disabled', $status );
?>
</select>
</p>
</div>
<div class="rx-hook-grid-3">
<p>
<label for="rx_hook"><?php esc_html_e( 'Hook Location', 'rx-theme' ); ?></label>
<select id="rx_hook" name="hook">
<?php
$current_hook = $editing_item['hook'] ?? 'wp_footer';
foreach ( $hook_locations as $hook_key => $hook_label ) {
printf(
'<option value="%s" %s>%s</option>',
esc_attr( $hook_key ),
selected( $current_hook, $hook_key, false ),
esc_html( $hook_label )
);
}
?>
</select>
<span class="rx-hook-mini">
<?php esc_html_e( 'Theme hooks must exist in your template files using do_action().', 'rx-theme' ); ?>
</span>
</p>
<p>
<label for="rx_priority"><?php esc_html_e( 'Priority', 'rx-theme' ); ?></label>
<input type="number" id="rx_priority" name="priority" value="<?php echo esc_attr( $editing_item['priority'] ?? 10 ); ?>" min="0" max="999">
</p>
<p>
<label for="rx_content_type"><?php esc_html_e( 'Content Type', 'rx-theme' ); ?></label>
<select id="rx_content_type" name="content_type">
<?php
$content_type = $editing_item['content_type'] ?? 'html';
$this->option( 'html', 'HTML / Text', $content_type );
$this->option( 'shortcode', 'Shortcode Content', $content_type );
$this->option( 'template_part', 'Template Part', $content_type );
?>
</select>
</p>
</div>
<p>
<label for="rx_content"><?php esc_html_e( 'Content', 'rx-theme' ); ?></label>
<textarea id="rx_content" name="content" placeholder="HTML, shortcode, or template path like template-parts/hooks/header-ad"><?php echo esc_textarea( $editing_item['content'] ?? '' ); ?></textarea>
<span class="rx-hook-mini">
<?php esc_html_e( 'For template part, write path without .php. Example: template-parts/hooks/header-ad', 'rx-theme' ); ?>
</span>
</p>
<h3><?php esc_html_e( 'Display Conditions', 'rx-theme' ); ?></h3>
<div class="rx-hook-grid-3">
<p>
<label for="rx_condition_mode"><?php esc_html_e( 'Condition Mode', 'rx-theme' ); ?></label>
<select id="rx_condition_mode" name="condition_mode">
<?php
$condition_mode = $editing_item['condition_mode'] ?? 'all';
$this->option( 'all', 'Show Everywhere', $condition_mode );
$this->option( 'include', 'Show Only On Selected Conditions', $condition_mode );
$this->option( 'exclude', 'Hide On Selected Conditions', $condition_mode );
?>
</select>
</p>
<p>
<label for="rx_logged_in"><?php esc_html_e( 'Login Visibility', 'rx-theme' ); ?></label>
<select id="rx_logged_in" name="logged_in">
<?php
$logged_in = $editing_item['logged_in'] ?? 'all';
$this->option( 'all', 'Everyone', $logged_in );
$this->option( 'logged_in', 'Logged-in Users Only', $logged_in );
$this->option( 'logged_out', 'Logged-out Visitors Only', $logged_in );
?>
</select>
</p>
<p>
<label for="rx_device"><?php esc_html_e( 'Device Visibility', 'rx-theme' ); ?></label>
<select id="rx_device" name="device">
<?php
$device = $editing_item['device'] ?? 'all';
$this->option( 'all', 'All Devices', $device );
$this->option( 'desktop', 'Desktop Only', $device );
$this->option( 'mobile', 'Mobile / Tablet Only', $device );
?>
</select>
</p>
</div>
<div class="rx-hook-grid">
<p>
<label for="rx_conditions"><?php esc_html_e( 'Conditions', 'rx-theme' ); ?></label>
<select id="rx_conditions" name="conditions[]" multiple size="12">
<?php
$current_conditions = $editing_item['conditions'] ?? array();
$condition_options = $this->get_condition_options();
foreach ( $condition_options as $condition_key => $condition_label ) {
printf(
'<option value="%s" %s>%s</option>',
esc_attr( $condition_key ),
selected( in_array( $condition_key, $current_conditions, true ), true, false ),
esc_html( $condition_label )
);
}
?>
</select>
<span class="rx-hook-mini">
<?php esc_html_e( 'Hold Ctrl/Cmd to select multiple conditions.', 'rx-theme' ); ?>
</span>
</p>
<p>
<label for="rx_roles"><?php esc_html_e( 'Allowed User Roles', 'rx-theme' ); ?></label>
<select id="rx_roles" name="roles[]" multiple size="12">
<?php
$current_roles = $editing_item['roles'] ?? array();
foreach ( $roles as $role_key => $role_data ) {
printf(
'<option value="%s" %s>%s</option>',
esc_attr( $role_key ),
selected( in_array( $role_key, $current_roles, true ), true, false ),
esc_html( translate_user_role( $role_data['name'] ) )
);
}
?>
</select>
<span class="rx-hook-mini">
<?php esc_html_e( 'Leave empty to allow all roles.', 'rx-theme' ); ?>
</span>
</p>
</div>
<div class="rx-hook-grid">
<p>
<label for="rx_start_date"><?php esc_html_e( 'Start Date', 'rx-theme' ); ?></label>
<input type="date" id="rx_start_date" name="start_date" value="<?php echo esc_attr( $editing_item['start_date'] ?? '' ); ?>">
<span class="rx-hook-mini"><?php esc_html_e( 'Optional schedule start date.', 'rx-theme' ); ?></span>
</p>
<p>
<label for="rx_end_date"><?php esc_html_e( 'End Date', 'rx-theme' ); ?></label>
<input type="date" id="rx_end_date" name="end_date" value="<?php echo esc_attr( $editing_item['end_date'] ?? '' ); ?>">
<span class="rx-hook-mini"><?php esc_html_e( 'Optional schedule end date.', 'rx-theme' ); ?></span>
</p>
</div>
<div class="rx-hook-grid">
<p>
<label for="rx_wrapper_class"><?php esc_html_e( 'Wrapper CSS Class', 'rx-theme' ); ?></label>
<input type="text" id="rx_wrapper_class" name="wrapper_class" value="<?php echo esc_attr( $editing_item['wrapper_class'] ?? '' ); ?>" placeholder="rx-header-ad custom-class">
</p>
<p>
<label for="rx_wrapper_id"><?php esc_html_e( 'Wrapper HTML ID', 'rx-theme' ); ?></label>
<input type="text" id="rx_wrapper_id" name="wrapper_id" value="<?php echo esc_attr( $editing_item['wrapper_id'] ?? '' ); ?>" placeholder="rx-special-banner">
</p>
</div>
<p>
<label>
<input type="checkbox" name="disable_wrapper" value="1" <?php checked( ! empty( $editing_item['disable_wrapper'] ) ); ?>>
<?php esc_html_e( 'Disable wrapper div', 'rx-theme' ); ?>
</label>
</p>
<p class="submit rx-hook-actions">
<button type="submit" class="button button-primary">
<?php esc_html_e( 'Save Hook Block', 'rx-theme' ); ?>
</button>
<a href="<?php echo esc_url( admin_url( 'themes.php?page=' . self::PAGE_SLUG ) ); ?>" class="button">
<?php esc_html_e( 'Add New / Reset Form', 'rx-theme' ); ?>
</a>
</p>
</form>
</div>
<div class="rx-hook-card">
<h2><?php esc_html_e( 'Saved Hook Blocks', 'rx-theme' ); ?></h2>
<?php if ( empty( $items ) ) : ?>
<p><?php esc_html_e( 'No hook blocks found.', 'rx-theme' ); ?></p>
<?php else : ?>
<table class="widefat striped">
<thead>
<tr>
<th><?php esc_html_e( 'Title', 'rx-theme' ); ?></th>
<th><?php esc_html_e( 'Hook', 'rx-theme' ); ?></th>
<th><?php esc_html_e( 'Priority', 'rx-theme' ); ?></th>
<th><?php esc_html_e( 'Type', 'rx-theme' ); ?></th>
<th><?php esc_html_e( 'Status', 'rx-theme' ); ?></th>
<th><?php esc_html_e( 'Actions', 'rx-theme' ); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ( $items as $item ) : ?>
<tr>
<td>
<strong><?php echo esc_html( $item['title'] ?? 'Untitled' ); ?></strong>
<br>
<span class="rx-hook-mini">
ID: <?php echo esc_html( $item['id'] ?? '' ); ?>
</span>
</td>
<td>
<span class="rx-hook-badge">
<?php echo esc_html( $item['hook'] ?? '' ); ?>
</span>
</td>
<td><?php echo esc_html( $item['priority'] ?? 10 ); ?></td>
<td><?php echo esc_html( $item['content_type'] ?? 'html' ); ?></td>
<td>
<?php if ( ! empty( $item['status'] ) && 'enabled' === $item['status'] ) : ?>
<span class="rx-hook-badge"><?php esc_html_e( 'Enabled', 'rx-theme' ); ?></span>
<?php else : ?>
<span class="rx-hook-danger"><?php esc_html_e( 'Disabled', 'rx-theme' ); ?></span>
<?php endif; ?>
</td>
<td>
<div class="rx-hook-actions">
<a class="button button-small" href="<?php echo esc_url( admin_url( 'themes.php?page=' . self::PAGE_SLUG . '&edit=' . rawurlencode( $item['id'] ) ) ); ?>">
<?php esc_html_e( 'Edit', 'rx-theme' ); ?>
</a>
<form method="post" action="" style="display:inline;">
<?php wp_nonce_field( self::NONCE_ACTION, self::NONCE_NAME ); ?>
<input type="hidden" name="rx_hook_builder_action" value="duplicate_item">
<input type="hidden" name="item_id" value="<?php echo esc_attr( $item['id'] ); ?>">
<button type="submit" class="button button-small">
<?php esc_html_e( 'Duplicate', 'rx-theme' ); ?>
</button>
</form>
<form method="post" action="" style="display:inline;">
<?php wp_nonce_field( self::NONCE_ACTION, self::NONCE_NAME ); ?>
<input type="hidden" name="rx_hook_builder_action" value="toggle_item">
<input type="hidden" name="item_id" value="<?php echo esc_attr( $item['id'] ); ?>">
<button type="submit" class="button button-small">
<?php
echo ( ! empty( $item['status'] ) && 'enabled' === $item['status'] )
? esc_html__( 'Disable', 'rx-theme' )
: esc_html__( 'Enable', 'rx-theme' );
?>
</button>
</form>
<form method="post" action="" style="display:inline;">
<?php wp_nonce_field( self::NONCE_ACTION, self::NONCE_NAME ); ?>
<input type="hidden" name="rx_hook_builder_action" value="delete_item">
<input type="hidden" name="item_id" value="<?php echo esc_attr( $item['id'] ); ?>">
<button type="submit" class="button button-small rx-hook-confirm rx-hook-danger">
<?php esc_html_e( 'Delete', 'rx-theme' ); ?>
</button>
</form>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
<div class="rx-hook-card">
<h2><?php esc_html_e( 'Import / Export', 'rx-theme' ); ?></h2>
<div class="rx-hook-grid">
<div>
<h3><?php esc_html_e( 'Export JSON', 'rx-theme' ); ?></h3>
<textarea id="rx-hook-export-json" readonly><?php echo esc_textarea( $export_json ); ?></textarea>
<p>
<button class="button rx-hook-copy" data-target="#rx-hook-export-json">
<?php esc_html_e( 'Copy Export JSON', 'rx-theme' ); ?>
</button>
</p>
</div>
<div>
<h3><?php esc_html_e( 'Import JSON', 'rx-theme' ); ?></h3>
<form method="post" action="">
<?php wp_nonce_field( self::NONCE_ACTION, self::NONCE_NAME ); ?>
<input type="hidden" name="rx_hook_builder_action" value="import_items">
<textarea name="import_json" placeholder="Paste exported JSON here"></textarea>
<p>
<label>
<input type="checkbox" name="replace_existing" value="1">
<?php esc_html_e( 'Replace existing hook blocks', 'rx-theme' ); ?>
</label>
</p>
<p>
<button type="submit" class="button button-primary rx-hook-confirm">
<?php esc_html_e( 'Import Hook Blocks', 'rx-theme' ); ?>
</button>
</p>
</form>
</div>
</div>
</div>
<div class="rx-hook-card">
<h2><?php esc_html_e( 'How to Add RX Theme Hooks in Template Files', 'rx-theme' ); ?></h2>
<p><?php esc_html_e( 'Use these examples inside your theme template files.', 'rx-theme' ); ?></p>
<pre><code><?php do_action( 'rx_before_header' ); ?>
<?php do_action( 'rx_after_header' ); ?>
<?php do_action( 'rx_before_main' ); ?>
<?php do_action( 'rx_after_main' ); ?>
<?php do_action( 'rx_before_footer' ); ?>
<?php do_action( 'rx_after_footer' ); ?></code></pre>
<p><?php esc_html_e( 'Shortcodes:', 'rx-theme' ); ?></p>
<pre><code>[rx_hook_area hook="rx_before_header"]
[rx_hook_block id="your-block-id"]</code></pre>
</div>
</div>
<?php
}
/**
* Admin notices.
*
* @return void
*/
private function admin_notices() {
if ( empty( $_GET['rx_hook_notice'] ) ) {
return;
}
$notice = sanitize_key( wp_unslash( $_GET['rx_hook_notice'] ) );
$messages = array(
'saved' => __( 'Hook block saved successfully.', 'rx-theme' ),
'deleted' => __( 'Hook block deleted successfully.', 'rx-theme' ),
'duplicated' => __( 'Hook block duplicated successfully.', 'rx-theme' ),
'toggled' => __( 'Hook block status changed successfully.', 'rx-theme' ),
'imported' => __( 'Hook blocks imported successfully.', 'rx-theme' ),
'error' => __( 'Something went wrong.', 'rx-theme' ),
);
$message = $messages[ $notice ] ?? $messages['error'];
printf(
'<div class="notice notice-success is-dismissible"><p>%s</p></div>',
esc_html( $message )
);
}
/**
* Handle admin actions.
*
* @return void
*/
public function handle_admin_actions() {
if ( empty( $_POST['rx_hook_builder_action'] ) ) {
return;
}
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
check_admin_referer( self::NONCE_ACTION, self::NONCE_NAME );
$action = sanitize_key( wp_unslash( $_POST['rx_hook_builder_action'] ) );
switch ( $action ) {
case 'save_item':
$this->save_item_from_post();
break;
case 'delete_item':
$this->delete_item_from_post();
break;
case 'duplicate_item':
$this->duplicate_item_from_post();
break;
case 'toggle_item':
$this->toggle_item_from_post();
break;
case 'import_items':
$this->import_items_from_post();
break;
}
}
/**
* Save item from POST.
*
* @return void
*/
private function save_item_from_post() {
$items = $this->get_items();
$id = ! empty( $_POST['item_id'] )
? sanitize_text_field( wp_unslash( $_POST['item_id'] ) )
: $this->generate_id();
$item = array(
'id' => $id,
'title' => isset( $_POST['title'] ) ? sanitize_text_field( wp_unslash( $_POST['title'] ) ) : '',
'status' => isset( $_POST['status'] ) ? sanitize_key( wp_unslash( $_POST['status'] ) ) : 'enabled',
'hook' => isset( $_POST['hook'] ) ? sanitize_key( wp_unslash( $_POST['hook'] ) ) : 'wp_footer',
'priority' => isset( $_POST['priority'] ) ? absint( $_POST['priority'] ) : 10,
'content_type' => isset( $_POST['content_type'] ) ? sanitize_key( wp_unslash( $_POST['content_type'] ) ) : 'html',
'content' => isset( $_POST['content'] ) ? wp_kses_post( wp_unslash( $_POST['content'] ) ) : '',
'condition_mode' => isset( $_POST['condition_mode'] ) ? sanitize_key( wp_unslash( $_POST['condition_mode'] ) ) : 'all',
'conditions' => isset( $_POST['conditions'] ) ? $this->sanitize_array( wp_unslash( $_POST['conditions'] ) ) : array(),
'logged_in' => isset( $_POST['logged_in'] ) ? sanitize_key( wp_unslash( $_POST['logged_in'] ) ) : 'all',
'device' => isset( $_POST['device'] ) ? sanitize_key( wp_unslash( $_POST['device'] ) ) : 'all',
'roles' => isset( $_POST['roles'] ) ? $this->sanitize_array( wp_unslash( $_POST['roles'] ) ) : array(),
'start_date' => isset( $_POST['start_date'] ) ? sanitize_text_field( wp_unslash( $_POST['start_date'] ) ) : '',
'end_date' => isset( $_POST['end_date'] ) ? sanitize_text_field( wp_unslash( $_POST['end_date'] ) ) : '',
'wrapper_class' => isset( $_POST['wrapper_class'] ) ? sanitize_html_class( wp_unslash( $_POST['wrapper_class'] ) ) : '',
'wrapper_id' => isset( $_POST['wrapper_id'] ) ? sanitize_html_class( wp_unslash( $_POST['wrapper_id'] ) ) : '',
'disable_wrapper' => ! empty( $_POST['disable_wrapper'] ) ? 1 : 0,
'updated_at' => current_time( 'mysql' ),
);
if ( empty( $item['title'] ) ) {
$item['title'] = 'Untitled Hook Block';
}
$existing = $this->get_item( $id );
if ( empty( $existing ) ) {
$item['created_at'] = current_time( 'mysql' );
} else {
$item['created_at'] = $existing['created_at'] ?? current_time( 'mysql' );
}
$items[ $id ] = $item;
$this->update_items( $items );
$this->redirect_with_notice( 'saved' );
}
/**
* Delete item from POST.
*
* @return void
*/
private function delete_item_from_post() {
$id = isset( $_POST['item_id'] ) ? sanitize_text_field( wp_unslash( $_POST['item_id'] ) ) : '';
$items = $this->get_items();
if ( $id && isset( $items[ $id ] ) ) {
unset( $items[ $id ] );
$this->update_items( $items );
}
$this->redirect_with_notice( 'deleted' );
}
/**
* Duplicate item from POST.
*
* @return void
*/
private function duplicate_item_from_post() {
$id = isset( $_POST['item_id'] ) ? sanitize_text_field( wp_unslash( $_POST['item_id'] ) ) : '';
$items = $this->get_items();
if ( $id && isset( $items[ $id ] ) ) {
$new_id = $this->generate_id();
$new = $items[ $id ];
$new['id'] = $new_id;
$new['title'] = ( $new['title'] ?? 'Hook Block' ) . ' Copy';
$new['created_at'] = current_time( 'mysql' );
$new['updated_at'] = current_time( 'mysql' );
$items[ $new_id ] = $new;
$this->update_items( $items );
}
$this->redirect_with_notice( 'duplicated' );
}
/**
* Toggle item status from POST.
*
* @return void
*/
private function toggle_item_from_post() {
$id = isset( $_POST['item_id'] ) ? sanitize_text_field( wp_unslash( $_POST['item_id'] ) ) : '';
$items = $this->get_items();
if ( $id && isset( $items[ $id ] ) ) {
$current_status = $items[ $id ]['status'] ?? 'enabled';
$items[ $id ]['status'] = 'enabled' === $current_status ? 'disabled' : 'enabled';
$items[ $id ]['updated_at'] = current_time( 'mysql' );
$this->update_items( $items );
}
$this->redirect_with_notice( 'toggled' );
}
/**
* Import items from POST.
*
* @return void
*/
private function import_items_from_post() {
$json = isset( $_POST['import_json'] ) ? wp_unslash( $_POST['import_json'] ) : '';
$decoded = json_decode( $json, true );
if ( ! is_array( $decoded ) ) {
$this->redirect_with_notice( 'error' );
}
$existing = ! empty( $_POST['replace_existing'] ) ? array() : $this->get_items();
foreach ( $decoded as $item ) {
if ( ! is_array( $item ) ) {
continue;
}
$id = ! empty( $item['id'] ) ? sanitize_text_field( $item['id'] ) : $this->generate_id();
$item['id'] = $id;
$item['title'] = sanitize_text_field( $item['title'] ?? 'Imported Hook Block' );
$item['status'] = sanitize_key( $item['status'] ?? 'disabled' );
$item['hook'] = sanitize_key( $item['hook'] ?? 'wp_footer' );
$item['priority'] = absint( $item['priority'] ?? 10 );
$item['content_type'] = sanitize_key( $item['content_type'] ?? 'html' );
$item['content'] = wp_kses_post( $item['content'] ?? '' );
$item['condition_mode'] = sanitize_key( $item['condition_mode'] ?? 'all' );
$item['conditions'] = isset( $item['conditions'] ) ? $this->sanitize_array( $item['conditions'] ) : array();
$item['logged_in'] = sanitize_key( $item['logged_in'] ?? 'all' );
$item['device'] = sanitize_key( $item['device'] ?? 'all' );
$item['roles'] = isset( $item['roles'] ) ? $this->sanitize_array( $item['roles'] ) : array();
$item['start_date'] = sanitize_text_field( $item['start_date'] ?? '' );
$item['end_date'] = sanitize_text_field( $item['end_date'] ?? '' );
$item['wrapper_class'] = sanitize_html_class( $item['wrapper_class'] ?? '' );
$item['wrapper_id'] = sanitize_html_class( $item['wrapper_id'] ?? '' );
$item['disable_wrapper'] = ! empty( $item['disable_wrapper'] ) ? 1 : 0;
$item['updated_at'] = current_time( 'mysql' );
$item['created_at'] = sanitize_text_field( $item['created_at'] ?? current_time( 'mysql' ) );
$existing[ $id ] = $item;
}
$this->update_items( $existing );
$this->redirect_with_notice( 'imported' );
}
/**
* Redirect with admin notice.
*
* @param string $notice Notice key.
* @return void
*/
private function redirect_with_notice( $notice ) {
wp_safe_redirect(
add_query_arg(
array(
'page' => self::PAGE_SLUG,
'rx_hook_notice' => sanitize_key( $notice ),
),
admin_url( 'themes.php' )
)
);
exit;
}
/**
* Get saved items.
*
* @return array
*/
public function get_items() {
$items = get_option( self::OPTION_NAME, array() );
if ( ! is_array( $items ) ) {
$items = array();
}
return $items;
}
/**
* Update saved items.
*
* @param array $items Items.
* @return bool
*/
public function update_items( $items ) {
return update_option( self::OPTION_NAME, $items, false );
}
/**
* Get single item.
*
* @param string $id Item ID.
* @return array
*/
public function get_item( $id ) {
$items = $this->get_items();
return $items[ $id ] ?? array();
}
/**
* Generate item ID.
*
* @return string
*/
private function generate_id() {
return 'rx_hook_' . wp_generate_password( 12, false, false );
}
/**
* Render one item.
*
* @param array $item Item.
* @param bool $check_condition Whether to check condition.
* @return void
*/
public function render_item( $item, $check_condition = true ) {
if ( $check_condition && ! $this->item_can_display( $item ) ) {
return;
}
$content_type = $item['content_type'] ?? 'html';
$content = $item['content'] ?? '';
if ( empty( $content ) ) {
return;
}
$output = '';
switch ( $content_type ) {
case 'shortcode':
$output = do_shortcode( $content );
break;
case 'template_part':
$output = $this->get_template_part_output( $content, $item );
break;
case 'html':
default:
$output = do_shortcode( wp_kses_post( $content ) );
break;
}
$output = apply_filters( 'rx_theme_hook_builder_output', $output, $item );
if ( empty( $output ) ) {
return;
}
if ( ! empty( $item['disable_wrapper'] ) ) {
echo $output; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
return;
}
$classes = array(
'rx-hook-builder-block',
'rx-hook-builder-' . sanitize_html_class( $item['id'] ?? '' ),
);
if ( ! empty( $item['wrapper_class'] ) ) {
$classes[] = sanitize_html_class( $item['wrapper_class'] );
}
$wrapper_id = ! empty( $item['wrapper_id'] ) ? sanitize_html_class( $item['wrapper_id'] ) : '';
printf(
'<div %s class="%s" data-rx-hook="%s">%s</div>',
$wrapper_id ? 'id="' . esc_attr( $wrapper_id ) . '"' : '',
esc_attr( implode( ' ', array_filter( $classes ) ) ),
esc_attr( $item['hook'] ?? '' ),
$output // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
);
}
/**
* Get template part output.
*
* @param string $path Template path.
* @param array $item Item.
* @return string
*/
private function get_template_part_output( $path, $item ) {
$path = trim( $path );
$path = str_replace( array( '../', './', '.php' ), '', $path );
$path = sanitize_text_field( $path );
if ( empty( $path ) ) {
return '';
}
ob_start();
set_query_var( 'rx_hook_builder_item', $item );
get_template_part( $path );
return ob_get_clean();
}
/**
* Determine whether item can display.
*
* @param array $item Item.
* @return bool
*/
public function item_can_display( $item ) {
if ( is_admin() ) {
return false;
}
if ( empty( $item['status'] ) || 'enabled' !== $item['status'] ) {
return false;
}
if ( ! $this->passes_date_rule( $item ) ) {
return false;
}
if ( ! $this->passes_login_rule( $item ) ) {
return false;
}
if ( ! $this->passes_role_rule( $item ) ) {
return false;
}
if ( ! $this->passes_device_rule( $item ) ) {
return false;
}
if ( ! $this->passes_condition_rule( $item ) ) {
return false;
}
return apply_filters( 'rx_theme_hook_builder_can_display', true, $item );
}
/**
* Date rule.
*
* @param array $item Item.
* @return bool
*/
private function passes_date_rule( $item ) {
$today = current_time( 'Y-m-d' );
$start_date = ! empty( $item['start_date'] ) ? $item['start_date'] : '';
$end_date = ! empty( $item['end_date'] ) ? $item['end_date'] : '';
if ( $start_date && $today < $start_date ) {
return false;
}
if ( $end_date && $today > $end_date ) {
return false;
}
return true;
}
/**
* Login rule.
*
* @param array $item Item.
* @return bool
*/
private function passes_login_rule( $item ) {
$logged_in = $item['logged_in'] ?? 'all';
if ( 'logged_in' === $logged_in && ! is_user_logged_in() ) {
return false;
}
if ( 'logged_out' === $logged_in && is_user_logged_in() ) {
return false;
}
return true;
}
/**
* Role rule.
*
* @param array $item Item.
* @return bool
*/
private function passes_role_rule( $item ) {
if ( empty( $item['roles'] ) || ! is_array( $item['roles'] ) ) {
return true;
}
if ( ! is_user_logged_in() ) {
return false;
}
$user = wp_get_current_user();
if ( empty( $user->roles ) ) {
return false;
}
return (bool) array_intersect( $item['roles'], $user->roles );
}
/**
* Device rule.
*
* @param array $item Item.
* @return bool
*/
private function passes_device_rule( $item ) {
$device = $item['device'] ?? 'all';
if ( 'all' === $device ) {
return true;
}
$is_mobile = wp_is_mobile();
if ( 'mobile' === $device && ! $is_mobile ) {
return false;
}
if ( 'desktop' === $device && $is_mobile ) {
return false;
}
return true;
}
/**
* Condition rule.
*
* @param array $item Item.
* @return bool
*/
private function passes_condition_rule( $item ) {
$mode = $item['condition_mode'] ?? 'all';
$conditions = $item['conditions'] ?? array();
if ( 'all' === $mode || empty( $conditions ) ) {
return true;
}
$matched = false;
foreach ( $conditions as $condition ) {
if ( $this->condition_matches( $condition ) ) {
$matched = true;
break;
}
}
if ( 'include' === $mode ) {
return $matched;
}
if ( 'exclude' === $mode ) {
return ! $matched;
}
return true;
}
/**
* Check individual condition.
*
* @param string $condition Condition.
* @return bool
*/
private function condition_matches( $condition ) {
switch ( $condition ) {
case 'front_page':
return is_front_page();
case 'home':
return is_home();
case 'singular':
return is_singular();
case 'single':
return is_single();
case 'page':
return is_page();
case 'attachment':
return is_attachment();
case 'archive':
return is_archive();
case 'category':
return is_category();
case 'tag':
return is_tag();
case 'author':
return is_author();
case 'date':
return is_date();
case 'search':
return is_search();
case '404':
return is_404();
case 'comments_open':
return is_singular() && comments_open();
case 'has_excerpt':
return is_singular() && has_excerpt();
case 'has_post_thumbnail':
return is_singular() && has_post_thumbnail();
case 'logged_in':
return is_user_logged_in();
case 'logged_out':
return ! is_user_logged_in();
default:
return (bool) apply_filters( 'rx_theme_hook_builder_condition_matches', false, $condition );
}
}
/**
* Get condition options.
*
* @return array
*/
private function get_condition_options() {
return apply_filters(
'rx_theme_hook_builder_condition_options',
array(
'front_page' => 'Front Page',
'home' => 'Blog Home',
'singular' => 'Any Singular Post/Page',
'single' => 'Single Posts',
'page' => 'Pages',
'attachment' => 'Attachment Pages',
'archive' => 'Any Archive',
'category' => 'Category Archive',
'tag' => 'Tag Archive',
'author' => 'Author Archive',
'date' => 'Date Archive',
'search' => 'Search Results',
'404' => '404 Page',
'comments_open' => 'Comments Open',
'has_excerpt' => 'Posts/Pages With Excerpt',
'has_post_thumbnail' => 'Posts/Pages With Featured Image',
'logged_in' => 'Logged-in Users',
'logged_out' => 'Logged-out Visitors',
)
);
}
/**
* Shortcode: render all items for a hook.
*
* Usage: [rx_hook_area hook="rx_before_header"]
*
* @param array $atts Shortcode attributes.
* @return string
*/
public function shortcode_hook_area( $atts ) {
$atts = shortcode_atts(
array(
'hook' => '',
),
$atts,
'rx_hook_area'
);
$hook = sanitize_key( $atts['hook'] );
if ( empty( $hook ) ) {
return '';
}
$items = $this->get_items();
ob_start();
foreach ( $items as $item ) {
if ( ! empty( $item['hook'] ) && $hook === $item['hook'] ) {
$this->render_item( $item );
}
}
return ob_get_clean();
}
/**
* Shortcode: render one item by ID.
*
* Usage: [rx_hook_block id="rx_hook_xxxx"]
*
* @param array $atts Shortcode attributes.
* @return string
*/
public function shortcode_hook_block( $atts ) {
$atts = shortcode_atts(
array(
'id' => '',
),
$atts,
'rx_hook_block'
);
$id = sanitize_text_field( $atts['id'] );
if ( empty( $id ) ) {
return '';
}
$item = $this->get_item( $id );
if ( empty( $item ) ) {
return '';
}
ob_start();
$this->render_item( $item );
return ob_get_clean();
}
/**
* Sanitize simple array.
*
* @param mixed $array Array.
* @return array
*/
private function sanitize_array( $array ) {
if ( ! is_array( $array ) ) {
return array();
}
return array_values(
array_filter(
array_map(
'sanitize_key',
$array
)
)
);
}
/**
* Print selected option.
*
* @param string $value Current value.
* @param string $label Label.
* @param string $current Current selected.
* @return void
*/
private function option( $value, $label, $current ) {
printf(
'<option value="%s" %s>%s</option>',
esc_attr( $value ),
selected( $current, $value, false ),
esc_html( $label )
);
}
}
endif;
/**
* Start RX Hook Builder.
*/
function rx_theme_hook_builder() {
return RX_Theme_Hook_Builder::instance();
}
rx_theme_hook_builder();
/**
* Helper function for theme templates.
*
* Usage:
* rx_theme_do_hook( 'rx_before_header' );
*
* @param string $hook Hook name.
* @return void
*/
function rx_theme_do_hook( $hook ) {
do_action( sanitize_key( $hook ) );
}
2. Load it from functions.php
Add this in your functions.php:
require_once get_template_directory() . '/inc/builders/hook-builder.php';
3. Add theme hook positions in your template files
Example in header.php:
<?php do_action( 'rx_before_site' ); ?>
<?php do_action( 'rx_before_header' ); ?>
<header id="masthead" class="site-header">
<?php do_action( 'rx_header' ); ?>
</header>
<?php do_action( 'rx_after_header' ); ?>
Example in footer.php:
<?php do_action( 'rx_before_footer' ); ?>
<footer id="colophon" class="site-footer">
<?php do_action( 'rx_footer' ); ?>
</footer>
<?php do_action( 'rx_after_footer' ); ?>
<?php do_action( 'rx_after_site' ); ?>
Example in single.php:
<?php do_action( 'rx_before_single' ); ?>
<main id="primary" class="site-main">
<?php do_action( 'rx_before_content' ); ?>
<?php
while ( have_posts() ) :
the_post();
do_action( 'rx_inside_article_top' );
the_content();
do_action( 'rx_inside_article_bottom' );
endwhile;
?>
<?php do_action( 'rx_after_content' ); ?>
</main>
<?php do_action( 'rx_after_single' ); ?>
4. How to use in WordPress admin
After adding the file:
Go to:
Appearance → RX Hook Builder
Then create a hook block like:
<div class="rx-top-banner">
<p>Welcome to RX Theme</p>
</div>
Choose hook:
rx_before_header
Or choose WordPress hook:
wp_footer
You can also use shortcode content:
Error: Contact form not found.
Or template part:
template-parts/hooks/header-ad
That will load:
template-parts/hooks/header-ad.php
Important safety note
I did not add raw PHP eval() execution inside the admin content box. That would be dangerous for a live WordPress theme. Instead, this version supports safer advanced methods:
do_action()
shortcode
template_part
filter extension
This is the better professional way.