Views: 6
Welcome to Episode 4 of building the Versana Companion plugin! In Episode 3, we added theme compatibility checking. Now it’s time to give users a place to configure the plugin.
In this episode, you’ll learn WordPress plugin settings integration – how to add your plugin’s settings directly into the Versana theme’s existing options page. Instead of creating a separate menu item (which clutters the admin), we’ll use WordPress hooks to add a new “Companion” tab to the theme’s options panel.
By the end of this tutorial, you’ll understand how to use filters and actions to extend existing admin pages, making your plugin feel like a natural part of the theme!
What We’ll Cover
- Understanding WordPress hooks (filters and actions)
- How Versana theme options work
- Adding a custom tab using filters
- Creating settings fields in the tab
- Saving settings with the theme
- Loading saved plugin settings
- Using extensibility hooks
- Testing the integration
Prerequisites
- Completed Episodes 2–3
- Versana theme active (REQUIRED!)
- Versana Companion plugin active
- Text editor open
- Basic understanding that hooks exist (we’ll explain everything)
Part 1: Understanding WordPress Hooks
What Are Hooks?
WordPress hooks are “connection points” where you can add your own code to existing WordPress or theme functionality.
Real-world analogy:
- Theme creates a settings page
- Theme says: “Hey, anyone want to add tabs here?”
- Your plugin says: “Yes! Add my tab please!”
- Theme adds your tab automatically
Two types of hooks:
1. Actions – Do something at a specific point
// Theme says: "I'm about to show tab content"
do_action( 'versana_before_tab_content' );
// Your plugin says: "I'll add something here!"
add_action( 'versana_before_tab_content', 'my_function' );
function my_function() {
echo '<p>My content here!</p>';
}
2. Filters – Modify data before it’s used
// Theme says: "Here are my tabs (anyone want to add more?)"
$tabs = apply_filters( 'versana_option_tabs', $tabs );
// Your plugin says: "I'll add my tab!"
add_filter( 'versana_option_tabs', 'my_add_tab' );
function my_add_tab( $tabs ) {
$tabs['my_tab'] = array( 'title' => 'My Tab' );
return $tabs;
}
Why Use Hooks for Settings?
❌ Without hooks (separate menu):
WordPress Admin Sidebar:
├── Dashboard
├── Posts
├── Pages
├── Appearance
│ └── Theme Options
├── Plugins
└── Versana Companion ← Separate menu!
Problem: Cluttered menu, settings in two places, confusing
✅ With hooks (integrated):
Appearance → Theme Options:
├── Header Tab
├── Footer Tab
├── Blog Tab
├── Companion Tab ← Added by plugin!
└── Integrations Tab
Benefit: Clean, integrated, all settings in one place!
Part 2: How Versana Theme Options Work
The Versana Tab System
Versana theme provides a hook for adding tabs:
// In Versana theme (themes/versana/inc/options-init.php)
function versana_get_option_tabs() {
$tabs = array(
'header' => array(
'title' => 'Header',
'callback' => 'versana_render_header_tab',
'priority' => 20,
),
'footer' => array( /* ... */ ),
'blog' => array( /* ... */ ),
);
// HERE'S THE HOOK! Plugins can add tabs!
$tabs = apply_filters( 'versana_option_tabs', $tabs );
return $tabs;
}
What this means:
- Theme creates default tabs (Header, Footer, Blog, etc.)
- Theme applies filter
'versana_option_tabs' - Any plugin can hook into this filter
- Plugin adds its own tab to the array
- Theme displays all tabs (including plugin tabs)
Understanding Tab Structure
Each tab needs this information:
$tabs['my_tab'] = array(
'title' => 'My Tab Title', // What users see
'icon' => 'dashicons-admin-plugins', // Icon (optional)
'callback' => 'my_render_function', // Function to display content
'priority' => 15, // Where in order (lower = earlier)
);
Priority determines order:
Priority 10 = First
Priority 20 = Second (Header)
Priority 30 = Third (Footer)
Priority 40 = Fourth (Blog)
etc.
We’ll use priority 10 to show our tab FIRST (before Header).
Part 3: Adding Our Tab to Theme Options
Step 1: Create Tab Registration Function
Open versana-companion.php and add this after your theme check function:
/**
* Add Companion tab to theme options
*
* Uses Versana theme's filter hook to add our settings tab
*
* @param array $tabs Existing tabs
* @return array Modified tabs with our tab added
*/
function versana_companion_add_settings_tab( $tabs ) {
// Add our tab at the beginning (priority 10)
$tabs['companion'] = array(
'title' => __( 'Companion', 'versana-companion' ),
'icon' => 'dashicons-admin-plugins',
'callback' => 'versana_companion_render_settings_tab',
'priority' => 10,
);
return $tabs;
}
add_filter( 'versana_option_tabs', 'versana_companion_add_settings_tab' );
Understanding the code:
function versana_companion_add_settings_tab( $tabs ) {
- Function receives existing tabs as parameter
$tabs= Array of all current tabs
$tabs['companion'] = array( /* ... */ );
- Add new tab with key ‘companion’
- Key must be unique (no other tab called ‘companion’)
'title' => __( 'Companion', 'versana-companion' ),
- Tab title shown to users
__()= Translation function- Second parameter = Text domain for translations
'icon' => 'dashicons-admin-plugins',
- Dashicon to show next to title
- Optional but looks professional
- Browse all icons: https://developer.wordpress.org/resource/dashicons/
'callback' => 'versana_companion_render_settings_tab',
- Function that displays tab content
- We’ll create this function next
'priority' => 10,
- Show first (before Header at 20)
- Lower number = earlier in list
return $tabs;
- Return modified tabs array
- CRITICAL: Always return the array in filters!
add_filter( 'versana_option_tabs', 'versana_companion_add_settings_tab' );
- Hook our function to the theme’s filter
- First parameter: Filter name (from theme)
- Second parameter: Our function name
That’s it! Our tab is now registered. But it won’t work yet because we haven’t created the callback function.
Part 4: Creating the Tab Content
Step 2: Render Tab Content Function
Add this function to display your settings:
/**
* Render Companion settings tab
*
* Displays settings form for plugin options
*/
function versana_companion_render_settings_tab() {
// Get current settings
$options = get_option( 'versana_theme_options', array() );
?>
<div class="versana-tab-content">
<h2><?php esc_html_e( 'Companion Plugin Settings', 'versana-companion' ); ?></h2>
<p class="description">
<?php esc_html_e( 'Configure Versana Companion plugin options.', 'versana-companion' ); ?>
</p>
<table class="form-table" role="presentation">
<tbody>
<!-- Enable Demo Import -->
<tr>
<th scope="row">
<?php esc_html_e( 'Demo Import', 'versana-companion' ); ?>
</th>
<td>
<label>
<input type="checkbox"
name="versana_theme_options[enable_demo_import]"
value="1"
<?php checked( isset( $options['enable_demo_import'] ) ? $options['enable_demo_import'] : true, true ); ?> />
<?php esc_html_e( 'Enable one-click demo import feature', 'versana-companion' ); ?>
</label>
<p class="description">
<?php esc_html_e( 'Allow users to import demo content to quickly set up their site.', 'versana-companion' ); ?>
</p>
</td>
</tr>
<!-- Default Demo -->
<tr>
<th scope="row">
<label for="default_demo">
<?php esc_html_e( 'Default Demo', 'versana-companion' ); ?>
</label>
</th>
<td>
<select id="default_demo" name="versana_theme_options[default_demo]">
<?php
$default_demo = isset( $options['default_demo'] ) ? $options['default_demo'] : 'business';
$demos = array(
'business' => __( 'Business', 'versana-companion' ),
'blog' => __( 'Blog', 'versana-companion' ),
'portfolio' => __( 'Portfolio', 'versana-companion' ),
);
foreach ( $demos as $value => $label ) :
?>
<option value="<?php echo esc_attr( $value ); ?>"
<?php selected( $default_demo, $value ); ?>>
<?php echo esc_html( $label ); ?>
</option>
<?php endforeach; ?>
</select>
<p class="description">
<?php esc_html_e( 'Choose which demo to show by default in the import library.', 'versana-companion' ); ?>
</p>
</td>
</tr>
<!-- Show Welcome Notice -->
<tr>
<th scope="row">
<?php esc_html_e( 'Welcome Notice', 'versana-companion' ); ?>
</th>
<td>
<label>
<input type="checkbox"
name="versana_theme_options[show_welcome_notice]"
value="1"
<?php checked( isset( $options['show_welcome_notice'] ) ? $options['show_welcome_notice'] : true, true ); ?> />
<?php esc_html_e( 'Show welcome notice after activation', 'versana-companion' ); ?>
</label>
<p class="description">
<?php esc_html_e( 'Display a helpful welcome message when the plugin is first activated.', 'versana-companion' ); ?>
</p>
</td>
</tr>
</tbody>
</table>
<?php
/**
* Extensibility Hook: Add more companion settings
*
* Allows other plugins/child themes to add settings to this tab
*
* @since 1.0.0
*/
do_action( 'versana_companion_tab_settings' );
?>
</div>
<?php
}
Understanding the structure:
<div class="versana-tab-content">
- Wrapper class used by theme
- Provides consistent styling
$options = get_option( 'versana_theme_options', array() );
- Get theme options (includes our plugin settings!)
- We’re storing in the SAME option as theme
- This is the key to integration
<table class="form-table" role="presentation">
- WordPress standard table for settings
- Provides consistent layout
name="versana_theme_options[enable_demo_import]"
- Field name structure is CRITICAL
versana_theme_options= Option name (same as theme)[enable_demo_import]= Our setting key- This stores in theme’s options array
<?php checked( isset( $options['enable_demo_import'] ) ? $options['enable_demo_import'] : true, true ); ?>
checked()= WordPress helper for checkboxes- Adds
checked="checked"if value is true isset()checks if key exists- Defaults to
trueif not set
do_action( 'versana_companion_tab_settings' );
- Our own extensibility hook!
- Other plugins can add settings here
- Good practice to add hooks in your plugin too
Part 5: How Saving Works (Automatically!)
The Magic of Integration
Here’s the beautiful part: Saving happens automatically!
How it works:
- User clicks “Save Settings” button (theme’s button)
- Form submits to
options.php(WordPress settings API) - WordPress calls theme’s sanitize function
- Theme’s function processes ALL fields (including ours!)
- Everything saves to
versana_theme_optionsin database - Success message shows
- Page reloads with saved settings
Why it works:
- Our fields use
name="versana_theme_options[key]" - Form includes
settings_fields( 'versana_options' ) - Theme registered:
register_setting( 'versana_options', 'versana_theme_options' ) - WordPress connects the dots automatically!
You don’t need to write:
- ❌ Form processing code
- ❌ Database save functions
- ❌ Nonce verification (theme does it)
- ❌ Success messages
- ❌ Sanitization (well, we should add our part…)
Part 6: Adding Sanitization for Our Fields
Step 3: Hook into Theme’s Sanitization
The theme sanitizes settings, but we should add our own for our fields:
/**
* Sanitize companion plugin settings
*
* Adds sanitization for plugin-specific fields
*
* @param array $sanitized Already sanitized options
* @param array $input Raw input
* @return array Modified sanitized options
*/
function versana_companion_sanitize_settings( $sanitized, $input ) {
// Boolean fields (checkboxes)
$boolean_fields = array(
'enable_demo_import',
'show_welcome_notice',
);
foreach ( $boolean_fields as $field ) {
if ( isset( $input[ $field ] ) ) {
$sanitized[ $field ] = (bool) $input[ $field ];
} else {
// Checkbox not checked = false
$sanitized[ $field ] = false;
}
}
// Default demo (select field with allowed values)
if ( isset( $input['default_demo'] ) ) {
$allowed_demos = array( 'business', 'blog', 'portfolio' );
$value = sanitize_key( $input['default_demo'] );
$sanitized['default_demo'] = in_array( $value, $allowed_demos, true ) ? $value : 'business';
}
return $sanitized;
}
add_filter( 'versana_sanitize_options', 'versana_companion_sanitize_settings', 10, 2 );
Understanding sanitization:
add_filter( 'versana_sanitize_options', 'versana_companion_sanitize_settings', 10, 2 );
- Hook into theme’s sanitization filter
- Priority 10 = Default (runs with theme’s function)
2= Accept 2 parameters ($sanitized and $input)
$sanitized[ $field ] = (bool) $input[ $field ];
- Convert to boolean (true/false)
(bool)casting ensures proper type
$allowed_demos = array( 'business', 'blog', 'portfolio' );
- Whitelist of allowed values
- Only accept these, reject everything else
in_array( $value, $allowed_demos, true )
- Check if value is in allowed list
- Third parameter
true= Strict comparison
Part 7: Using Plugin Settings in Code
Step 4: Helper Function to Get Settings
Create a helper function to easily access your settings:
/**
* Get companion plugin setting
*
* Retrieves plugin setting from theme options
*
* @param string $key Setting key
* @param mixed $default Default value if not set
* @return mixed Setting value
*/
function versana_companion_get_setting( $key, $default = null ) {
$options = get_option( 'versana_theme_options', array() );
// Check if setting exists
if ( isset( $options[ $key ] ) ) {
return $options[ $key ];
}
// Return default if provided
if ( $default !== null ) {
return $default;
}
// Fallback defaults
$defaults = array(
'enable_demo_import' => true,
'default_demo' => 'business',
'show_welcome_notice' => true,
);
return isset( $defaults[ $key ] ) ? $defaults[ $key ] : null;
}
Usage anywhere in your plugin:
// Check if demo import is enabled
if ( versana_companion_get_setting( 'enable_demo_import' ) ) {
// Show demo import button
echo '<button>Import Demo</button>';
}
// Get default demo
$default = versana_companion_get_setting( 'default_demo' );
echo 'Default demo: ' . $default; // "business"
// Check welcome notice setting
if ( versana_companion_get_setting( 'show_welcome_notice' ) ) {
// Show welcome message
add_action( 'admin_notices', 'show_welcome' );
}
Part 8: Testing the Integration
Test Your Settings Tab
1. Go to theme options:
WordPress Admin → Appearance → Theme Options
2. Look for your tab:
You should see tabs:
[Companion] [Header] [Footer] [Blog] [Integrations] [Advanced]
↑
Your tab! (first position)
3. Click Companion tab:
Should show:
- Demo Import checkbox (checked by default)
- Default Demo dropdown (Business selected)
- Welcome Notice checkbox (checked by default)
4. Test changing settings:
- Uncheck “Enable Demo Import”
- Change default demo to “Blog”
- Click “Save Settings” button (at bottom)
5. Verify save worked:
Should see:
✓ Settings saved successfully.
Settings should persist:
- Demo Import = unchecked
- Default Demo = Blog
6. Test in code:
// Add temporary test to your plugin init
function versana_companion_test_settings() {
$demo_import = versana_companion_get_setting( 'enable_demo_import' );
$default = versana_companion_get_setting( 'default_demo' );
echo '<div class="notice notice-info">';
echo '<p>Demo Import: ' . ( $demo_import ? 'Enabled' : 'Disabled' ) . '</p>';
echo '<p>Default Demo: ' . esc_html( $default ) . '</p>';
echo '</div>';
}
add_action( 'admin_notices', 'versana_companion_test_settings' );
Visit any admin page, should see your settings values!
Part 9: Complete Integration Code
All Functions Together
Here’s the complete code for versana-companion.php:
<?php
/**
* Plugin Name: Versana Companion
* Description: Adds demo import and advanced features to Versana theme
* Version: 1.0.0
* Author: Your Name
* Text Domain: versana-companion
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Plugin constants
*/
define( 'VERSANA_COMPANION_VERSION', '1.0.0' );
define( 'VERSANA_COMPANION_PATH', plugin_dir_path( __FILE__ ) );
define( 'VERSANA_COMPANION_URL', plugin_dir_url( __FILE__ ) );
/**
* Initialize plugin
*/
function versana_companion_init() {
// Check theme compatibility (from Episode 3)
if ( ! versana_companion_is_theme_active() ) {
add_action( 'admin_notices', 'versana_companion_theme_error_notice' );
return;
}
// Add settings tab to theme options
add_filter( 'versana_option_tabs', 'versana_companion_add_settings_tab' );
// Add sanitization for our settings
add_filter( 'versana_sanitize_options', 'versana_companion_sanitize_settings', 10, 2 );
}
add_action( 'plugins_loaded', 'versana_companion_init' );
/**
* Check if Versana theme active (from Episode 3)
*/
function versana_companion_is_theme_active() {
$theme = wp_get_theme();
return ( 'Versana' === $theme->get('Name') || 'versana' === $theme->get('Template') );
}
/**
* Theme error notice (from Episode 3)
*/
function versana_companion_theme_error_notice() {
?>
<div class="notice notice-error is-dismissible">
<p>
<strong>Versana Companion Error:</strong>
This plugin requires the Versana theme.
</p>
</div>
<?php
}
/**
* Add Companion tab to theme options
*/
function versana_companion_add_settings_tab( $tabs ) {
$tabs['companion'] = array(
'title' => __( 'Companion', 'versana-companion' ),
'icon' => 'dashicons-admin-plugins',
'callback' => 'versana_companion_render_settings_tab',
'priority' => 10,
);
return $tabs;
}
/**
* Render Companion settings tab
*/
function versana_companion_render_settings_tab() {
$options = get_option( 'versana_theme_options', array() );
?>
<div class="versana-tab-content">
<h2><?php esc_html_e( 'Companion Plugin Settings', 'versana-companion' ); ?></h2>
<p class="description">
<?php esc_html_e( 'Configure Versana Companion plugin options.', 'versana-companion' ); ?>
</p>
<table class="form-table" role="presentation">
<tbody>
<tr>
<th scope="row">
<?php esc_html_e( 'Demo Import', 'versana-companion' ); ?>
</th>
<td>
<label>
<input type="checkbox"
name="versana_theme_options[enable_demo_import]"
value="1"
<?php checked( isset( $options['enable_demo_import'] ) ? $options['enable_demo_import'] : true, true ); ?> />
<?php esc_html_e( 'Enable one-click demo import feature', 'versana-companion' ); ?>
</label>
<p class="description">
<?php esc_html_e( 'Allow users to import demo content.', 'versana-companion' ); ?>
</p>
</td>
</tr>
<tr>
<th scope="row">
<label for="default_demo">
<?php esc_html_e( 'Default Demo', 'versana-companion' ); ?>
</label>
</th>
<td>
<select id="default_demo" name="versana_theme_options[default_demo]">
<?php
$default_demo = isset( $options['default_demo'] ) ? $options['default_demo'] : 'business';
$demos = array(
'business' => __( 'Business', 'versana-companion' ),
'blog' => __( 'Blog', 'versana-companion' ),
'portfolio' => __( 'Portfolio', 'versana-companion' ),
);
foreach ( $demos as $value => $label ) :
?>
<option value="<?php echo esc_attr( $value ); ?>" <?php selected( $default_demo, $value ); ?>>
<?php echo esc_html( $label ); ?>
</option>
<?php endforeach; ?>
</select>
<p class="description">
<?php esc_html_e( 'Default demo in import library.', 'versana-companion' ); ?>
</p>
</td>
</tr>
<tr>
<th scope="row">
<?php esc_html_e( 'Welcome Notice', 'versana-companion' ); ?>
</th>
<td>
<label>
<input type="checkbox"
name="versana_theme_options[show_welcome_notice]"
value="1"
<?php checked( isset( $options['show_welcome_notice'] ) ? $options['show_welcome_notice'] : true, true ); ?> />
<?php esc_html_e( 'Show welcome notice after activation', 'versana-companion' ); ?>
</label>
</td>
</tr>
</tbody>
</table>
<?php do_action( 'versana_companion_tab_settings' ); ?>
</div>
<?php
}
/**
* Sanitize companion settings
*/
function versana_companion_sanitize_settings( $sanitized, $input ) {
// Boolean fields
$boolean_fields = array(
'enable_demo_import',
'show_welcome_notice',
);
foreach ( $boolean_fields as $field ) {
$sanitized[ $field ] = isset( $input[ $field ] ) ? (bool) $input[ $field ] : false;
}
// Default demo
if ( isset( $input['default_demo'] ) ) {
$allowed = array( 'business', 'blog', 'portfolio' );
$value = sanitize_key( $input['default_demo'] );
$sanitized['default_demo'] = in_array( $value, $allowed, true ) ? $value : 'business';
}
return $sanitized;
}
/**
* Get companion setting
*/
function versana_companion_get_setting( $key, $default = null ) {
$options = get_option( 'versana_theme_options', array() );
if ( isset( $options[ $key ] ) ) {
return $options[ $key ];
}
if ( $default !== null ) {
return $default;
}
$defaults = array(
'enable_demo_import' => true,
'default_demo' => 'business',
'show_welcome_notice' => true,
);
return isset( $defaults[ $key ] ) ? $defaults[ $key ] : null;
}
/**
* Activation
*/
function versana_companion_activate() {
add_option( 'versana_companion_version', VERSANA_COMPANION_VERSION );
}
register_activation_hook( __FILE__, 'versana_companion_activate' );
/**
* Load text domain
*/
function versana_companion_load_textdomain() {
load_plugin_textdomain( 'versana-companion', false, dirname( plugin_basename( __FILE__ ) ) . '/languages' );
}
add_action( 'plugins_loaded', 'versana_companion_load_textdomain' );
Conclusion
Congratulations! You’ve successfully integrated your plugin settings into the Versana theme’s options page using WordPress hooks. This is professional plugin development at its best!
What We Accomplished
✅ Understanding WordPress hooks (filters and actions) ✅ Using theme’s extensibility system ✅ Adding custom tab with filter hook ✅ Creating settings fields ✅ Automatic saving (no extra code!) ✅ Sanitizing plugin settings ✅ Helper function for settings access ✅ Professional integration ✅ Clean, non-cluttered admin
What We Learned
Technical Skills:
- WordPress filters (
add_filter) - WordPress actions (
add_action,do_action) - Theme extensibility hooks
- Settings integration
- Automatic form processing
- Sanitization patterns
- Helper function patterns
Best Practices:
- Integrate with theme when possible
- Don’t clutter admin with separate menus
- Use existing theme systems
- Add your own extensibility hooks
- Provide helper functions
- Follow theme’s patterns
- Test integration thoroughly
Key Takeaways
For WordPress plugin settings integration:
- Use Theme Hooks – Don’t duplicate functionality
- Follow Theme Patterns – Match their structure
- Share Options – Store in theme’s option
- Add Sanitization – For your specific fields
- Provide Helpers – Easy access functions
- Add Your Hooks – For future extensibility
- Test Integration – Verify everything works
Your Plugin Progress
✓ Episode 2: Basic plugin
✓ Episode 3: Theme check
✓ Episode 4: Settings integration ← YOU ARE HERE
→ Episode 5: Database tables
→ Episode 6: File operations
→ Episode 7: Demo import
Frequently Asked Questions
Q: What if theme doesn’t have extensibility hooks? A: Then create your own admin page. This approach only works with themes that provide hooks.
Q: Can I still access settings if theme is deactivated? A: Settings are in versana_theme_options, so they stay even if theme changes. But your plugin won’t load without Versana.
Q: What if I want a separate menu anyway? A: That’s fine! This is just one approach. Separate menus work too, just more menu clutter.
Q: How do I know what hooks a theme provides? A: Check theme documentation, or search theme code for apply_filters and do_action.
Q: Can other plugins add to my tab? A: Yes! You added do_action( 'versana_companion_tab_settings' ) so others can hook in.
Q: What if setting keys conflict with theme? A: Use unique prefixes. We used enable_demo_import which is specific to our plugin.
Q: Do I need to create defaults? A: Yes, in your helper function. Always provide sensible defaults.
Q: Can I add multiple tabs? A: Yes! Just add more items to the tabs array with different keys and priorities.
Next Episode Preview
Episode 5: Creating Database Tables
Now that we can configure settings, we’ll learn to store more complex data in custom database tables for our demo catalog!
Congratulations on completing Episode 4! You’ve mastered WordPress plugin settings integration!
Series: Building Versana Companion Plugin
Episode: 4 of ongoing series (Settings Integration Complete)


Leave a Reply