wordpress-engineer
Skill1 starsChanged 7 months agoMITView on GitHub
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.
In this repository13 other files it gives its agents.
- gutenberg-layoutSkill
- php-cleanupSkill
- verificationSkill
- woocommerceSkill
- wordpress-databaseSkill
- wordpress-i18nSkill
- wordpress-patternsSkill
- wordpress-performanceSkill
- wordpress-securitySkill
- wordpress-standardsSkill
- wp-debuggingSkill
- wp-hooksSkill
- wp-testingSkill
The file
Show its rules and text
We split this skill into the rules it gives an agent
15 rules. About 0 are common, 2 are distinctive, and 13 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈2
Distinctive · estimate
This author's own choices. Worth a look.
13
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 40
Use get_template_part( 'template-parts/content', get_post_type() ) to load reusable partials.
- Very specific: line 64
Never use <script> tags directly in templates.
- Very specific: line 70
Use styles in theme.json for global CSS.
- Very specific: line 72
Use add_theme_support( 'block-templates' ) is not needed — presence of templates/ signals block theme
- Distinctive: line 78
Every plugin must begin with the file header:
- Very specific: line 112
Create uninstall.php (not the uninstall hook) for data cleanup — it runs in a clean context:
- Very specific: line 229
Use Composer autoloading (spl_autoload_register fallback) for class files.
- Very specific: line 240
Always return a value in filter callbacks — omitting the return silently removes the content.
- Very specific: line 273
Document every custom hook with a @since tag and parameter descriptions.
- Very specific: line 303
Always call wp_reset_postdata() after a custom query loop.
- Very specific: line 330
Use delete_transient() when the underlying data changes to prevent stale reads.
- Distinctive: line 352
Transients: use for external API responses and slow queries.
- Very specific: line 353
Query optimization: avoid meta_query on large tables without an index.
- Very specific: line 355
CDN integration: use add_filter( 'wp_calculate_image_srcset', ...
- Very specific: line 400
Use InnerBlocks in block edit and save to allow nested content.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
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-runRelated
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
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

