Extending WordPress Data & Admin: CPTs, Settings API, and Metadata
A comprehensive architectural guide to data modeling and backend administration. This article covers the essential APIs required to build complex applications on WordPress: defining custom data structures via Post Types and Taxonomies, managing configuration persistence through the Settings API, and handling entity relationships with the Meta and Transients subsystems.
Admin Menus
add_menu_page()
Registers a top-level menu item in the WordPress admin sidebar; accepts parameters for page title, menu title, capability, slug, callback function, icon, and position. This is the foundation for creating custom admin interfaces for your plugin or theme.
add_action('admin_menu', function() { add_menu_page( 'My Plugin Settings', // Page title 'My Plugin', // Menu title 'manage_options', // Capability 'my-plugin-slug', // Menu slug 'my_plugin_render_page', // Callback function 'dashicons-admin-generic', // Icon 30 // Position ); });
add_submenu_page()
Creates a submenu item under any existing top-level menu by specifying the parent slug; useful for organizing multiple settings pages or features under a single parent menu without cluttering the admin sidebar.
add_submenu_page( 'my-plugin-slug', // Parent slug 'Advanced Settings', // Page title 'Advanced', // Menu title 'manage_options', // Capability 'my-plugin-advanced', // Submenu slug 'render_advanced_page' // Callback );
┌─────────────────────────┐
│ My Plugin ◄─────── Top-level (add_menu_page)
├─────────────────────────┤
│ ├── Dashboard ◄─────── Submenu (auto-created)
│ ├── Advanced ◄─────── Submenu (add_submenu_page)
│ └── Import ◄─────── Submenu (add_submenu_page)
└─────────────────────────┘
add_options_page()
A convenience wrapper around add_submenu_page() that automatically places your settings page under the "Settings" menu; ideal for plugins that only need a single configuration page without requiring a dedicated top-level menu.
add_action('admin_menu', function() { add_options_page( 'My Plugin Options', // Page title 'My Plugin', // Menu title 'manage_options', // Capability 'my-plugin-options', // Slug 'my_options_callback' // Render function ); }); // Result: Settings → My Plugin
add_management_page()
Wrapper function that adds a submenu page under the "Tools" menu; appropriate for import/export functionality, database maintenance utilities, or diagnostic tools that don't fit under Settings.
add_management_page( 'Export Data', // Page title 'Export My Data', // Menu title 'manage_options', // Capability 'my-plugin-export', // Slug 'render_export_page' // Callback ); // Result: Tools → Export My Data
add_theme_page()
Adds a submenu under "Appearance" menu; specifically designed for theme-related settings like layout options, color schemes, or typography controls that logically belong with other theme customization options.
add_theme_page( 'Theme Layout Options', 'Layout Options', 'edit_theme_options', // Theme-specific capability 'theme-layout', 'render_layout_options' ); // Result: Appearance → Layout Options
add_plugins_page()
Places a submenu item under the "Plugins" menu; rarely used but appropriate for plugin-specific utilities like license management, bulk plugin operations, or plugin-related diagnostics.
add_plugins_page( 'License Manager', 'Licenses', 'activate_plugins', 'license-manager', 'render_license_page' ); // Result: Plugins → Licenses
add_users_page()
Adds a submenu under "Users" menu; suitable for user-related features like bulk user operations, extended profile settings, or user analytics that complement WordPress's native user management.
add_users_page( 'User Analytics', 'Analytics', 'list_users', 'user-analytics', 'render_user_analytics' ); // Result: Users → Analytics
Menu Icons (Dashicons)
WordPress includes Dashicons, a built-in icon font with 300+ icons for admin menus; use the class name directly or pass a base64-encoded SVG, or a URL to a custom image (20x20 pixels recommended).
// Using Dashicons add_menu_page('Title', 'Menu', 'manage_options', 'slug', 'cb', 'dashicons-chart-pie'); // Using custom SVG (base64) add_menu_page('Title', 'Menu', 'manage_options', 'slug', 'cb', 'data:image/svg+xml;base64,' . base64_encode('<svg>...</svg>')); // Common Dashicons: // dashicons-admin-home dashicons-admin-settings // dashicons-admin-tools dashicons-chart-bar // dashicons-database dashicons-cloud
Menu Positioning
Menu position is controlled by a numeric value where lower numbers appear higher; standard WordPress menus occupy specific positions, so choose values between them to avoid collisions or use decimals for precision.
Position Map:
┌────────┬─────────────────────┐
│ 2 │ Dashboard │
│ 4 │ Separator │
│ 5 │ Posts │
│ 10 │ Media │
│ 20 │ Pages │
│ 25 │ Comments │
│ 59 │ Separator │
│ 60 │ Appearance │
│ 65 │ Plugins │
│ 70 │ Users │
│ 75 │ Tools │
│ 80 │ Settings │
│ 99 │ Separator │
└────────┴─────────────────────┘
// Use decimals to avoid conflicts
add_menu_page(..., 26.5); // Between Comments and Appearance
Capability Requirements
Every menu page requires a capability string that determines which user roles can access it; always use the minimum required capability following the principle of least privilege to maintain security.
// Common capabilities hierarchy: // manage_options → Administrators only // edit_others_posts → Editors and above // publish_posts → Authors and above // edit_posts → Contributors and above // read → All logged-in users add_menu_page( 'Editor Tools', 'Editor Tools', 'edit_others_posts', // Editors+ can see this 'editor-tools', 'render_editor_tools' ); // Custom capability check if (current_user_can('manage_options')) { // Admin-only code }
Settings API
register_setting()
Registers a setting to be saved in the options table and associates it with a settings group; this is required before any setting can be saved via the Settings API and enables automatic sanitization/validation.
add_action('admin_init', function() { register_setting( 'my_plugin_settings_group', // Option group 'my_plugin_option_name', // Option name in DB [ 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', 'default' => 'default_value' ] ); });
add_settings_section()
Creates a logical grouping of related settings fields within a settings page; sections provide visual organization with an optional title and description callback that renders before the fields.
add_settings_section( 'general_section', // Section ID 'General Settings', // Section title function() { // Callback for description echo '<p>Configure basic plugin settings below.</p>'; }, 'my-plugin-settings' // Page slug where section appears );
┌─────────────────────────────────────┐
│ General Settings ◄─── Section Title
│ Configure basic settings... ◄─── Section Callback
├─────────────────────────────────────┤
│ Field 1: [___________] ◄─── Settings Fields
│ Field 2: [___________]
└─────────────────────────────────────┘
add_settings_field()
Registers an individual form field within a settings section; the callback function renders the actual HTML input element, giving you full control over the field's markup and behavior.
add_settings_field( 'api_key_field', // Field ID 'API Key', // Field label function() { // Render callback $value = get_option('my_plugin_api_key', ''); echo '<input type="text" name="my_plugin_api_key" value="' . esc_attr($value) . '" class="regular-text">'; }, 'my-plugin-settings', // Page slug 'general_section' // Section ID );
settings_fields()
Outputs required hidden fields (nonce, action, option_page) for the settings form; must be called inside your <form> tag to enable WordPress's automatic settings saving and security verification.
function render_settings_page() { ?> <div class="wrap"> <h1>My Plugin Settings</h1> <form method="post" action="options.php"> <?php settings_fields('my_plugin_settings_group'); // Hidden fields do_settings_sections('my-plugin-settings'); submit_button(); ?> </form> </div> <?php }
do_settings_sections()
Renders all registered sections and their fields for a specific page slug; this single function call outputs the complete settings form content based on your add_settings_section() and add_settings_field() registrations.
// This single call outputs: do_settings_sections('my-plugin-settings'); // Resulting structure: // ┌─ Section 1 ─────────────────┐ // │ Field 1.1 │ // │ Field 1.2 │ // └─────────────────────────────┘ // ┌─ Section 2 ─────────────────┐ // │ Field 2.1 │ // │ Field 2.2 │ // └─────────────────────────────┘
Settings Sanitization
Sanitization cleans and normalizes input data before saving to the database; always define a sanitize callback in register_setting() to strip malicious content, enforce data types, and ensure consistent formatting.
register_setting('my_group', 'my_option', [ 'sanitize_callback' => 'my_sanitize_function' ]); function my_sanitize_function($input) { $sanitized = []; // Sanitize text field $sanitized['title'] = sanitize_text_field($input['title']); // Sanitize email $sanitized['email'] = sanitize_email($input['email']); // Sanitize integer $sanitized['count'] = absint($input['count']); // Sanitize URL $sanitized['url'] = esc_url_raw($input['url']); return $sanitized; }
Settings Validation
Validation verifies that sanitized data meets business requirements; unlike sanitization (which transforms data), validation checks correctness and rejects invalid input with error messages using add_settings_error().
function validate_my_settings($input) { $input = my_sanitize_function($input); // Validate API key format if (!preg_match('/^[a-zA-Z0-9]{32}$/', $input['api_key'])) { add_settings_error( 'my_plugin_settings', 'invalid_api_key', 'API key must be 32 alphanumeric characters.', 'error' ); $input['api_key'] = get_option('my_option')['api_key']; // Keep old value } // Validate numeric range if ($input['count'] < 1 || $input['count'] > 100) { add_settings_error('my_plugin_settings', 'invalid_count', 'Count must be between 1 and 100.', 'error'); } return $input; }
Settings Errors
Error handling system that stores and displays validation messages; use add_settings_error() to queue messages during validation and settings_errors() in your page template to display them with appropriate styling.
// Adding errors (in sanitize/validate callback) add_settings_error( 'my_settings', // Setting slug 'my_error_code', // Error code 'Your error message.', // Message 'error' // Type: error, warning, success, info ); // Displaying errors (in page render callback) function render_settings_page() { ?> <div class="wrap"> <h1>Settings</h1> <?php settings_errors(); // Displays all queued messages ?> <form method="post" action="options.php"> <!-- form content --> </form> </div> <?php }
Options API
add_option()
Creates a new option in the wp_options table only if it doesn't already exist; useful for setting default values during plugin activation without overwriting user-modified settings.
// Basic usage add_option('my_plugin_version', '1.0.0'); // With all parameters add_option( 'my_plugin_settings', // Option name ['key' => 'value'], // Value (can be array) '', // Deprecated parameter 'yes' // Autoload: 'yes' or 'no' ); // Common pattern in activation hook register_activation_hook(__FILE__, function() { add_option('my_plugin_installed', time()); add_option('my_plugin_config', [ 'enabled' => true, 'limit' => 10 ]); });
update_option()
Saves or updates an option value in the database; if the option doesn't exist, it creates it (unlike add_option), making it the go-to function for saving settings in most scenarios.
// Simple update update_option('my_plugin_counter', 42); // Update with autoload control update_option('large_data_blob', $data, false); // Don't autoload // Common pattern: increment counter $count = get_option('visit_count', 0); update_option('visit_count', $count + 1); // Returns true if value changed, false if same or error if (update_option('my_setting', $new_value)) { // Value was actually updated }
get_option()
Retrieves an option value from the database with optional default fallback; autoloaded options are cached in memory, so repeated calls within a request don't hit the database.
// Basic retrieval with default $value = get_option('my_plugin_setting', 'default_value'); // Array option $config = get_option('my_plugin_config', [ 'enabled' => false, 'limit' => 5 ]); // Check if option exists (returns false if not found) $exists = get_option('maybe_exists'); if ($exists === false) { // Option doesn't exist OR value is literally false } // Safer existence check $value = get_option('my_option', 'NOT_FOUND_SENTINEL'); if ($value === 'NOT_FOUND_SENTINEL') { // Definitely doesn't exist }
delete_option()
Removes an option completely from the database; essential for clean uninstallation to avoid leaving orphaned data, typically called in uninstall.php or a deactivation hook.
// Basic deletion delete_option('my_plugin_setting'); // Cleanup pattern in uninstall.php if (!defined('WP_UNINSTALL_PLUGIN')) exit; // Delete all plugin options $options_to_delete = [ 'my_plugin_version', 'my_plugin_settings', 'my_plugin_cache' ]; foreach ($options_to_delete as $option) { delete_option($option); } // Delete options with prefix pattern global $wpdb; $wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name LIKE 'my_plugin_%'");
Autoload Parameter
Controls whether an option is loaded into memory on every WordPress request; set to 'no' for large data or infrequently accessed options to reduce memory usage and improve performance.
// Autoload YES (default) - loaded on every page add_option('frequently_used', $value, '', 'yes'); // Autoload NO - only loaded when explicitly requested add_option('large_json_blob', $huge_data, '', 'no'); update_option('infrequent_data', $value, false); // 3rd param = autoload // Check autoload status global $wpdb; $autoload = $wpdb->get_var( $wpdb->prepare( "SELECT autoload FROM {$wpdb->options} WHERE option_name = %s", 'my_option' ) );
Memory Impact:
┌────────────────────────────────────────────┐
│ WordPress Init │
│ ├── Load autoload='yes' options (1 query) │
│ │ └── All in memory immediately │
│ └── autoload='no' options │
│ └── Separate query when needed │
└────────────────────────────────────────────┘
Option Groups
Logical grouping used by the Settings API to manage permissions and nonce verification; option groups link registered settings together for secure batch saving via options.php.
// Register multiple options in one group add_action('admin_init', function() { // Group: my_plugin_group register_setting('my_plugin_group', 'my_plugin_option_a'); register_setting('my_plugin_group', 'my_plugin_option_b'); register_setting('my_plugin_group', 'my_plugin_option_c'); }); // Form uses the group name settings_fields('my_plugin_group'); // Authorizes saving all 3 options // Alternative: single serialized option (often cleaner) register_setting('my_plugin_group', 'my_plugin_all_settings'); // Stores: ['option_a' => x, 'option_b' => y, 'option_c' => z]
Serialized Options
Storing arrays or objects as a single option value; WordPress automatically serializes PHP arrays/objects when saving and unserializes when retrieving, enabling complex configuration in one database row.
// Saving array as single option $settings = [ 'api_key' => 'abc123', 'enabled' => true, 'limits' => [ 'posts' => 10, 'users' => 50 ], 'features' => ['feature_a', 'feature_b'] ]; update_option('my_plugin_settings', $settings); // Database stores: a:4:{s:7:"api_key";s:6:"abc123";...} // Retrieval (automatic unserialization) $settings = get_option('my_plugin_settings', []); echo $settings['limits']['posts']; // 10 // Update single nested value $settings = get_option('my_plugin_settings', []); $settings['limits']['posts'] = 20; update_option('my_plugin_settings', $settings);
Transients API
set_transient()
Stores a value with an expiration time for temporary caching; data persists across requests and expires automatically, making it ideal for caching API responses, complex queries, or computed data.
// Cache API response for 1 hour $api_data = wp_remote_get('https://api.example.com/data'); set_transient('my_api_cache', $api_data, HOUR_IN_SECONDS); // Time constants available: // MINUTE_IN_SECONDS = 60 // HOUR_IN_SECONDS = 3600 // DAY_IN_SECONDS = 86400 // WEEK_IN_SECONDS = 604800 // Complex query caching $results = $wpdb->get_results($expensive_query); set_transient('expensive_query_cache', $results, 12 * HOUR_IN_SECONDS);
get_transient()
Retrieves a cached transient value; returns false if the transient doesn't exist or has expired, making it easy to implement cache-or-fetch patterns in your code.
// Standard cache-or-fetch pattern function get_cached_api_data() { $cached = get_transient('my_api_cache'); if ($cached !== false) { return $cached; // Cache hit } // Cache miss - fetch fresh data $response = wp_remote_get('https://api.example.com/data'); $data = json_decode(wp_remote_retrieve_body($response), true); // Store for future requests set_transient('my_api_cache', $data, HOUR_IN_SECONDS); return $data; }
delete_transient()
Manually removes a transient before its natural expiration; essential for cache invalidation when underlying data changes, ensuring users see fresh content immediately.
// Delete specific transient delete_transient('my_api_cache'); // Invalidate cache when related data changes add_action('save_post', function($post_id) { delete_transient('recent_posts_cache'); delete_transient('post_count_cache'); }); // Bulk delete pattern (transients with prefix) global $wpdb; $wpdb->query( "DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_my_plugin_%' OR option_name LIKE '_transient_timeout_my_plugin_%'" );
Transient Expiration
Expiration is specified in seconds at creation time; WordPress cleans expired transients lazily (on next access) or via garbage collection, and setting expiration to 0 means no automatic expiration.
// Various expiration examples set_transient('short_cache', $data, 5 * MINUTE_IN_SECONDS); // 5 minutes set_transient('medium_cache', $data, 6 * HOUR_IN_SECONDS); // 6 hours set_transient('long_cache', $data, 7 * DAY_IN_SECONDS); // 1 week // No expiration (persists until deleted) set_transient('permanent_cache', $data, 0); // Dynamic expiration based on content type $expiration = ($is_volatile_data) ? MINUTE_IN_SECONDS : DAY_IN_SECONDS; set_transient('dynamic_cache', $data, $expiration);
Transient Lifecycle:
┌──────────┐ ┌──────────┐ ┌──────────┐
│ SET │────►│ STORED │────►│ EXPIRED │
│ (+ TTL) │ │ │ │ │
└──────────┘ └──────────┘ └──────────┘
│ │
▼ ▼
┌──────────┐ ┌──────────┐
│ GET │ │ GET │
│ (cached) │ │ (false) │
└──────────┘ └──────────┘
Site Transients
Multisite-aware transients that are stored network-wide rather than per-site; use set_site_transient(), get_site_transient(), and delete_site_transient() for data that should be shared across all sites in a network.
// Site-wide transient (shared across multisite network) set_site_transient('network_stats', $stats, DAY_IN_SECONDS); $stats = get_site_transient('network_stats'); delete_site_transient('network_stats'); // Single site: stored in wp_options // Multisite: stored in wp_sitemeta (network-wide) // Use case: license validation across network set_site_transient('license_valid', true, WEEK_IN_SECONDS); // Comparison: // set_transient() → wp_options (site-specific) // set_site_transient() → wp_sitemeta (network-wide)
Transients vs Options
Transients are for temporary/cached data with expiration while options are for permanent settings; transients may be stored in external object cache (Redis/Memcached) while options always persist in the database.
┌─────────────────────┬─────────────────────────────────────┐
│ Feature │ Options │ Transients │
├─────────────────────┼──────────────────┼──────────────────┤
│ Expiration │ Never │ Yes (TTL-based) │
│ Purpose │ Permanent config │ Temporary cache │
│ External cache │ No │ Yes (if enabled) │
│ Delete on loss │ Data loss │ Just re-cache │
│ Storage │ wp_options │ wp_options OR │
│ │ │ object cache │
└─────────────────────┴──────────────────┴──────────────────┘
// Rule of thumb:
// - User settings → Options
// - API responses → Transients
// - Computed data → Transients
// - Feature flags → Options
Object Cache Fallback
Transients automatically utilize external object caches (Redis, Memcached) when available via a drop-in; without an object cache, they fall back to the wp_options table, providing seamless performance scaling.
// Same code works everywhere - storage is automatic: set_transient('my_cache', $data, HOUR_IN_SECONDS); // Without object cache: // └── Stored in wp_options table // With object cache (Redis/Memcached): // └── Stored in memory cache (much faster) // └── Never touches database // Check if object cache is available if (wp_using_ext_object_cache()) { // Redis/Memcached active - transients are fast } else { // Database fallback - consider longer TTLs } // Flow diagram: // set_transient() // │ // ▼ // ┌─────────────────────┐ // │ External cache │──YES──► Store in Redis/Memcached // │ available? │ // └─────────────────────┘ // │ NO // ▼ // ┌─────────────────────┐ // │ Store in wp_options │ // │ with timeout │ // └─────────────────────┘
Custom Post Types
register_post_type()
The core WordPress function that creates custom content types beyond default posts/pages; called during init hook with a unique slug (max 20 chars, no uppercase) and an array of configuration arguments that define behavior, visibility, and capabilities of the new content type.
add_action('init', function() { register_post_type('product', [ 'public' => true, 'label' => 'Products' ]); });
Post type arguments
An associative array passed as the second parameter to register_post_type() controlling all aspects of the CPT including visibility (public, show_ui), query behavior (publicly_queryable, exclude_from_search), admin interface (show_in_menu, menu_position), and data structure (hierarchical, supports).
┌─────────────────────────────────────────────────────┐
│ POST TYPE ARGUMENTS │
├─────────────────────────────────────────────────────┤
│ public ─────────► Controls overall visibility │
│ ├── show_ui │
│ ├── publicly_queryable │
│ ├── show_in_nav_menus │
│ └── exclude_from_search │
├─────────────────────────────────────────────────────┤
│ labels ─────────► Admin interface text │
│ supports ───────► Editor features │
│ capabilities ───► Permission mapping │
│ rewrite ────────► URL structure │
│ taxonomies ─────► Associated categories/tags │
└─────────────────────────────────────────────────────┘
Labels array
Defines all translatable UI strings shown in WordPress admin for your CPT, including menu names, button text, and messages; WordPress provides defaults based on label but explicit labels array ensures proper grammar and internationalization.
'labels' => [ 'name' => __('Products', 'textdomain'), 'singular_name' => __('Product', 'textdomain'), 'add_new' => __('Add New', 'textdomain'), 'add_new_item' => __('Add New Product', 'textdomain'), 'edit_item' => __('Edit Product', 'textdomain'), 'new_item' => __('New Product', 'textdomain'), 'view_item' => __('View Product', 'textdomain'), 'search_items' => __('Search Products', 'textdomain'), 'not_found' => __('No products found', 'textdomain'), 'not_found_in_trash' => __('No products found in Trash', 'textdomain'), 'all_items' => __('All Products', 'textdomain'), ]
Capabilities
Maps custom post type actions to WordPress capabilities, determining which user roles can create, edit, delete, and publish content; use capability_type for simple mapping or capabilities array for granular control, combined with map_meta_cap => true.
// Simple: inherits from 'post' or custom type 'capability_type' => 'product', 'map_meta_cap' => true, // This creates capabilities: // edit_product, read_product, delete_product // edit_products, edit_others_products, publish_products // read_private_products, delete_products, etc. // Then assign to roles: $role = get_role('editor'); $role->add_cap('edit_products'); $role->add_cap('publish_products');
Supports array
Specifies which core WordPress features are enabled for the post type editor, including title, content editor, featured image, excerpts, comments, revisions, and custom fields; omitting a feature removes it from the edit screen.
'supports' => [ 'title', // Post title 'editor', // Content editor (Gutenberg/Classic) 'thumbnail', // Featured image 'excerpt', // Excerpt field 'comments', // Comments 'trackbacks', // Trackbacks 'revisions', // Revision history 'author', // Author selector 'page-attributes', // Menu order, parent (if hierarchical) 'post-formats', // Post formats 'custom-fields', // Custom fields meta box ] // Add support later: add_post_type_support('product', 'excerpt'); // Remove support: remove_post_type_support('product', 'comments');
REST API integration
Controls whether the custom post type is accessible via WordPress REST API using show_in_rest, rest_base for endpoint slug, and rest_controller_class for custom controller; essential for Gutenberg editor support.
register_post_type('product', [ 'public' => true, 'show_in_rest' => true, // Enable REST API 'rest_base' => 'products', // /wp-json/wp/v2/products 'rest_controller_class' => 'WP_REST_Posts_Controller', 'supports' => ['title', 'editor'], // Required for Gutenberg ]); // API Endpoints created: // GET /wp-json/wp/v2/products // POST /wp-json/wp/v2/products // GET /wp-json/wp/v2/products/{id} // PUT /wp-json/wp/v2/products/{id} // DELETE /wp-json/wp/v2/products/{id}
Taxonomies assignment
Associates existing or custom taxonomies with the post type either during registration via taxonomies argument or later using register_taxonomy_for_object_type(), enabling categorization and filtering of custom content.
// Method 1: During CPT registration register_post_type('product', [ 'public' => true, 'taxonomies' => ['category', 'post_tag', 'product_type'], ]); // Method 2: After registration register_taxonomy_for_object_type('category', 'product'); // Method 3: During taxonomy registration register_taxonomy('brand', ['product', 'post'], [ 'label' => 'Brands', 'hierarchical' => true, ]);
Rewrite rules
Configures URL structure for the CPT using rewrite argument with options for custom slug, pagination, feeds, and endpoint masks; set to false to disable pretty permalinks entirely.
'rewrite' => [ 'slug' => 'shop/products', // URL: /shop/products/my-product 'with_front' => false, // Don't prepend blog prefix 'feeds' => true, // Enable RSS feeds 'pages' => true, // Enable pagination 'ep_mask' => EP_PERMALINK, // Endpoint mask ] // URL Structure: // Single: example.com/shop/products/product-name/ // Archive: example.com/shop/products/ // Feed: example.com/shop/products/feed/ // Page 2: example.com/shop/products/page/2/
Menu position
Integer value determining where the CPT appears in WordPress admin sidebar; common positions are 5 (below Posts), 20 (below Pages), 25 (below Comments), with decimals like 25.5 for precise placement between existing items.
┌──────────────────────────────────┐
│ WordPress Admin Menu Positions │
├──────────────────────────────────┤
│ 2 - Dashboard │
│ 4 - Separator │
│ 5 - Posts │
│ 10 - Media │
│ 15 - Links │
│ 20 - Pages │
│ 25 - Comments │
│ 59 - Separator │
│ 60 - Appearance │
│ 65 - Plugins │
│ 70 - Users │
│ 75 - Tools │
│ 80 - Settings │
│ 99 - Separator │
└──────────────────────────────────┘
'menu_position' => 5.5 // After Posts, before Media
Menu icon
Specifies the admin menu icon using Dashicons class name, custom image URL, base64-encoded SVG, or 'none' for CSS-based icons; Dashicons preferred for consistency with WordPress UI.
// Dashicons (recommended) 'menu_icon' => 'dashicons-cart' // Custom image URL 'menu_icon' => get_template_directory_uri() . '/icons/product.png' // Base64 SVG 'menu_icon' => 'data:image/svg+xml;base64,' . base64_encode('<svg>...</svg>') // Common Dashicons: // dashicons-admin-post dashicons-format-gallery // dashicons-admin-page dashicons-products // dashicons-cart dashicons-store // dashicons-calendar dashicons-portfolio // dashicons-location dashicons-book
Show in nav menus
Boolean show_in_nav_menus argument controls whether the CPT appears in Appearance → Menus for adding to navigation menus; defaults to value of public but can be overridden for internal-use post types.
register_post_type('product', [ 'public' => true, 'show_in_nav_menus' => true, // Appears in menu builder ]); register_post_type('order', [ 'public' => false, 'show_ui' => true, 'show_in_nav_menus' => false, // Internal CPT, no menu option ]);
Archive pages
Enable archive listing pages with has_archive set to true or a custom slug string; creates a template-loadable page at /post-type-slug/ or custom path, following template hierarchy archive-{post_type}.php.
'has_archive' => true, // Uses: /products/ 'has_archive' => 'all-products', // Uses: /all-products/ // Template Hierarchy for Archives: // 1. archive-product.php // 2. archive.php // 3. index.php // Query archive in templates: if (is_post_type_archive('product')) { // Product archive page }
Flushing rewrite rules
After registering new CPTs, rewrite rules must be flushed using flush_rewrite_rules() to update .htaccess; never call on every page load—only on plugin activation/deactivation hooks to avoid performance issues.
// CORRECT: On plugin activation register_activation_hook(__FILE__, function() { my_register_post_types(); // Register CPT first flush_rewrite_rules(); // Then flush }); register_deactivation_hook(__FILE__, function() { flush_rewrite_rules(); }); // WRONG: Never do this! add_action('init', function() { register_post_type('product', [...]); flush_rewrite_rules(); // ❌ Kills performance! }); // Alternative: Visit Settings → Permalinks → Save
Custom Taxonomies
register_taxonomy()
Core function to create custom classification systems for organizing content, called during init hook with taxonomy slug, associated post types array, and configuration arguments; must be registered before associated post types or use priority.
add_action('init', function() { register_taxonomy('genre', ['book', 'post'], [ 'label' => 'Genres', 'hierarchical' => true, 'show_in_rest' => true, 'rewrite' => ['slug' => 'book-genre'], ]); }, 0); // Priority 0 to register before CPTs
Taxonomy arguments
Configuration array controlling taxonomy behavior including hierarchy type, admin visibility, query behavior, REST API exposure, and URL structure; most arguments mirror post type options for consistency.
$args = [ 'hierarchical' => true, // Category-style vs tag-style 'public' => true, // Publicly queryable 'show_ui' => true, // Admin interface 'show_in_menu' => true, // In admin menu 'show_in_nav_menus' => true, // In nav menu builder 'show_in_rest' => true, // REST API + Gutenberg 'show_admin_column' => true, // Column in post list 'query_var' => true, // Enable ?genre=fiction 'rewrite' => ['slug' => 'genre'], 'capabilities' => [], 'labels' => [], ];
Hierarchical vs non-hierarchical
hierarchical => true creates category-like taxonomies with parent-child relationships and checkbox UI; false creates tag-like flat taxonomies with comma-separated input field and no nesting support.
HIERARCHICAL (true) NON-HIERARCHICAL (false)
┌─────────────────────┐ ┌─────────────────────┐
│ ☑ Fiction │ │ Tags: │
│ ☐ Science Fiction │ │ ┌─────────────────┐ │
│ ☑ Fantasy │ │ │ red, blue, sale │ │
│ ☐ Epic Fantasy │ │ └─────────────────┘ │
│ ☐ Non-Fiction │ │ Separate with commas│
└─────────────────────┘ └─────────────────────┘
// Parent-child supported // Flat structure only
get_term_children($parent_id) // No nesting available
Labels array (Taxonomies)
Defines all UI text strings for taxonomy admin screens including menu labels, form text, and messages; get_taxonomy_labels() provides defaults but explicit labels ensure proper singular/plural forms.
'labels' => [ 'name' => _x('Genres', 'taxonomy general name'), 'singular_name' => _x('Genre', 'taxonomy singular name'), 'search_items' => __('Search Genres'), 'all_items' => __('All Genres'), 'parent_item' => __('Parent Genre'), // Hierarchical only 'parent_item_colon' => __('Parent Genre:'), 'edit_item' => __('Edit Genre'), 'update_item' => __('Update Genre'), 'add_new_item' => __('Add New Genre'), 'new_item_name' => __('New Genre Name'), 'menu_name' => __('Genres'), 'not_found' => __('No genres found'), 'back_to_items' => __('← Back to Genres'), ]
Rewrite rules (Taxonomies)
Configures taxonomy URL structure with custom slug, optional hierarchical paths for nested terms, and front-page prefix control; archives display at /taxonomy-slug/term-slug/.
'rewrite' => [ 'slug' => 'book-genre', 'with_front' => false, 'hierarchical' => true, // Allow /genre/parent/child/ 'ep_mask' => EP_NONE, ] // URLs Generated: // /book-genre/fiction/ // /book-genre/fiction/sci-fi/ (if hierarchical) // Disable rewrites: 'rewrite' => false // Query only via ?taxonomy=term
REST API integration (Taxonomies)
Enable REST API access with show_in_rest, customize endpoint with rest_base, and provide custom controller via rest_controller_class; required for Gutenberg block editor taxonomy panels.
register_taxonomy('genre', 'post', [ 'show_in_rest' => true, 'rest_base' => 'genres', 'rest_controller_class' => 'WP_REST_Terms_Controller', ]); // Endpoints: // GET /wp-json/wp/v2/genres // POST /wp-json/wp/v2/genres // GET /wp-json/wp/v2/genres/{id} // PUT /wp-json/wp/v2/genres/{id} // DELETE /wp-json/wp/v2/genres/{id}
Default terms
Set default term(s) automatically assigned to posts when no term is selected using default_term argument (WP 5.5+); term is created if it doesn't exist.
register_taxonomy('status', 'product', [ 'hierarchical' => true, 'default_term' => [ 'name' => 'Draft', 'slug' => 'draft', 'description' => 'Product pending review', ], ]); // Pre-5.5 alternative: add_action('save_post_product', function($post_id) { if (!has_term('', 'status', $post_id)) { wp_set_object_terms($post_id, 'draft', 'status'); } });
Taxonomy capabilities
Controls permissions for managing terms using capabilities array mapping meta capabilities to primitive capabilities; combine with map_meta_cap handling in custom capability setups.
'capabilities' => [ 'manage_terms' => 'manage_genres', // Access taxonomy admin 'edit_terms' => 'edit_genres', // Edit terms 'delete_terms' => 'delete_genres', // Delete terms 'assign_terms' => 'assign_genres', // Assign to posts ] // Grant to role: $editor = get_role('editor'); $editor->add_cap('manage_genres'); $editor->add_cap('edit_genres'); $editor->add_cap('delete_genres'); $editor->add_cap('assign_genres');
Meta Boxes
add_meta_box()
Registers custom meta boxes on post editing screens using add_meta_boxes hook; parameters include unique ID, title, callback function, screen(s), context placement, and priority for ordering.
add_action('add_meta_boxes', function() { add_meta_box( 'product_details', // Unique ID 'Product Details', // Title 'render_product_meta_box', // Callback function 'product', // Screen (post type) 'normal', // Context 'high' // Priority ); }); function render_product_meta_box($post) { $price = get_post_meta($post->ID, '_product_price', true); echo '<input type="text" name="product_price" value="' . esc_attr($price) . '">'; }
remove_meta_box()
Removes default or custom meta boxes from edit screens; call during add_meta_boxes hook with the meta box ID, screen, and context where it was registered.
add_action('add_meta_boxes', function() { // Remove default meta boxes remove_meta_box('slugdiv', 'post', 'normal'); // Slug remove_meta_box('commentsdiv', 'post', 'normal'); // Comments remove_meta_box('commentstatusdiv', 'post', 'normal'); // Discussion remove_meta_box('authordiv', 'post', 'normal'); // Author remove_meta_box('postexcerpt', 'post', 'normal'); // Excerpt remove_meta_box('trackbacksdiv', 'post', 'normal'); // Trackbacks remove_meta_box('postcustom', 'post', 'normal'); // Custom Fields remove_meta_box('revisionsdiv', 'post', 'normal'); // Revisions // Remove from page remove_meta_box('pageparentdiv', 'page', 'side'); // Page Attributes });
Meta box callback
Function that outputs the HTML content of the meta box; receives $post object and $metabox array (containing 'id', 'title', 'callback', 'args') as parameters.
function render_product_meta_box($post, $metabox) { // Security nonce wp_nonce_field('product_meta_box', 'product_meta_nonce'); // Get existing values $price = get_post_meta($post->ID, '_price', true); $sku = get_post_meta($post->ID, '_sku', true); // Access callback args $custom_arg = $metabox['args']['custom_setting'] ?? ''; ?> <p> <label>Price: $</label> <input type="number" name="price" value="<?php echo esc_attr($price); ?>"> </p> <p> <label>SKU:</label> <input type="text" name="sku" value="<?php echo esc_attr($sku); ?>"> </p> <?php }
Meta box context
Determines screen location using 'normal' (main column), 'side' (sidebar), or 'advanced' (below normal); controls where the meta box appears on the edit screen layout.
┌─────────────────────────────────────────────────────────┐
│ EDIT SCREEN LAYOUT │
├───────────────────────────────────┬─────────────────────┤
│ │ │
│ NORMAL CONTEXT │ SIDE CONTEXT │
│ ┌─────────────────────────┐ │ ┌───────────────┐ │
│ │ Meta Box (high) │ │ │ Publish │ │
│ └─────────────────────────┘ │ ├───────────────┤ │
│ ┌─────────────────────────┐ │ │ Categories │ │
│ │ Meta Box (core) │ │ ├───────────────┤ │
│ └─────────────────────────┘ │ │ Custom Side │ │
│ │ │ Meta Box │ │
│ ADVANCED CONTEXT │ └───────────────┘ │
│ ┌─────────────────────────┐ │ │
│ │ Meta Box (default) │ │ │
│ └─────────────────────────┘ │ │
└───────────────────────────────────┴─────────────────────┘
Meta box priority
Controls vertical ordering within a context using 'high', 'core', 'default', or 'low'; meta boxes with same priority are ordered by registration sequence.
// Priorities within same context (top to bottom): // 1. high - Important, user-facing // 2. core - WordPress core boxes // 3. default - Standard plugins // 4. low - Less important info add_meta_box('important_box', 'Important', $cb, 'post', 'normal', 'high'); add_meta_box('standard_box', 'Standard', $cb, 'post', 'normal', 'default'); add_meta_box('info_box', 'Extra Info', $cb, 'post', 'normal', 'low');
Saving meta box data
Handle saving via save_post or save_post_{post_type} hook, with validation for autosave, permissions, nonce verification, and post type checks before updating meta.
add_action('save_post_product', function($post_id, $post, $update) { // Skip autosave if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) return; // Verify nonce if (!isset($_POST['product_meta_nonce'])) return; if (!wp_verify_nonce($_POST['product_meta_nonce'], 'product_meta_box')) return; // Check permissions if (!current_user_can('edit_post', $post_id)) return; // Sanitize and save if (isset($_POST['price'])) { update_post_meta($post_id, '_price', floatval($_POST['price'])); } if (isset($_POST['sku'])) { update_post_meta($post_id, '_sku', sanitize_text_field($_POST['sku'])); } }, 10, 3);
Nonce verification
Security mechanism preventing CSRF attacks by generating unique tokens with wp_nonce_field() in the form and validating with wp_verify_nonce() on save; essential for any form submission handling.
// In meta box callback - generate nonce function render_meta_box($post) { wp_nonce_field('my_meta_action', 'my_meta_nonce'); // ... form fields } // On save - verify nonce add_action('save_post', function($post_id) { // Check nonce exists if (!isset($_POST['my_meta_nonce'])) { return $post_id; } // Verify nonce is valid if (!wp_verify_nonce($_POST['my_meta_nonce'], 'my_meta_action')) { return $post_id; } // Safe to process data update_post_meta($post_id, '_my_field', sanitize_text_field($_POST['my_field'])); });
Meta box styling
Custom CSS for meta boxes using admin-specific stylesheets enqueued via admin_enqueue_scripts hook, targeting meta box IDs and WordPress admin classes for consistent appearance.
add_action('admin_enqueue_scripts', function($hook) { if (!in_array($hook, ['post.php', 'post-new.php'])) return; wp_enqueue_style('my-metabox-styles', plugin_dir_url(__FILE__) . 'css/metabox.css'); });
/* metabox.css */ #product_details .inside { padding: 12px; } #product_details label { display: block; font-weight: 600; margin-bottom: 5px; } #product_details input[type="text"], #product_details input[type="number"] { width: 100%; padding: 8px; border: 1px solid #8c8f94; border-radius: 4px; } #product_details .field-group { margin-bottom: 15px; } #product_details .description { color: #646970; font-style: italic; }
Post Meta API
add_post_meta()
Adds a new meta field to a post; allows duplicate keys when fourth parameter is false (default), returns meta ID on success, or false if key exists with unique constraint.
// Add single value (unique = true prevents duplicates) add_post_meta($post_id, '_product_price', 29.99, true); // Add multiple values with same key (unique = false) add_post_meta($post_id, '_gallery_image', 'image1.jpg', false); add_post_meta($post_id, '_gallery_image', 'image2.jpg', false); add_post_meta($post_id, '_gallery_image', 'image3.jpg', false); // Returns: (int) meta_id on success, (bool) false on failure $meta_id = add_post_meta($post_id, '_key', 'value');
update_post_meta()
Updates existing meta or creates if not exists; preferred over add_post_meta() for single values as it handles both insert and update cases automatically.
// Update or create meta field update_post_meta($post_id, '_product_price', 34.99); // Update specific value when multiple exist update_post_meta($post_id, '_gallery_image', 'new.jpg', 'old.jpg'); // Returns: // - (int) meta_id if field didn't exist (created) // - (bool) true if updated successfully // - (bool) false if value unchanged or error
get_post_meta()
Retrieves meta values from a post; third parameter $single controls return format—true returns single value, false returns array of all values for that key; empty string returns all meta.
// Get single value (most common) $price = get_post_meta($post_id, '_product_price', true); // Returns: "29.99" or "" if not exists // Get all values for key (when duplicates exist) $images = get_post_meta($post_id, '_gallery_image', false); // Returns: ['image1.jpg', 'image2.jpg', 'image3.jpg'] // Get ALL meta for post $all_meta = get_post_meta($post_id, '', true); // Returns: ['_price' => ['29.99'], '_sku' => ['ABC123'], ...]
delete_post_meta()
Removes meta field from a post; optional third parameter deletes only matching value when key has multiple entries, otherwise removes all entries for that key.
// Delete all meta with key delete_post_meta($post_id, '_product_price'); // Delete specific value (when multiple values exist) delete_post_meta($post_id, '_gallery_image', 'image2.jpg'); // Delete all meta for all posts with key (use carefully!) delete_post_meta_by_key('_temporary_cache'); // Returns: (bool) true on success, false on failure
Meta key naming conventions
Use descriptive, lowercase keys with underscores; prefix with plugin/theme slug for namespacing; avoid reserved names and keep under 255 characters; consider prefixing with underscore to hide from default Custom Fields UI.
// GOOD naming conventions: '_myplugin_product_price' // Prefixed, descriptive, hidden '_theme_name_hero_image' // Namespaced to theme '_wc_product_inventory' // WooCommerce style // BAD naming conventions: 'price' // Too generic, collision risk 'myPluginPrice' // Avoid camelCase 'my-plugin-price' // Avoid hyphens (SQL issues) 'a' // Non-descriptive // Namespacing pattern: _{prefix}_{feature}_{field}
Hidden meta keys (underscore prefix)
Meta keys beginning with underscore _ are hidden from the default "Custom Fields" metabox in post editor; convention for plugin/theme internal data that shouldn't be user-editable, but still accessible via API.
// Hidden from Custom Fields UI update_post_meta($post_id, '_internal_data', 'value'); // Visible in Custom Fields UI update_post_meta($post_id, 'user_visible_data', 'value'); // To show protected fields, filter: add_filter('is_protected_meta', function($protected, $key, $type) { if ($key === '_my_visible_private_key') { return false; // Show in Custom Fields } return $protected; }, 10, 3);
Metadata sanitization
Always sanitize data before saving and escape when outputting; use appropriate sanitization functions based on data type to prevent security vulnerabilities and ensure data integrity.
add_action('save_post', function($post_id) { // Text fields $title = sanitize_text_field($_POST['title']); $textarea = sanitize_textarea_field($_POST['description']); // URLs $url = esc_url_raw($_POST['website']); // Email $email = sanitize_email($_POST['email']); // Numbers $price = floatval($_POST['price']); $quantity = absint($_POST['quantity']); // HTML (allowed tags) $html = wp_kses_post($_POST['content']); // Arrays $items = array_map('sanitize_text_field', (array) $_POST['items']); // Save sanitized data update_post_meta($post_id, '_price', $price); });
User Meta API
add_user_meta()
Adds meta field to a user record; works identically to add_post_meta() with unique parameter controlling duplicate keys; returns meta ID or false.
// Add unique meta (prevents duplicates) add_user_meta($user_id, 'favorite_color', 'blue', true); // Allow multiple values for same key add_user_meta($user_id, 'achievement', 'first_post', false); add_user_meta($user_id, 'achievement', 'ten_comments', false); add_user_meta($user_id, 'achievement', 'profile_complete', false);
update_user_meta()
Updates or creates user meta field; preferred for single values as it handles upsert logic; note that core user fields (email, display_name) use wp_update_user() instead.
// Update/create user preference update_user_meta($user_id, 'theme_preference', 'dark'); // Update specific value in multi-value field update_user_meta($user_id, 'skill', 'PHP', 'php'); // Core fields use different function: wp_update_user([ 'ID' => $user_id, 'display_name' => 'John Doe', 'user_email' => 'john@example.com' ]);
get_user_meta()
Retrieves user meta values; third parameter determines single value (string) or all values (array) return; empty key returns all user meta fields.
// Get single value $color = get_user_meta($user_id, 'favorite_color', true); // Returns: 'blue' // Get all values for key $achievements = get_user_meta($user_id, 'achievement', false); // Returns: ['first_post', 'ten_comments', 'profile_complete'] // Get all user meta $all_meta = get_user_meta($user_id); // Returns: ['first_name' => [...], 'last_name' => [...], ...]
delete_user_meta()
Removes meta field from user; optional value parameter for targeted deletion when multiple values exist.
// Delete all instances of key delete_user_meta($user_id, 'deprecated_setting'); // Delete specific value only delete_user_meta($user_id, 'achievement', 'first_post'); // Bulk delete (use carefully) $users = get_users(); foreach ($users as $user) { delete_user_meta($user->ID, 'old_plugin_data'); }
User profile fields
Add custom fields to user profile edit screen using show_user_profile (own profile) and edit_user_profile (other users) hooks; save via personal_options_update and edit_user_profile_update.
// Add fields to profile add_action('show_user_profile', 'add_custom_user_fields'); add_action('edit_user_profile', 'add_custom_user_fields'); function add_custom_user_fields($user) { ?> <h3>Extra Profile Information</h3> <table class="form-table"> <tr> <th><label for="twitter">Twitter Handle</label></th> <td> <input type="text" name="twitter" id="twitter" value="<?php echo esc_attr(get_user_meta($user->ID, 'twitter', true)); ?>" class="regular-text"> </td> </tr> </table> <?php } // Save fields add_action('personal_options_update', 'save_custom_user_fields'); add_action('edit_user_profile_update', 'save_custom_user_fields'); function save_custom_user_fields($user_id) { if (!current_user_can('edit_user', $user_id)) return; update_user_meta($user_id, 'twitter', sanitize_text_field($_POST['twitter'])); }
Custom user fields
Extend user registration and create additional user data storage beyond default WordPress fields; integrate with REST API and use for frontend profile features.
// Add to registration form add_action('register_form', function() { $phone = $_POST['phone'] ?? ''; ?> <p> <label for="phone">Phone Number</label> <input type="tel" name="phone" id="phone" value="<?php echo esc_attr($phone); ?>"> </p> <?php }); // Validate on registration add_filter('registration_errors', function($errors, $login, $email) { if (empty($_POST['phone'])) { $errors->add('phone_error', '<strong>Error</strong>: Phone required.'); } return $errors; }, 10, 3); // Save on registration add_action('user_register', function($user_id) { if (isset($_POST['phone'])) { update_user_meta($user_id, 'phone', sanitize_text_field($_POST['phone'])); } });
Term Meta API
add_term_meta()
Adds meta field to a taxonomy term (categories, tags, custom taxonomies); available since WordPress 4.4; works identically to post meta with unique parameter.
// Add meta to a term $term_id = 42; // Category or taxonomy term ID add_term_meta($term_id, 'featured', true, true); add_term_meta($term_id, 'color', '#ff6600', true); // Multiple values add_term_meta($term_id, 'related_term', 15, false); add_term_meta($term_id, 'related_term', 23, false);
update_term_meta()
Updates or creates term meta field; preferred for single values to avoid duplicates.
// Update term color update_term_meta($term_id, 'color', '#3366cc'); // Update term image update_term_meta($term_id, 'thumbnail_id', $attachment_id); // Update specific value when multiples exist update_term_meta($term_id, 'related_term', 45, 23); // Replace 23 with 45
get_term_meta()
Retrieves term meta values; mirrors post meta API with single/array return control.
// Get single value $color = get_term_meta($term_id, 'color', true); // Returns: '#3366cc' // Get all values for key $related = get_term_meta($term_id, 'related_term', false); // Returns: [15, 45] // Get all term meta $all_meta = get_term_meta($term_id, '', true);
delete_term_meta()
Removes meta field from a term; supports targeted deletion with value parameter.
// Delete all meta with key delete_term_meta($term_id, 'deprecated_field'); // Delete specific value delete_term_meta($term_id, 'related_term', 45);
Term meta in admin
Add custom fields to taxonomy add/edit screens using {taxonomy}_add_form_fields, {taxonomy}_edit_form_fields, and save hooks; enhances category/term management with additional data.
// Add field to "Add New Category" form add_action('category_add_form_fields', function($taxonomy) { ?> <div class="form-field"> <label for="term_color">Color</label> <input type="color" name="term_color" id="term_color" value="#000000"> <p class="description">Category highlight color</p> </div> <?php }); // Add field to "Edit Category" form add_action('category_edit_form_fields', function($term, $taxonomy) { $color = get_term_meta($term->term_id, 'color', true); ?> <tr class="form-field"> <th><label for="term_color">Color</label></th> <td> <input type="color" name="term_color" id="term_color" value="<?php echo esc_attr($color ?: '#000000'); ?>"> </td> </tr> <?php }, 10, 2); // Save on create add_action('created_category', function($term_id) { if (isset($_POST['term_color'])) { update_term_meta($term_id, 'color', sanitize_hex_color($_POST['term_color'])); } }); // Save on edit add_action('edited_category', function($term_id) { if (isset($_POST['term_color'])) { update_term_meta($term_id, 'color', sanitize_hex_color($_POST['term_color'])); } });
Comment Meta API
add_comment_meta()
Adds meta field to a comment; useful for storing additional comment data like ratings, reaction types, or moderation flags.
// Add rating to comment add_comment_meta($comment_id, 'rating', 5, true); // Add multiple flags add_comment_meta($comment_id, 'flag', 'spam_report', false); add_comment_meta($comment_id, 'flag', 'inappropriate', false);
update_comment_meta()
Updates or creates comment meta field; commonly used for rating systems, comment reactions, or administrative metadata.
// Update comment rating update_comment_meta($comment_id, 'rating', 4); // Update helpful votes $votes = (int) get_comment_meta($comment_id, 'helpful_votes', true); update_comment_meta($comment_id, 'helpful_votes', $votes + 1); // Mark as verified purchase update_comment_meta($comment_id, 'verified_purchase', true);
get_comment_meta()
Retrieves comment meta values; essential for displaying ratings, badges, or custom comment features on frontend.
// Get rating for display $rating = get_comment_meta($comment_id, 'rating', true); if ($rating) { echo str_repeat('★', $rating) . str_repeat('☆', 5 - $rating); } // Get all flags $flags = get_comment_meta($comment_id, 'flag', false); // Returns: ['spam_report', 'inappropriate'] // Check verified status $verified = get_comment_meta($comment_id, 'verified_purchase', true); if ($verified) { echo '<span class="verified-badge">Verified Purchase</span>'; }
delete_comment_meta()
Removes meta field from a comment; useful for cleanup or removing moderation flags after resolution.
// Remove rating delete_comment_meta($comment_id, 'rating'); // Remove specific flag delete_comment_meta($comment_id, 'flag', 'spam_report'); // Clear all flags delete_comment_meta($comment_id, 'flag'); // Cleanup on comment deletion add_action('delete_comment', function($comment_id) { delete_comment_meta($comment_id, 'rating'); delete_comment_meta($comment_id, 'verified_purchase'); delete_comment_meta($comment_id, 'flag'); });
Quick Reference Chart
┌────────────────────────────────────────────────────────────────────┐ │ WORDPRESS META API COMPARISON │ ├──────────────┬─────────────┬──────────────┬────────────┬───────────┤ │ Function │ Post │ User │ Term │ Comment │ ├──────────────┼─────────────┼──────────────┼────────────┼───────────┤ │ Add │add_post_meta│add_user_meta │add_term_meta│add_comment_meta│ │ Update │update_post_meta│update_user_meta│update_term_meta│update_comment_meta│ │ Get │get_post_meta│get_user_meta │get_term_meta│get_comment_meta│ │ Delete │delete_post_meta│delete_user_meta│delete_term_meta│delete_comment_meta│ ├──────────────┼─────────────┼──────────────┼────────────┼───────────┤ │ DB Table │ postmeta │ usermeta │ termmeta │commentmeta│ └──────────────┴─────────────┴──────────────┴────────────┴───────────┘ FUNCTION SIGNATURES: add_{type}_meta( $id, $key, $value, $unique = false ) update_{type}_meta( $id, $key, $value, $prev_value = '' ) get_{type}_meta( $id, $key = '', $single = false ) delete_{type}_meta( $id, $key, $value = '' )