| name | wordpress-development |
| description | This skill provides comprehensive WordPress development expertise including self-hosted setup with Docker and Nginx, theme development (block and classic), plugin development with security best practices, performance optimization, and security hardening. Use this skill when setting up WordPress environments, developing themes or plugins, optimizing page load times, or implementing security measures. Triggers on requests involving WordPress, WP-CLI, WordPress hooks and filters, theme customization, plugin creation, or WordPress security and performance. |
| user-invocable | true |
WordPress Development Skill
Expert-level WordPress development guidance covering the full stack: infrastructure, themes, plugins, performance, and security.
When to Use This Skill
- Setting up self-hosted WordPress (Docker, server configuration, WP-CLI)
- Developing WordPress themes (block themes, classic themes, child themes)
- Building WordPress plugins (hooks, REST API, custom post types, settings pages)
- Optimizing WordPress performance (caching, lazy loading, database, CDN)
- Implementing WordPress security (sanitization, nonces, capability checks, OWASP)
Quick Reference
Essential WP-CLI Commands
wp core download --locale=en_US
wp core install --url="example.com" --title="Site" --admin_user="admin" --admin_password="password" --admin_email="admin@example.com"
wp core update
wp core verify-checksums
wp plugin install <plugin-slug> --activate
wp plugin update --all
wp plugin deactivate <plugin-slug>
wp db export backup.sql
wp db import backup.sql
wp search-replace 'old-domain.com' 'new-domain.com' --dry-run
wp cache flush
wp transient delete --all
wp cron event run --due-now
Project Structure Standards
project/
โโโ docker/
โ โโโ docker-compose.yml
โ โโโ nginx/
โ โ โโโ default.conf
โ โโโ php/
โ โโโ uploads.ini
โโโ wp-content/
โ โโโ themes/
โ โ โโโ theme-name/
โ โโโ plugins/
โ โ โโโ plugin-name/
โ โโโ uploads/
โโโ .env
1. Self-Hosted WordPress Setup
Docker + Nginx Setup
To set up a new WordPress environment, use the Docker Compose configuration in assets/docker/.
cp -r assets/docker/ ./docker
cp assets/docker/.env.example .env
nano .env
docker-compose up -d
docker-compose exec wordpress wp core install \
--url="localhost" \
--title="My Site" \
--admin_user="admin" \
--admin_password="secure_password" \
--admin_email="admin@example.com"
Production Checklist
wp-config.php Security Settings
define('DISALLOW_FILE_EDIT', true);
define('FORCE_SSL_ADMIN', true);
define('WP_POST_REVISIONS', 5);
define('AUTOSAVE_INTERVAL', 300);
define('AUTH_KEY', 'unique-phrase');
define('SECURE_AUTH_KEY', 'unique-phrase');
define('LOGGED_IN_KEY', 'unique-phrase');
define('NONCE_KEY', 'unique-phrase');
define('AUTH_SALT', 'unique-phrase');
define('SECURE_AUTH_SALT', 'unique-phrase');
define('LOGGED_IN_SALT', 'unique-phrase');
define('NONCE_SALT', 'unique-phrase');
2. Theme Development
Block Theme Structure (WordPress 5.9+)
theme-name/
โโโ style.css # Theme metadata
โโโ theme.json # Global styles and settings
โโโ functions.php # Theme setup (minimal)
โโโ templates/
โ โโโ index.html # Fallback template
โ โโโ single.html # Single post
โ โโโ page.html # Single page
โ โโโ archive.html # Archive pages
โ โโโ 404.html # Not found
โ โโโ parts/
โ โโโ header.html # Header template part
โ โโโ footer.html # Footer template part
โโโ patterns/ # Block patterns
โ โโโ hero.php
โโโ assets/
โโโ css/
โโโ js/
โโโ images/
theme.json Configuration
{
"$schema": "https://schemas.wp.org/trunk/theme.json",
"version": 2,
"settings": {
"color": {
"palette": [
{ "slug": "primary", "color": "#0073aa", "name": "Primary" },
{ "slug": "secondary", "color": "#23282d", "name": "Secondary" }
]
},
"typography": {
"fontFamilies": [
{
"fontFamily": "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
"slug": "system",
"name": "System"
}
],
"fontSizes": [
{ "slug": "small", "size": "0.875rem", "name": "Small" },
{ "slug": "medium", "size": "1rem", "name": "Medium" },
{ "slug": "large", "size": "1.5rem", "name": "Large" }
]
},
"layout": {
"contentSize": "800px",
"wideSize": "1200px"
}
},
"styles": {
"color": {
"background": "var(--wp--preset--color--white)",
"text": "var(--wp--preset--color--secondary)"
}
}
}
Classic Theme Essentials
<?php
declare(strict_types=1);
namespace ThemeName;
spl_autoload_register(function (string $class): void {
$prefix = 'ThemeName\\';
$base_dir = __DIR__ . '/inc/';
$len = strlen($prefix);
if (strncmp($prefix, $class, $len) !== 0) {
return;
}
$relative_class = substr($class, $len);
$file = $base_dir . str_replace('\\', '/', $relative_class) . '.php';
if (file_exists($file)) {
require $file;
}
});
add_action('after_setup_theme', function (): void {
add_theme_support('title-tag');
add_theme_support('post-thumbnails');
add_theme_support('html5', [
'search-form',
'comment-form',
'comment-list',
'gallery',
'caption',
'style',
'script',
]);
add_theme_support('responsive-embeds');
add_theme_support('wp-block-styles');
register_nav_menus([
'primary' => __('Primary Menu', 'theme-name'),
'footer' => __('Footer Menu', 'theme-name'),
]);
});
add_action('wp_enqueue_scripts', function (): void {
$theme_version = wp_get_theme()->get('Version');
wp_enqueue_style(
'theme-style',
get_stylesheet_uri(),
[],
$theme_version
);
wp_enqueue_script(
'theme-script',
get_theme_file_uri('/assets/js/main.js'),
[],
$theme_version,
true
);
});
Child Theme Setup
<?php
add_action('wp_enqueue_scripts', function (): void {
wp_enqueue_style(
'parent-style',
get_template_directory_uri() . '/style.css'
);
wp_enqueue_style(
'child-style',
get_stylesheet_uri(),
['parent-style'],
wp_get_theme()->get('Version')
);
});
3. Plugin Development
Plugin Boilerplate
To create a new plugin, use the boilerplate in assets/plugin-boilerplate/ or run:
python3 scripts/create-plugin.py my-plugin-name "My Plugin" "Plugin description"
Plugin Structure (PSR-12 + WordPress)
plugin-name/
โโโ plugin-name.php # Main plugin file
โโโ uninstall.php # Cleanup on uninstall
โโโ composer.json # Dependencies
โโโ phpcs.xml # Coding standards
โโโ src/
โ โโโ Plugin.php # Main plugin class
โ โโโ Admin/
โ โ โโโ Settings.php # Settings page
โ โ โโโ MetaBox.php # Meta boxes
โ โโโ Frontend/
โ โ โโโ Shortcodes.php # Shortcodes
โ โโโ PostTypes/
โ โ โโโ CustomPost.php # Custom post types
โ โโโ Rest/
โ โโโ Api.php # REST API endpoints
โโโ assets/
โ โโโ css/
โ โโโ js/
โ โโโ images/
โโโ templates/
โ โโโ admin/
โโโ languages/
โ โโโ plugin-name.pot
โโโ tests/
โโโ Unit/
Main Plugin File Pattern
<?php
declare(strict_types=1);
namespace MyPlugin;
defined('ABSPATH') || exit;
define('MY_PLUGIN_VERSION', '1.0.0');
define('MY_PLUGIN_FILE', __FILE__);
define('MY_PLUGIN_PATH', plugin_dir_path(__FILE__));
define('MY_PLUGIN_URL', plugin_dir_url(__FILE__));
if (file_exists(MY_PLUGIN_PATH . 'vendor/autoload.php')) {
require_once MY_PLUGIN_PATH . 'vendor/autoload.php';
}
add_action('plugins_loaded', function (): void {
load_plugin_textdomain('my-plugin', false, dirname(plugin_basename(__FILE__)) . '/languages');
Plugin::getInstance()->init();
});
register_activation_hook(__FILE__, function (): void {
Plugin::activate();
});
register_deactivation_hook(__FILE__, function (): void {
Plugin::deactivate();
});
Common Hooks Reference
add_action('init', 'register_post_types');
add_action('admin_init', 'register_settings');
add_action('admin_menu', 'add_menu_pages');
add_action('wp_enqueue_scripts', 'enqueue_frontend_assets');
add_action('admin_enqueue_scripts', 'enqueue_admin_assets');
add_action('save_post', 'save_meta_data', 10, 3);
add_action('wp_ajax_my_action', 'ajax_handler');
add_action('wp_ajax_nopriv_my_action', 'ajax_handler');
add_action('rest_api_init', 'register_rest_routes');
add_filter('the_content', 'modify_content');
add_filter('the_title', 'modify_title');
add_filter('upload_mimes', 'add_mime_types');
add_filter('plugin_action_links_' . $basename, 'add_links');
Custom Post Type Registration
<?php
declare(strict_types=1);
namespace MyPlugin\PostTypes;
class Product
{
public const POST_TYPE = 'product';
public function register(): void
{
add_action('init', [$this, 'registerPostType']);
add_action('init', [$this, 'registerTaxonomies']);
}
public function registerPostType(): void
{
$labels = [
'name' => __('Products', 'my-plugin'),
'singular_name' => __('Product', 'my-plugin'),
'add_new' => __('Add New', 'my-plugin'),
'add_new_item' => __('Add New Product', 'my-plugin'),
'edit_item' => __('Edit Product', 'my-plugin'),
'new_item' => __('New Product', 'my-plugin'),
'view_item' => __('View Product', 'my-plugin'),
'search_items' => __('Search Products', 'my-plugin'),
'not_found' => __('No products found', 'my-plugin'),
'not_found_in_trash' => __('No products found in trash', 'my-plugin'),
];
$args = [
'labels' => $labels,
'public' => true,
'publicly_queryable' => true,
'show_ui' => true,
'show_in_menu' => true,
'show_in_rest' => true,
'query_var' => true,
'rewrite' => ['slug' => 'products', 'with_front' => false],
'capability_type' => 'post',
'has_archive' => true,
'hierarchical' => false,
'menu_position' => 5,
'menu_icon' => 'dashicons-cart',
'supports' => ['title', 'editor', 'thumbnail', 'excerpt', 'custom-fields'],
];
register_post_type(self::POST_TYPE, $args);
}
public function registerTaxonomies(): void
{
register_taxonomy('product_category', self::POST_TYPE, [
'labels' => [
'name' => __('Categories', 'my-plugin'),
'singular_name' => __('Category', 'my-plugin'),
],
'hierarchical' => true,
'public' => true,
'show_in_rest' => true,
'rewrite' => ['slug' => 'product-category'],
]);
}
}
Settings Page Pattern
<?php
declare(strict_types=1);
namespace MyPlugin\Admin;
class Settings
{
private const OPTION_GROUP = 'my_plugin_options';
private const OPTION_NAME = 'my_plugin_settings';
private const PAGE_SLUG = 'my-plugin-settings';
public function register(): void
{
add_action('admin_menu', [$this, 'addMenuPage']);
add_action('admin_init', [$this, 'registerSettings']);
}
public function addMenuPage(): void
{
add_options_page(
__('My Plugin Settings', 'my-plugin'),
__('My Plugin', 'my-plugin'),
'manage_options',
self::PAGE_SLUG,
[$this, 'renderPage']
);
}
public function registerSettings(): void
{
register_setting(
self::OPTION_GROUP,
self::OPTION_NAME,
[
'type' => 'array',
'sanitize_callback' => [$this, 'sanitize'],
'default' => $this->getDefaults(),
]
);
add_settings_section(
'general_section',
__('General Settings', 'my-plugin'),
null,
self::PAGE_SLUG
);
add_settings_field(
'enable_feature',
__('Enable Feature', 'my-plugin'),
[$this, 'renderCheckbox'],
self::PAGE_SLUG,
'general_section',
['field' => 'enable_feature']
);
}
public function sanitize(array $input): array
{
$sanitized = [];
$sanitized['enable_feature'] = isset($input['enable_feature']) ? 1 : 0;
return $sanitized;
}
private function getDefaults(): array
{
return ['enable_feature' => 0];
}
public function renderCheckbox(array $args): void
{
$options = get_option(self::OPTION_NAME, $this->getDefaults());
$field = $args['field'];
$checked = checked($options[$field] ?? 0, 1, false);
printf(
'<input type="checkbox" name="%s[%s]" value="1" %s />',
esc_attr(self::OPTION_NAME),
esc_attr($field),
$checked
);
}
public function renderPage(): void
{
if (!current_user_can('manage_options')) {
return;
}
?>
<div class="wrap">
<h1><?php echo esc_html(get_admin_page_title()); ?></h1>
<form action="options.php" method="post">
<?php
settings_fields(self::OPTION_GROUP);
do_settings_sections(self::PAGE_SLUG);
submit_button();
?>
</form>
</div>
<?php
}
}
REST API Endpoint
<?php
declare(strict_types=1);
namespace MyPlugin\Rest;
use WP_REST_Request;
use WP_REST_Response;
use WP_Error;
class Api
{
private const NAMESPACE = 'my-plugin/v1';
public function register(): void
{
add_action('rest_api_init', [$this, 'registerRoutes']);
}
public function registerRoutes(): void
{
register_rest_route(self::NAMESPACE, '/items', [
[
'methods' => 'GET',
'callback' => [$this, 'getItems'],
'permission_callback' => [$this, 'checkReadPermission'],
],
[
'methods' => 'POST',
'callback' => [$this, 'createItem'],
'permission_callback' => [$this, 'checkWritePermission'],
'args' => $this->getItemArgs(),
],
]);
register_rest_route(self::NAMESPACE, '/items/(?P<id>\d+)', [
[
'methods' => 'GET',
'callback' => [$this, 'getItem'],
'permission_callback' => [$this, 'checkReadPermission'],
],
[
'methods' => 'PUT',
'callback' => [$this, 'updateItem'],
'permission_callback' => [$this, 'checkWritePermission'],
],
[
'methods' => 'DELETE',
'callback' => [$this, 'deleteItem'],
'permission_callback' => [$this, 'checkDeletePermission'],
],
]);
}
public function checkReadPermission(): bool
{
return true;
}
public function checkWritePermission(): bool
{
return current_user_can('edit_posts');
}
public function checkDeletePermission(): bool
{
return current_user_can('delete_posts');
}
public function getItems(WP_REST_Request $request): WP_REST_Response
{
$items = [];
return new WP_REST_Response($items, 200);
}
public function createItem(WP_REST_Request $request): WP_REST_Response|WP_Error
{
$title = sanitize_text_field($request->get_param('title'));
if (empty($title)) {
return new WP_Error(
'missing_title',
__('Title is required', 'my-plugin'),
['status' => 400]
);
}
$item_id = wp_insert_post([
'post_title' => $title,
'post_type' => 'item',
'post_status' => 'publish',
]);
if (is_wp_error($item_id)) {
return $item_id;
}
return new WP_REST_Response(['id' => $item_id], 201);
}
private function getItemArgs(): array
{
return [
'title' => [
'required' => true,
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
'validate_callback' => fn($value) => !empty($value),
],
];
}
}
4. Performance Optimization
See references/performance-optimization.md for comprehensive optimization techniques.
Quick Performance Wins
remove_action('wp_head', 'print_emoji_detection_script', 7);
remove_action('wp_print_styles', 'print_emoji_styles');
add_filter('script_loader_src', fn($src) => remove_query_arg('ver', $src));
add_filter('style_loader_src', fn($src) => remove_query_arg('ver', $src));
add_filter('xmlrpc_enabled', '__return_false');
define('WP_POST_REVISIONS', 5);
define('WP_MEMORY_LIMIT', '256M');
Database Optimization
DELETE FROM wp_posts WHERE post_type = 'revision';
DELETE pm FROM wp_postmeta pm
LEFT JOIN wp_posts p ON p.ID = pm.post_id
WHERE p.ID IS NULL;
DELETE FROM wp_options WHERE option_name LIKE '%_transient_%'
AND option_name NOT LIKE '%_transient_timeout_%';
OPTIMIZE TABLE wp_options, wp_posts, wp_postmeta, wp_comments;
Lazy Loading & Async
add_filter('script_loader_tag', function (string $tag, string $handle): string {
$defer_scripts = ['theme-script', 'analytics'];
if (in_array($handle, $defer_scripts, true)) {
return str_replace(' src', ' defer src', $tag);
}
return $tag;
}, 10, 2);
add_filter('wp_get_attachment_image_attributes', function (array $attr): array {
$attr['loading'] = 'lazy';
$attr['decoding'] = 'async';
return $attr;
});
Object Caching with Redis
define('WP_REDIS_HOST', 'redis');
define('WP_REDIS_PORT', 6379);
define('WP_REDIS_DATABASE', 0);
$cache_key = 'my_expensive_query';
$data = wp_cache_get($cache_key);
if (false === $data) {
$data = expensive_database_query();
wp_cache_set($cache_key, $data, '', 3600);
}
5. Security Best Practices
See references/security-checklist.md for the complete security checklist.
Input Sanitization
$title = sanitize_text_field($_POST['title']);
$email = sanitize_email($_POST['email']);
$url = esc_url_raw($_POST['url']);
$content = wp_kses_post($_POST['content']);
$filename = sanitize_file_name($_POST['filename']);
$id = absint($_POST['id']);
$ids = array_map('absint', (array) $_POST['ids']);
Output Escaping
<input value="<?php echo esc_attr($value); ?>">
<p><?php echo esc_html($text); ?></p>
<a href="<?php echo esc_url($url); ?>">Link</a>
<script>var data = <?php echo wp_json_encode($data); ?>;</script>
echo esc_html__('Translated text', 'text-domain');
echo esc_attr__('Translated attribute', 'text-domain');
Nonce Verification
wp_nonce_field('my_action_nonce', 'my_nonce');
public function handleFormSubmit(): void
{
if (!isset($_POST['my_nonce']) ||
!wp_verify_nonce($_POST['my_nonce'], 'my_action_nonce')) {
wp_die(__('Security check failed', 'my-plugin'));
}
if (!current_user_can('manage_options')) {
wp_die(__('Unauthorized access', 'my-plugin'));
}
}
wp_localize_script('my-script', 'myAjax', [
'url' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('my_ajax_nonce'),
]);
public function ajaxHandler(): void
{
check_ajax_referer('my_ajax_nonce', 'nonce');
wp_send_json_success(['message' => 'Success']);
}
Capability Checks
if (!current_user_can('edit_post', $post_id)) {
wp_die(__('You cannot edit this post.', 'my-plugin'));
}
'permission_callback' => function (WP_REST_Request $request): bool {
$post_id = $request->get_param('id');
return current_user_can('edit_post', $post_id);
}
if (current_user_can('manage_options')) {
}
SQL Injection Prevention
global $wpdb;
$results = $wpdb->get_results(
"SELECT * FROM {$wpdb->posts} WHERE post_title = '$title'"
);
$results = $wpdb->get_results(
$wpdb->prepare(
"SELECT * FROM {$wpdb->posts} WHERE post_title = %s",
$title
)
);
$results = $wpdb->get_results(
$wpdb->prepare(
"SELECT * FROM {$wpdb->posts} WHERE post_type = %s AND ID = %d",
$post_type,
$post_id
)
);
$ids = [1, 2, 3];
$placeholders = implode(',', array_fill(0, count($ids), '%d'));
$results = $wpdb->get_results(
$wpdb->prepare(
"SELECT * FROM {$wpdb->posts} WHERE ID IN ($placeholders)",
...$ids
)
);
File Upload Security
public function handleFileUpload(): array|WP_Error
{
if (!current_user_can('upload_files')) {
return new WP_Error('unauthorized', 'Cannot upload files');
}
$file = $_FILES['my_file'];
$allowed_types = ['image/jpeg', 'image/png', 'image/gif', 'application/pdf'];
$file_type = wp_check_filetype($file['name']);
if (!in_array($file_type['type'], $allowed_types, true)) {
return new WP_Error('invalid_type', 'File type not allowed');
}
if ($file['size'] > 5 * 1024 * 1024) {
return new WP_Error('too_large', 'File exceeds maximum size');
}
require_once ABSPATH . 'wp-admin/includes/file.php';
require_once ABSPATH . 'wp-admin/includes/media.php';
require_once ABSPATH . 'wp-admin/includes/image.php';
$upload = wp_handle_upload($file, ['test_form' => false]);
if (isset($upload['error'])) {
return new WP_Error('upload_error', $upload['error']);
}
return $upload;
}
XSS Prevention Checklist
- Always escape output - Use
esc_html(), esc_attr(), esc_url(), wp_kses_post()
- Never trust user input - Sanitize all input before processing
- Use Content Security Policy - Add CSP headers
- Validate data types - Ensure integers are integers, emails are emails
- Use prepared statements - Never concatenate SQL queries
Coding Standards
This skill follows PSR-12 + WordPress standards:
<?xml version="1.0"?>
<ruleset name="WordPress Plugin">
<description>PSR-12 with WordPress additions</description>
<rule ref="PSR12"/>
<rule ref="WordPress.Security"/>
<rule ref="WordPress.DB.PreparedSQL"/>
<rule ref="WordPress.WP.I18n"/>
<file>./src</file>
<file>./plugin-name.php</file>
<exclude-pattern>/vendor/*</exclude-pattern>
<exclude-pattern>/node_modules/*</exclude-pattern>
</ruleset>
Run with: composer require --dev squizlabs/php_codesniffer wp-coding-standards/wpcs
Resources
- Docker setup:
assets/docker/
- Plugin boilerplate:
assets/plugin-boilerplate/
- Theme boilerplate:
assets/theme-boilerplate/
- Security checklist:
references/security-checklist.md
- Performance guide:
references/performance-optimization.md
- Create plugin script:
scripts/create-plugin.py
- Create theme script:
scripts/create-theme.py