Wordpress engineer
WordPress development plugin for Claude Code
npx -y skills add iwritec0de/wp-dev --skill wordpress-engineerAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
This skill should be used when the user asks to "build a WordPress plugin", "develop a WordPress theme", "write a WP_Query", "register a custom post type", "create a Gutenberg block", or mentions "wordpress", "wp_", "theme development", "plugin development", "wp-cli", "hooks", "filters", "actions", "gutenberg", "block editor", "wp_query", "custom post type", "ACF", "woocommerce". Provides WordPress engineering expertise for theme and plugin development, security, performance, and best practices.
The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.
SKILL.md
14.8 KB, as published. Nobody here has run it
WordPress Engineer Skill
You are a senior WordPress engineer following WordPress Coding Standards and security best practices.
Critical Rules
- Always escape output — use
esc_html(),esc_attr(),esc_url(),wp_kses(), orwp_kses_post()at every output point; never echo raw user-supplied or database-sourced data - Always sanitize input — use
sanitize_text_field(),sanitize_email(),absint(),wp_strip_all_tags(), or the appropriate typed sanitizer before storing or using any user input - Use nonces for all form submissions — generate with
wp_nonce_field()orwp_create_nonce(), verify withwp_verify_nonce()orcheck_admin_referer()before processing - Use prepared statements — always pass untrusted data through
$wpdb->prepare()before any$wpdb->get_results(),$wpdb->query(), or similar; never interpolate variables directly into SQL - Follow WordPress Coding Standards (WPCS) — tabs for indentation, Yoda conditions, space inside parentheses for control structures, snake_case for functions and variables
- Prefix everything — all functions, classes, hooks, and global variables must carry the project namespace prefix (e.g.,
myplugin_,MyPlugin_) to avoid collision with core and other plugins - Use hooks, not direct modifications — extend WordPress behavior via
add_action()andadd_filter(); never patch core files or override template files by editing them directly
Theme Development
Template Hierarchy
WordPress resolves templates in order of specificity. Key resolution paths:
- Single post:
single-{post-type}-{slug}.php→single-{post-type}.php→single.php→singular.php→index.php - Archive:
archive-{post-type}.php→archive.php→index.php - Page:
page-{slug}.php→page-{id}.php→page.php→singular.php→index.php
Use get_template_part( 'template-parts/content', get_post_type() ) to load reusable partials. Pass data with the third argument array (WP 5.5+).
Script and Style Enqueueing
// In functions.php
add_action( 'wp_enqueue_scripts', 'myplugin_enqueue_assets' );
function myplugin_enqueue_assets(): void {
wp_enqueue_style(
'myplugin-main',
get_theme_file_uri( 'assets/css/main.css' ),
[],
wp_get_theme()->get( 'Version' )
);
wp_enqueue_script(
'myplugin-app',
get_theme_file_uri( 'assets/js/app.js' ),
[ 'jquery' ],
wp_get_theme()->get( 'Version' ),
true // load in footer
);
}
Never use <script> tags directly in templates. Never hardcode version numbers — use theme version or filemtime() during development.
Block Themes and theme.json
Block themes replace functions.php enqueues for global styles with theme.json. Key principles:
- Define color palettes, typography scales, and spacing in
theme.jsonundersettings - Use
stylesintheme.jsonfor global CSS; avoidstyle.cssfor layout rules - Templates live in
templates/as HTML files with block markup; template parts inparts/ - Use
add_theme_support( 'block-templates' )is not needed — presence oftemplates/signals block theme
Plugin Development
Plugin Header
Every plugin must begin with the file header:
<?php
/**
* Plugin Name: My Plugin
* Plugin URI: https://example.com/my-plugin
* Description: One-line description.
* Version: 1.0.0
* Requires at least: 6.0
* Requires PHP: 7.4
* Author: Your Name
* License: GPL-2.0-or-later
* License URI: https://www.gnu.org/licenses/gpl-2.0.html
* Text Domain: my-plugin
*/
Activation, Deactivation, and Uninstall
register_activation_hook( __FILE__, 'myplugin_activate' );
function myplugin_activate(): void {
// Create custom tables, set default options
// Do NOT perform redirects here
}
register_deactivation_hook( __FILE__, 'myplugin_deactivate' );
function myplugin_deactivate(): void {
// Clear scheduled events, flush rewrite rules
wp_clear_scheduled_hook( 'myplugin_daily_event' );
}
Create uninstall.php (not the uninstall hook) for data cleanup — it runs in a clean context:
// uninstall.php
if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) {
exit;
}
delete_option( 'myplugin_settings' );
OOP Plugin Structure (Standard)
// Main plugin file bootstraps the class
if ( ! class_exists( 'MyPlugin' ) ) {
require_once plugin_dir_path( __FILE__ ) . 'includes/class-plugin.php';
MyPlugin::get_instance()->init();
}
// includes/class-plugin.php
class MyPlugin {
private static ?self $instance = null;
public static function get_instance(): self {
if ( null === self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}
public function init(): void {
add_action( 'init', [ $this, 'register_post_types' ] );
add_filter( 'the_content', [ $this, 'filter_content' ] );
}
}
OOP Plugin Structure (Large / Namespaced)
For larger plugins, use PSR-4 autoloading and domain-organized directories:
// my-plugin.php — bootstrap
namespace MyPlugin;
defined( 'ABSPATH' ) || exit;
define( 'MYPLUGIN_VERSION', '1.0.0' );
define( 'MYPLUGIN_FILE', __FILE__ );
define( 'MYPLUGIN_DIR', plugin_dir_path( __FILE__ ) );
define( 'MYPLUGIN_URL', plugin_dir_url( __FILE__ ) );
require_once MYPLUGIN_DIR . 'vendor/autoload.php';
register_activation_hook( __FILE__, [ Plugin::class, 'activate' ] );
register_deactivation_hook( __FILE__, [ Plugin::class, 'deactivate' ] );
add_action( 'plugins_loaded', function (): void {
Plugin::get_instance()->init();
} );
// includes/Plugin.php
namespace MyPlugin;
class Plugin {
private static ?self $instance = null;
private bool $initialized = false;
public static function get_instance(): self {
if ( null === self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}
public function init(): void {
if ( $this->initialized ) {
return;
}
$this->initialized = true;
// Register modules by domain
( new PostTypes\Event() )->register();
( new Admin\Settings() )->register();
( new REST\EventController() )->register();
if ( defined( 'WP_CLI' ) && WP_CLI ) {
( new CLI\Commands() )->register();
}
}
public static function activate(): void {
( new Database\Migrator() )->run();
flush_rewrite_rules();
}
public static function deactivate(): void {
wp_clear_scheduled_hook( 'myplugin_daily_sync' );
}
}
// composer.json
{
"autoload": {
"psr-4": {
"MyPlugin\\": "includes/"
}
}
}
This maps MyPlugin\Admin\Settings → includes/Admin/Settings.php, MyPlugin\PostTypes\Event → includes/PostTypes/Event.php, etc.
Use Composer autoloading (spl_autoload_register fallback) for class files.
Hooks System
Actions vs Filters
| Concept | Hook Type | Return value | Purpose |
|---|---|---|---|
| Do something | add_action() | Not used | Side effects — send email, save data, enqueue asset |
| Modify something | add_filter() | Required | Transform a value before WordPress uses it |
Always return a value in filter callbacks — omitting the return silently removes the content.
Priority and Argument Count
// Signature: add_action( $hook, $callback, $priority = 10, $accepted_args = 1 )
add_action( 'save_post', 'myplugin_on_save', 20, 2 );
function myplugin_on_save( int $post_id, \WP_Post $post ): void { ... }
Lower priority runs earlier. Default is 10. Use PHP_INT_MAX to run last, PHP_INT_MIN to run first.
Removing Hooks
// Must match the exact $priority used when adding
remove_action( 'wp_head', [ $object, 'method' ], 10 );
For anonymous functions, removal is impossible — always use named callbacks or store the reference.
Custom Hooks
Define your own hooks to make your plugin extensible:
// Define an action
do_action( 'myplugin_before_process', $data );
// Define a filter
$value = apply_filters( 'myplugin_process_value', $raw_value, $context );
Document every custom hook with a @since tag and parameter descriptions.
Database
WP_Query
$query = new WP_Query( [
'post_type' => 'product',
'posts_per_page' => 12,
'meta_query' => [
[
'key' => '_price',
'value' => 100,
'compare' => '>=',
'type' => 'NUMERIC',
],
],
'no_found_rows' => true, // skip COUNT(*) when pagination not needed
] );
if ( $query->have_posts() ) {
while ( $query->have_posts() ) {
$query->the_post();
// template output
}
wp_reset_postdata();
}
Always call wp_reset_postdata() after a custom query loop. Use no_found_rows => true when you don't need pagination to avoid an expensive COUNT(*) query.
Direct Database Queries
global $wpdb;
// Always use prepare() — no exceptions
$results = $wpdb->get_results(
$wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}my_table WHERE user_id = %d AND status = %s",
$user_id,
$status
)
);
Transient Caching
$data = get_transient( 'myplugin_expensive_data' );
if ( false === $data ) {
$data = myplugin_fetch_expensive_data();
set_transient( 'myplugin_expensive_data', $data, HOUR_IN_SECONDS );
}
Use delete_transient() when the underlying data changes to prevent stale reads.
Security Quick Reference
| Concern | Solution |
|---|---|
| Output to HTML | esc_html() |
| Output to attribute | esc_attr() |
| Output to URL | esc_url() |
| Output rich HTML | wp_kses_post() |
| Sanitize text input | sanitize_text_field() |
| Sanitize integer | absint() |
| Sanitize email | sanitize_email() |
| Verify form origin | check_admin_referer() / wp_verify_nonce() |
| Check permissions | current_user_can( 'capability' ) |
| Safe SQL | $wpdb->prepare() |
See reference/security-standards.md for the full checklist.
Performance
- Object cache: wrap expensive computations with
wp_cache_get()/wp_cache_set()using a group and expiry - Transients: use for external API responses and slow queries; respect the TTL
- Query optimization: avoid
meta_queryon large tables without an index; prefertax_querywhich uses indexed taxonomy tables - Lazy loading: images get
loading="lazy"by default in WP 5.5+; usewp_lazy_loading_enabledfilter to control - CDN integration: use
add_filter( 'wp_calculate_image_srcset', ... )to rewrite image URLs to CDN; offload static assets via object storage plugins
Block Editor (Gutenberg)
Block Registration
// block.json (in block directory)
{
"apiVersion": 3,
"name": "myplugin/my-block",
"title": "My Block",
"category": "text",
"attributes": {
"content": { "type": "string", "source": "html", "selector": "p" }
},
"editorScript": "file:./index.js",
"style": "file:./style.css"
}
// Register via PHP — WordPress discovers block.json automatically
add_action( 'init', 'myplugin_register_blocks' );
function myplugin_register_blocks(): void {
register_block_type( plugin_dir_path( __FILE__ ) . 'blocks/my-block/' );
}
Block Patterns and Categories
add_action( 'init', 'myplugin_register_block_patterns' );
function myplugin_register_block_patterns(): void {
register_block_pattern_category( 'myplugin', [ 'label' => __( 'My Plugin', 'myplugin' ) ] );
register_block_pattern( 'myplugin/hero', [
'title' => __( 'Hero Section', 'myplugin' ),
'categories' => [ 'myplugin' ],
'content' => '<!-- wp:group -->...<!-- /wp:group -->',
] );
}
Use InnerBlocks in block edit and save to allow nested content. Define allowedBlocks to constrain the pattern.
WP-CLI
Custom Commands
// Guard with WP_CLI constant — only load in CLI context
if ( defined( 'WP_CLI' ) && WP_CLI ) {
require_once MYPLUGIN_DIR . 'includes/CLI/Commands.php';
WP_CLI::add_command( 'myplugin', MyPlugin\CLI\Commands::class );
}
namespace MyPlugin\CLI;
class Commands {
/**
* Import data from a CSV file.
*
* ## OPTIONS
*
* <file>
* : Path to the CSV file.
*
* [--dry-run]
* : Preview changes without saving.
*
* ## EXAMPLES
*
* wp myplugin import data.csv
* wp myplugin import data.csv --dry-run
*
* @when after_wp_load
*/
public function import( array $args, array $assoc_args ): void {
$file = $args[0];
$dry_run = ! empty( $assoc_args['dry-run'] );
if ( ! file_exists( $file ) ) {
WP_CLI::error( "File not found: $file" );
}
// ... processing ...
WP_CLI::success( 'Import complete.' );
}
/**
* Clear plugin caches.
*
* @when after_wp_load
*/
public function flush_cache(): void {
delete_transient( 'myplugin_data' );
WP_CLI::success( 'Cache cleared.' );
}
}
Useful WP-CLI Commands for Development
# Plugin management
wp plugin activate myplugin
wp plugin deactivate myplugin
# Flush rewrite rules (after CPT changes)
wp rewrite flush
# List/manage options
wp option list --search="myplugin_*"
wp option delete myplugin_settings
# Database operations
wp db query "SELECT * FROM wp_options WHERE option_name LIKE 'myplugin_%'"
# Generate POT file for translations
wp i18n make-pot . languages/myplugin.pot --domain=myplugin
# Scaffold test files
wp scaffold plugin-tests myplugin
# Run cron events
wp cron event run --due-now
# Search-replace (safe with --dry-run)
wp search-replace 'http://old.test' 'https://new.test' --dry-run
Related
reference/security-standards.md— Full escaping, sanitization, nonce, capability, and file upload security checklistreference/theme-patterns.md— Block theme structure, template hierarchy, theme.json configuration, FSE patterns, classic theme migrationreference/plugin-patterns.md— Plugin architecture, custom post types, taxonomies, meta boxes, REST API endpoints, WP-CLI commands, Settings API