All skills
jeffallan avatar

/wordpress-pro

@efebc44
by jeffallanjeffallan/claude-skills12k stars
1,124

Develops custom WordPress themes and plugins, creates and registers Gutenberg blocks and block patterns, configures WooCommerce stores, implements WordPress REST API endpoints, applies security hardening (nonces, sanitization, escaping, capability checks), and optimizes performance through caching and query tuning. Use when building WordPress themes, writing plugins, customizing Gutenberg blocks, extending WooCommerce, working with ACF, using the WordPress REST API, applying hooks and filters, or improving WordPress performance and security.

Use this Skill: https://skilld.dev/gh/jeffallan/claude-skills/wordpress-pro

This session only. Nothing lands on disk.

referencesplugin-architecture.md

≈8.2k tokens on demand. Your agent reads this file only when SKILL.md points to it.

Plugin Architecture


Plugin Structure

Minimal Plugin Structure

plugin-name/
├── plugin-name.php        # Main plugin file with header
├── uninstall.php          # Cleanup on uninstall
├── includes/
│   ├── class-plugin-name.php
│   ├── class-activator.php
│   ├── class-deactivator.php
│   └── class-loader.php
├── admin/
│   ├── class-admin.php
│   ├── css/
│   └── js/
├── public/
│   ├── class-public.php
│   ├── css/
│   └── js/
├── languages/
│   └── plugin-name.pot
└── README.txt

Full Plugin Structure (Enterprise)

plugin-name/
├── plugin-name.php
├── uninstall.php
├── composer.json
├── phpcs.xml.dist
├── phpunit.xml.dist
├── includes/
│   ├── class-plugin.php           # Main plugin class
│   ├── class-activator.php        # Activation logic
│   ├── class-deactivator.php      # Deactivation logic
│   ├── class-loader.php           # Hook loader
│   ├── class-i18n.php             # Internationalization
│   ├── Traits/
│   │   └── Singleton.php
│   ├── Interfaces/
│   │   ├── Registrable.php
│   │   └── Hookable.php
│   ├── Services/
│   │   └── class-api-service.php
│   └── Repositories/
│       └── class-data-repository.php
├── admin/
│   ├── class-admin.php
│   ├── class-settings.php
│   ├── partials/
│   │   └── settings-page.php
│   ├── css/
│   └── js/
├── public/
│   ├── class-frontend.php
│   ├── partials/
│   ├── css/
│   └── js/
├── blocks/
│   └── custom-block/
├── templates/
│   └── single-custom-type.php
├── languages/
├── tests/
│   ├── bootstrap.php
│   └── unit/
└── vendor/

Main Plugin File

Plugin Header

<?php
/**
 * Plugin Name:       Plugin Name
 * Plugin URI:        https://example.com/plugin-name
 * Description:       A brief description of what this plugin does.
 * Version:           1.0.0
 * Requires at least: 6.4
 * Requires PHP:      8.1
 * Author:            Author Name
 * Author URI:        https://example.com
 * License:           GPL v2 or later
 * License URI:       https://www.gnu.org/licenses/gpl-2.0.html
 * Text Domain:       plugin-name
 * Domain Path:       /languages
 * Update URI:        https://example.com/plugin-name
 *
 * @package PluginName
 */

declare(strict_types=1);

namespace PluginName;

// Prevent direct access
defined('ABSPATH') || exit;

// Plugin constants
define('PLUGIN_NAME_VERSION', '1.0.0');
define('PLUGIN_NAME_FILE', __FILE__);
define('PLUGIN_NAME_PATH', plugin_dir_path(__FILE__));
define('PLUGIN_NAME_URL', plugin_dir_url(__FILE__));
define('PLUGIN_NAME_BASENAME', plugin_basename(__FILE__));

// Autoloader
if (file_exists(PLUGIN_NAME_PATH . 'vendor/autoload.php')) {
    require_once PLUGIN_NAME_PATH . 'vendor/autoload.php';
}

// Manual includes if no autoloader
require_once PLUGIN_NAME_PATH . 'includes/class-plugin.php';
require_once PLUGIN_NAME_PATH . 'includes/class-activator.php';
require_once PLUGIN_NAME_PATH . 'includes/class-deactivator.php';

/**
 * Plugin activation hook
 */
function activate(): void {
    Activator::activate();
}
register_activation_hook(__FILE__, __NAMESPACE__ . '\\activate');

/**
 * Plugin deactivation hook
 */
function deactivate(): void {
    Deactivator::deactivate();
}
register_deactivation_hook(__FILE__, __NAMESPACE__ . '\\deactivate');

/**
 * Initialize the plugin
 */
function init(): void {
    $plugin = new Plugin();
    $plugin->run();
}
add_action('plugins_loaded', __NAMESPACE__ . '\\init');

Activation & Deactivation

Activator Class

<?php
declare(strict_types=1);

namespace PluginName;

/**
 * Fired during plugin activation
 */
class Activator {

    /**
     * Activation tasks
     */
    public static function activate(): void {
        // Check requirements
        self::check_requirements();

        // Create database tables
        self::create_tables();

        // Set default options
        self::set_default_options();

        // Schedule cron events
        self::schedule_events();

        // Add capabilities
        self::add_capabilities();

        // Flush rewrite rules (if registering CPT/taxonomy)
        flush_rewrite_rules();

        // Set activation flag for welcome notice
        set_transient('plugin_name_activated', true, 30);
    }

    /**
     * Check system requirements
     */
    private static function check_requirements(): void {
        if (version_compare(PHP_VERSION, '8.1', '<')) {
            deactivate_plugins(PLUGIN_NAME_BASENAME);
            wp_die(
                esc_html__('This plugin requires PHP 8.1 or higher.', 'plugin-name'),
                'Plugin Activation Error',
                ['back_link' => true]
            );
        }

        global $wp_version;
        if (version_compare($wp_version, '6.4', '<')) {
            deactivate_plugins(PLUGIN_NAME_BASENAME);
            wp_die(
                esc_html__('This plugin requires WordPress 6.4 or higher.', 'plugin-name'),
                'Plugin Activation Error',
                ['back_link' => true]
            );
        }
    }

    /**
     * Create custom database tables
     */
    private static function create_tables(): void {
        global $wpdb;

        $charset_collate = $wpdb->get_charset_collate();
        $table_name = $wpdb->prefix . 'plugin_name_data';

        $sql = "CREATE TABLE IF NOT EXISTS {$table_name} (
            id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
            user_id bigint(20) unsigned NOT NULL DEFAULT 0,
            data_key varchar(191) NOT NULL,
            data_value longtext,
            created_at datetime DEFAULT CURRENT_TIMESTAMP,
            updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
            PRIMARY KEY (id),
            KEY user_id (user_id),
            KEY data_key (data_key)
        ) {$charset_collate};";

        require_once ABSPATH . 'wp-admin/includes/upgrade.php';
        dbDelta($sql);

        // Store DB version
        update_option('plugin_name_db_version', PLUGIN_NAME_VERSION);
    }

    /**
     * Set default options
     */
    private static function set_default_options(): void {
        $defaults = [
            'enabled'          => true,
            'api_key'          => '',
            'cache_duration'   => 3600,
            'items_per_page'   => 10,
            'allowed_roles'    => ['administrator', 'editor'],
        ];

        if (get_option('plugin_name_settings') === false) {
            add_option('plugin_name_settings', $defaults);
        }
    }

    /**
     * Schedule cron events
     */
    private static function schedule_events(): void {
        if (!wp_next_scheduled('plugin_name_daily_cleanup')) {
            wp_schedule_event(time(), 'daily', 'plugin_name_daily_cleanup');
        }
    }

    /**
     * Add custom capabilities
     */
    private static function add_capabilities(): void {
        $admin = get_role('administrator');
        if ($admin) {
            $admin->add_cap('manage_plugin_name');
            $admin->add_cap('edit_plugin_name_data');
        }
    }
}

Deactivator Class

<?php
declare(strict_types=1);

namespace PluginName;

/**
 * Fired during plugin deactivation
 */
class Deactivator {

    /**
     * Deactivation tasks
     */
    public static function deactivate(): void {
        // Clear scheduled events
        self::clear_scheduled_events();

        // Clear transients
        self::clear_transients();

        // Flush rewrite rules
        flush_rewrite_rules();

        // Note: Do NOT delete options or tables here
        // That should only happen in uninstall.php
    }

    /**
     * Clear all scheduled cron events
     */
    private static function clear_scheduled_events(): void {
        $timestamp = wp_next_scheduled('plugin_name_daily_cleanup');
        if ($timestamp) {
            wp_unschedule_event($timestamp, 'plugin_name_daily_cleanup');
        }

        // Clear all instances of our events
        wp_clear_scheduled_hook('plugin_name_daily_cleanup');
    }

    /**
     * Clear transients
     */
    private static function clear_transients(): void {
        global $wpdb;

        // Clear specific transients
        delete_transient('plugin_name_cache');
        delete_transient('plugin_name_activated');

        // Clear all plugin transients (use with caution)
        $wpdb->query(
            $wpdb->prepare(
                "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s OR option_name LIKE %s",
                '_transient_plugin_name_%',
                '_transient_timeout_plugin_name_%'
            )
        );
    }
}

uninstall.php

<?php
/**
 * Uninstall script - runs when plugin is deleted
 *
 * @package PluginName
 */

// If uninstall not called from WordPress, exit
if (!defined('WP_UNINSTALL_PLUGIN')) {
    exit;
}

// Check if we should preserve data
$settings = get_option('plugin_name_settings', []);
$preserve_data = $settings['preserve_data_on_uninstall'] ?? false;

if (!$preserve_data) {
    global $wpdb;

    // Delete options
    delete_option('plugin_name_settings');
    delete_option('plugin_name_db_version');

    // Delete user meta
    $wpdb->query("DELETE FROM {$wpdb->usermeta} WHERE meta_key LIKE 'plugin_name_%'");

    // Delete post meta
    $wpdb->query("DELETE FROM {$wpdb->postmeta} WHERE meta_key LIKE '_plugin_name_%'");

    // Delete custom tables
    $wpdb->query("DROP TABLE IF EXISTS {$wpdb->prefix}plugin_name_data");

    // Delete transients
    $wpdb->query(
        $wpdb->prepare(
            "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s OR option_name LIKE %s",
            '_transient_plugin_name_%',
            '_transient_timeout_plugin_name_%'
        )
    );

    // Remove capabilities
    $roles = ['administrator', 'editor'];
    foreach ($roles as $role_name) {
        $role = get_role($role_name);
        if ($role) {
            $role->remove_cap('manage_plugin_name');
            $role->remove_cap('edit_plugin_name_data');
        }
    }

    // Clear any cached data
    wp_cache_flush();
}

Settings API

Settings Class

<?php
declare(strict_types=1);

namespace PluginName\Admin;

/**
 * Plugin settings management
 */
class Settings {

    private const OPTION_GROUP = 'plugin_name_settings';
    private const OPTION_NAME = 'plugin_name_settings';
    private const PAGE_SLUG = 'plugin-name-settings';

    /**
     * Initialize settings
     */
    public function init(): void {
        add_action('admin_menu', [$this, 'add_menu_page']);
        add_action('admin_init', [$this, 'register_settings']);
    }

    /**
     * Add admin menu page
     */
    public function add_menu_page(): void {
        add_options_page(
            __('Plugin Name Settings', 'plugin-name'),
            __('Plugin Name', 'plugin-name'),
            'manage_options',
            self::PAGE_SLUG,
            [$this, 'render_settings_page']
        );
    }

    /**
     * Register all settings
     */
    public function register_settings(): void {
        register_setting(
            self::OPTION_GROUP,
            self::OPTION_NAME,
            [
                'type'              => 'array',
                'sanitize_callback' => [$this, 'sanitize_settings'],
                'default'           => $this->get_defaults(),
            ]
        );

        // General section
        add_settings_section(
            'plugin_name_general',
            __('General Settings', 'plugin-name'),
            [$this, 'render_general_section'],
            self::PAGE_SLUG
        );

        // API section
        add_settings_section(
            'plugin_name_api',
            __('API Settings', 'plugin-name'),
            [$this, 'render_api_section'],
            self::PAGE_SLUG
        );

        // Fields
        $this->add_fields();
    }

    /**
     * Add settings fields
     */
    private function add_fields(): void {
        // Enable field
        add_settings_field(
            'enabled',
            __('Enable Plugin', 'plugin-name'),
            [$this, 'render_checkbox_field'],
            self::PAGE_SLUG,
            'plugin_name_general',
            [
                'label_for'   => 'enabled',
                'description' => __('Enable or disable the plugin functionality.', 'plugin-name'),
            ]
        );

        // Items per page
        add_settings_field(
            'items_per_page',
            __('Items Per Page', 'plugin-name'),
            [$this, 'render_number_field'],
            self::PAGE_SLUG,
            'plugin_name_general',
            [
                'label_for'   => 'items_per_page',
                'min'         => 1,
                'max'         => 100,
                'description' => __('Number of items to display per page.', 'plugin-name'),
            ]
        );

        // API Key
        add_settings_field(
            'api_key',
            __('API Key', 'plugin-name'),
            [$this, 'render_text_field'],
            self::PAGE_SLUG,
            'plugin_name_api',
            [
                'label_for'   => 'api_key',
                'type'        => 'password',
                'description' => __('Enter your API key for external service.', 'plugin-name'),
            ]
        );

        // Cache duration
        add_settings_field(
            'cache_duration',
            __('Cache Duration', 'plugin-name'),
            [$this, 'render_select_field'],
            self::PAGE_SLUG,
            'plugin_name_api',
            [
                'label_for'   => 'cache_duration',
                'options'     => [
                    '900'   => __('15 minutes', 'plugin-name'),
                    '1800'  => __('30 minutes', 'plugin-name'),
                    '3600'  => __('1 hour', 'plugin-name'),
                    '86400' => __('1 day', 'plugin-name'),
                ],
                'description' => __('How long to cache API responses.', 'plugin-name'),
            ]
        );
    }

    /**
     * Get default settings
     */
    private function get_defaults(): array {
        return [
            'enabled'        => true,
            'api_key'        => '',
            'cache_duration' => 3600,
            'items_per_page' => 10,
        ];
    }

    /**
     * Sanitize settings
     */
    public function sanitize_settings(array $input): array {
        $sanitized = [];

        $sanitized['enabled'] = !empty($input['enabled']);

        $sanitized['api_key'] = sanitize_text_field($input['api_key'] ?? '');

        $sanitized['cache_duration'] = absint($input['cache_duration'] ?? 3600);
        if (!in_array($sanitized['cache_duration'], [900, 1800, 3600, 86400], true)) {
            $sanitized['cache_duration'] = 3600;
        }

        $sanitized['items_per_page'] = absint($input['items_per_page'] ?? 10);
        $sanitized['items_per_page'] = max(1, min(100, $sanitized['items_per_page']));

        return $sanitized;
    }

    /**
     * Render settings page
     */
    public function render_settings_page(): void {
        if (!current_user_can('manage_options')) {
            return;
        }

        // Show success message
        if (isset($_GET['settings-updated'])) {
            add_settings_error(
                self::OPTION_GROUP,
                'settings_updated',
                __('Settings saved.', 'plugin-name'),
                'updated'
            );
        }
        ?>
        <div class="wrap">
            <h1><?php echo esc_html(get_admin_page_title()); ?></h1>

            <?php settings_errors(self::OPTION_GROUP); ?>

            <form action="options.php" method="post">
                <?php
                settings_fields(self::OPTION_GROUP);
                do_settings_sections(self::PAGE_SLUG);
                submit_button(__('Save Settings', 'plugin-name'));
                ?>
            </form>
        </div>
        <?php
    }

    /**
     * Render general section description
     */
    public function render_general_section(): void {
        echo '<p>' . esc_html__('Configure general plugin settings.', 'plugin-name') . '</p>';
    }

    /**
     * Render API section description
     */
    public function render_api_section(): void {
        echo '<p>' . esc_html__('Configure API connection settings.', 'plugin-name') . '</p>';
    }

    /**
     * Render checkbox field
     */
    public function render_checkbox_field(array $args): void {
        $options = get_option(self::OPTION_NAME, $this->get_defaults());
        $value = $options[$args['label_for']] ?? false;
        ?>
        <input type="checkbox"
               id="<?php echo esc_attr($args['label_for']); ?>"
               name="<?php echo esc_attr(self::OPTION_NAME . '[' . $args['label_for'] . ']'); ?>"
               value="1"
               <?php checked($value, true); ?>
        />
        <?php if (!empty($args['description'])): ?>
            <p class="description"><?php echo esc_html($args['description']); ?></p>
        <?php endif;
    }

    /**
     * Render text field
     */
    public function render_text_field(array $args): void {
        $options = get_option(self::OPTION_NAME, $this->get_defaults());
        $value = $options[$args['label_for']] ?? '';
        $type = $args['type'] ?? 'text';
        ?>
        <input type="<?php echo esc_attr($type); ?>"
               id="<?php echo esc_attr($args['label_for']); ?>"
               name="<?php echo esc_attr(self::OPTION_NAME . '[' . $args['label_for'] . ']'); ?>"
               value="<?php echo esc_attr($value); ?>"
               class="regular-text"
        />
        <?php if (!empty($args['description'])): ?>
            <p class="description"><?php echo esc_html($args['description']); ?></p>
        <?php endif;
    }

    /**
     * Render number field
     */
    public function render_number_field(array $args): void {
        $options = get_option(self::OPTION_NAME, $this->get_defaults());
        $value = $options[$args['label_for']] ?? 0;
        ?>
        <input type="number"
               id="<?php echo esc_attr($args['label_for']); ?>"
               name="<?php echo esc_attr(self::OPTION_NAME . '[' . $args['label_for'] . ']'); ?>"
               value="<?php echo esc_attr($value); ?>"
               min="<?php echo esc_attr($args['min'] ?? 0); ?>"
               max="<?php echo esc_attr($args['max'] ?? 100); ?>"
               class="small-text"
        />
        <?php if (!empty($args['description'])): ?>
            <p class="description"><?php echo esc_html($args['description']); ?></p>
        <?php endif;
    }

    /**
     * Render select field
     */
    public function render_select_field(array $args): void {
        $options = get_option(self::OPTION_NAME, $this->get_defaults());
        $value = $options[$args['label_for']] ?? '';
        ?>
        <select id="<?php echo esc_attr($args['label_for']); ?>"
                name="<?php echo esc_attr(self::OPTION_NAME . '[' . $args['label_for'] . ']'); ?>">
            <?php foreach ($args['options'] as $key => $label): ?>
                <option value="<?php echo esc_attr($key); ?>" <?php selected($value, $key); ?>>
                    <?php echo esc_html($label); ?>
                </option>
            <?php endforeach; ?>
        </select>
        <?php if (!empty($args['description'])): ?>
            <p class="description"><?php echo esc_html($args['description']); ?></p>
        <?php endif;
    }
}

Custom Post Types & Taxonomies

Registering Custom Post Types

<?php
declare(strict_types=1);

namespace PluginName;

/**
 * Register custom post types and taxonomies
 */
class CustomPostTypes {

    /**
     * Initialize
     */
    public function init(): void {
        add_action('init', [$this, 'register_post_types']);
        add_action('init', [$this, 'register_taxonomies']);
    }

    /**
     * Register custom post type
     */
    public function register_post_types(): void {
        $labels = [
            'name'                  => _x('Products', 'Post Type General Name', 'plugin-name'),
            'singular_name'         => _x('Product', 'Post Type Singular Name', 'plugin-name'),
            'menu_name'             => __('Products', 'plugin-name'),
            'name_admin_bar'        => __('Product', 'plugin-name'),
            'archives'              => __('Product Archives', 'plugin-name'),
            'attributes'            => __('Product Attributes', 'plugin-name'),
            'parent_item_colon'     => __('Parent Product:', 'plugin-name'),
            'all_items'             => __('All Products', 'plugin-name'),
            'add_new_item'          => __('Add New Product', 'plugin-name'),
            'add_new'               => __('Add New', 'plugin-name'),
            'new_item'              => __('New Product', 'plugin-name'),
            'edit_item'             => __('Edit Product', 'plugin-name'),
            'update_item'           => __('Update Product', 'plugin-name'),
            'view_item'             => __('View Product', 'plugin-name'),
            'view_items'            => __('View Products', 'plugin-name'),
            'search_items'          => __('Search Product', 'plugin-name'),
            'not_found'             => __('Not found', 'plugin-name'),
            'not_found_in_trash'    => __('Not found in Trash', 'plugin-name'),
            'featured_image'        => __('Featured Image', 'plugin-name'),
            'set_featured_image'    => __('Set featured image', 'plugin-name'),
            'remove_featured_image' => __('Remove featured image', 'plugin-name'),
            'use_featured_image'    => __('Use as featured image', 'plugin-name'),
            'insert_into_item'      => __('Insert into product', 'plugin-name'),
            'uploaded_to_this_item' => __('Uploaded to this product', 'plugin-name'),
            'items_list'            => __('Products list', 'plugin-name'),
            'items_list_navigation' => __('Products list navigation', 'plugin-name'),
            'filter_items_list'     => __('Filter products list', 'plugin-name'),
        ];

        $args = [
            'label'               => __('Product', 'plugin-name'),
            'description'         => __('Product custom post type', 'plugin-name'),
            'labels'              => $labels,
            'supports'            => ['title', 'editor', 'thumbnail', 'excerpt', 'custom-fields', 'revisions'],
            'taxonomies'          => ['product_category', 'product_tag'],
            'hierarchical'        => false,
            'public'              => true,
            'show_ui'             => true,
            'show_in_menu'        => true,
            'menu_position'       => 20,
            'menu_icon'           => 'dashicons-products',
            'show_in_admin_bar'   => true,
            'show_in_nav_menus'   => true,
            'can_export'          => true,
            'has_archive'         => 'products',
            'exclude_from_search' => false,
            'publicly_queryable'  => true,
            'capability_type'     => 'post',
            'show_in_rest'        => true,  // Enable Gutenberg
            'rest_base'           => 'products',
            'rewrite'             => [
                'slug'       => 'product',
                'with_front' => false,
            ],
            'template'            => [
                ['core/image', ['align' => 'wide']],
                ['core/paragraph', ['placeholder' => 'Product description...']],
            ],
            'template_lock'       => false,  // 'all', 'insert', false
        ];

        register_post_type('product', $args);
    }

    /**
     * Register custom taxonomies
     */
    public function register_taxonomies(): void {
        // Product Category (hierarchical like categories)
        $category_labels = [
            'name'              => _x('Product Categories', 'taxonomy general name', 'plugin-name'),
            'singular_name'     => _x('Product Category', 'taxonomy singular name', 'plugin-name'),
            'search_items'      => __('Search Product Categories', 'plugin-name'),
            'all_items'         => __('All Product Categories', 'plugin-name'),
            'parent_item'       => __('Parent Product Category', 'plugin-name'),
            'parent_item_colon' => __('Parent Product Category:', 'plugin-name'),
            'edit_item'         => __('Edit Product Category', 'plugin-name'),
            'update_item'       => __('Update Product Category', 'plugin-name'),
            'add_new_item'      => __('Add New Product Category', 'plugin-name'),
            'new_item_name'     => __('New Product Category Name', 'plugin-name'),
            'menu_name'         => __('Categories', 'plugin-name'),
        ];

        register_taxonomy('product_category', ['product'], [
            'hierarchical'      => true,
            'labels'            => $category_labels,
            'show_ui'           => true,
            'show_admin_column' => true,
            'query_var'         => true,
            'show_in_rest'      => true,
            'rewrite'           => ['slug' => 'product-category'],
        ]);

        // Product Tags (non-hierarchical like tags)
        $tag_labels = [
            'name'                       => _x('Product Tags', 'taxonomy general name', 'plugin-name'),
            'singular_name'              => _x('Product Tag', 'taxonomy singular name', 'plugin-name'),
            'search_items'               => __('Search Product Tags', 'plugin-name'),
            'popular_items'              => __('Popular Product Tags', 'plugin-name'),
            'all_items'                  => __('All Product Tags', 'plugin-name'),
            'edit_item'                  => __('Edit Product Tag', 'plugin-name'),
            'update_item'                => __('Update Product Tag', 'plugin-name'),
            'add_new_item'               => __('Add New Product Tag', 'plugin-name'),
            'new_item_name'              => __('New Product Tag Name', 'plugin-name'),
            'separate_items_with_commas' => __('Separate tags with commas', 'plugin-name'),
            'add_or_remove_items'        => __('Add or remove tags', 'plugin-name'),
            'choose_from_most_used'      => __('Choose from the most used tags', 'plugin-name'),
            'not_found'                  => __('No tags found.', 'plugin-name'),
            'menu_name'                  => __('Tags', 'plugin-name'),
        ];

        register_taxonomy('product_tag', ['product'], [
            'hierarchical'      => false,
            'labels'            => $tag_labels,
            'show_ui'           => true,
            'show_admin_column' => true,
            'query_var'         => true,
            'show_in_rest'      => true,
            'rewrite'           => ['slug' => 'product-tag'],
        ]);
    }
}

Plugin Updates

Self-Hosted Update Checker

<?php
declare(strict_types=1);

namespace PluginName;

/**
 * Handle plugin updates from custom server
 */
class UpdateChecker {

    private string $plugin_slug;
    private string $update_url;
    private string $plugin_file;

    public function __construct() {
        $this->plugin_slug = 'plugin-name';
        $this->plugin_file = PLUGIN_NAME_BASENAME;
        $this->update_url = 'https://example.com/api/plugin-updates/';
    }

    /**
     * Initialize update checker
     */
    public function init(): void {
        add_filter('pre_set_site_transient_update_plugins', [$this, 'check_for_update']);
        add_filter('plugins_api', [$this, 'plugin_info'], 20, 3);
        add_action('in_plugin_update_message-' . $this->plugin_file, [$this, 'update_message'], 10, 2);
    }

    /**
     * Check for plugin updates
     */
    public function check_for_update(object $transient): object {
        if (empty($transient->checked)) {
            return $transient;
        }

        $remote = $this->get_remote_info();

        if (
            $remote &&
            version_compare(PLUGIN_NAME_VERSION, $remote->version, '<') &&
            version_compare($remote->requires, get_bloginfo('version'), '<=') &&
            version_compare($remote->requires_php, PHP_VERSION, '<=')
        ) {
            $transient->response[$this->plugin_file] = (object) [
                'slug'        => $this->plugin_slug,
                'plugin'      => $this->plugin_file,
                'new_version' => $remote->version,
                'url'         => $remote->url,
                'package'     => $remote->package,
                'icons'       => (array) ($remote->icons ?? []),
                'banners'     => (array) ($remote->banners ?? []),
                'tested'      => $remote->tested ?? '',
                'requires'    => $remote->requires ?? '',
            ];
        }

        return $transient;
    }

    /**
     * Plugin information for update screen
     */
    public function plugin_info(mixed $result, string $action, object $args): mixed {
        if ($action !== 'plugin_information' || $args->slug !== $this->plugin_slug) {
            return $result;
        }

        $remote = $this->get_remote_info();

        if (!$remote) {
            return $result;
        }

        return (object) [
            'name'            => $remote->name,
            'slug'            => $this->plugin_slug,
            'version'         => $remote->version,
            'author'          => $remote->author,
            'author_profile'  => $remote->author_profile ?? '',
            'requires'        => $remote->requires,
            'tested'          => $remote->tested,
            'requires_php'    => $remote->requires_php,
            'sections'        => (array) $remote->sections,
            'download_link'   => $remote->package,
            'banners'         => (array) ($remote->banners ?? []),
            'icons'           => (array) ($remote->icons ?? []),
            'last_updated'    => $remote->last_updated ?? '',
            'homepage'        => $remote->url ?? '',
        ];
    }

    /**
     * Custom update message
     */
    public function update_message(array $plugin_data, object $response): void {
        if (!empty($response->upgrade_notice)) {
            printf(
                '<br /><strong>%s</strong>: %s',
                esc_html__('Upgrade Notice', 'plugin-name'),
                esc_html($response->upgrade_notice)
            );
        }
    }

    /**
     * Get remote plugin information
     */
    private function get_remote_info(): ?object {
        $transient_key = 'plugin_name_update_info';
        $remote = get_transient($transient_key);

        if ($remote !== false) {
            return $remote === 'error' ? null : $remote;
        }

        $response = wp_remote_get($this->update_url . 'info.json', [
            'timeout' => 10,
            'headers' => [
                'Accept' => 'application/json',
            ],
        ]);

        if (
            is_wp_error($response) ||
            wp_remote_retrieve_response_code($response) !== 200 ||
            empty(wp_remote_retrieve_body($response))
        ) {
            set_transient($transient_key, 'error', HOUR_IN_SECONDS);
            return null;
        }

        $remote = json_decode(wp_remote_retrieve_body($response));
        set_transient($transient_key, $remote, 12 * HOUR_IN_SECONDS);

        return $remote;
    }
}

Best Practices

Do

  • Use namespaces to avoid function/class name collisions
  • Follow WordPress Coding Standards (WPCS)
  • Include proper plugin headers with all required fields
  • Implement proper activation/deactivation/uninstall hooks
  • Use the Settings API for options pages
  • Register CPTs with show_in_rest for Gutenberg support
  • Cache remote API responses with transients
  • Provide translation support with text domains
  • Include a proper uninstall.php for cleanup

Do Not

  • Access the database directly without proper preparation
  • Store options without sanitization
  • Skip capability checks in admin functions
  • Leave orphaned data after uninstall
  • Hardcode plugin paths (use constants)
  • Register hooks in constructors (use init methods)
  • Modify core WordPress tables
  • Include heavy operations in activation hooks

Source: SKILL.md on GitHub

2 alerts16d5 checks · Risk CRITICAL
  • Gen Agent Trust Hub16d

    This skill is flagged as malicious due to confirmed detections by security scanners. The primary configuration file (SKILL.md) is identified as malware, and the associated documentation link is currently blacklisted as a malicious URL.

  • Socket16d

    No alerts

  • Snyk16d

    Risk: LOW · No issues

  • Runlayer6mo

    3/6 files flagged

  • ZeroLeaks5mo

    Score: 93/100 · 2 sections analyzed

Signed by skilld at efebc44. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 2 months ago.

Steadyupdated 5 months ago
Other metadata
metadata
{
  "author": "https://github.com/Jeffallan",
  "version": "1.1.0",
  "domain": "platform",
  "triggers": "WordPress, WooCommerce, Gutenberg, WordPress theme, WordPress plugin, custom blocks, ACF, WordPress REST API, hooks, filters, WordPress performance, WordPress security",
  "role": "expert",
  "scope": "implementation",
  "output-format": "code",
  "related-skills": "php-pro, laravel-specialist, fullstack-guardian, security-reviewer"
}
  • Security
  • Performance
  • wordpress
  • php
  • gutenberg
  • woocommerce
  • theme-development
  • plugin-development
  • rest-api
  • acf

README badge

README badge for jeffallan/claude-skills/wordpress-pro

Develops custom WordPress themes, plugins, Gutenberg blocks, and WooCommerce stores with security hardening (nonces, sanitization, escaping, capability checks) and performance optimization. Use when building WordPress features, extending with the REST API, applying hooks and filters, or hardening against common vulnerabilities.

Generated from the current SKILL.md.

Does this skill cover WooCommerce customization?
Yes. The skill includes configuring WooCommerce stores, extending product/order functionality, and applying hooks and filters specific to WooCommerce workflows.
What PHP and WordPress versions does this target?
WordPress 6.4+ and PHP 8.1+. The skill enforces modern standards and will not generate code using deprecated functions.
Does this skill handle Gutenberg block development?
Yes. It covers creating and registering custom Gutenberg blocks, block patterns, and full-site editing (FSE) workflows.
Will this skill validate code against WordPress standards?
The skill references phpcs with the WordPress Coding Standard (WPCS) for validation and expects you to run checks manually, but does not automatically lint generated code.
Does this cover ACF integration?
Yes. Working with Advanced Custom Fields (ACF) is listed as a supported use case, including field registration and retrieval patterns.

Generated from the current SKILL.md. These answers refresh after source changes.