wordpress-development
SKILL.md
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
# Core management
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
# Plugin management
wp plugin install <plugin-slug> --activate
wp plugin update --all
wp plugin deactivate <plugin-slug>
# Database
wp db export backup.sql
wp db import backup.sql
wp search-replace 'old-domain.com' 'new-domain.com' --dry-run
# Maintenance
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/.
# Copy and customize
cp -r assets/docker/ ./docker
cp assets/docker/.env.example .env
# Edit environment variables
nano .env
# Start services
docker-compose up -d
# Install WordPress via WP-CLI
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
- Configure SSL/TLS with Let's Encrypt
- Set up automated backups (database + uploads)
- Configure proper file permissions (755 dirs, 644 files)
- Disable file editing in wp-config.php:
define('DISALLOW_FILE_EDIT', true); - Set proper memory limits:
define('WP_MEMORY_LIMIT', '256M'); - Configure Redis/Memcached for object caching
- Set up log rotation
wp-config.php Security Settings
// Disable file editing
define('DISALLOW_FILE_EDIT', true);
// Force SSL admin
define('FORCE_SSL_ADMIN', true);
// Limit revisions
define('WP_POST_REVISIONS', 5);
// Auto-save interval
define('AUTOSAVE_INTERVAL', 300);
// Security keys (generate at https://api.wordpress.org/secret-key/1.1/salt/)
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
// functions.php - Theme setup
declare(strict_types=1);
namespace ThemeName;
// Autoload classes
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 {
// Theme supports
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 menus
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');
// Styles
wp_enqueue_style(
'theme-style',
get_stylesheet_uri(),
[],
$theme_version
);
// Scripts
wp_enqueue_script(
'theme-script',
get_theme_file_uri('/assets/js/main.js'),
[],
$theme_version,
true
);
});
Child Theme Setup
<?php
/**
* Theme Name: Parent Theme Child
* Template: parent-theme
* Version: 1.0.0
*/
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
/**
* Plugin Name: My Plugin
* Plugin URI: https://example.com/plugin
* Description: Plugin description here.
* Version: 1.0.0
* Requires at least: 6.0
* Requires PHP: 8.0
* Author: Your Name
* Author URI: https://example.com
* License: GPL v2 or later
* License URI: https://www.gnu.org/licenses/gpl-2.0.html
* Text Domain: my-plugin
* Domain Path: /languages
*/
declare(strict_types=1);
namespace MyPlugin;
// Prevent direct access
defined('ABSPATH') || exit;
// Plugin constants
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__));
// Composer autoloader
if (file_exists(MY_PLUGIN_PATH . 'vendor/autoload.php')) {
require_once MY_PLUGIN_PATH . 'vendor/autoload.php';
}
// Initialize plugin
add_action('plugins_loaded', function (): void {
load_plugin_textdomain('my-plugin', false, dirname(plugin_basename(__FILE__)) . '/languages');
Plugin::getInstance()->init();
});
// Activation hook
register_activation_hook(__FILE__, function (): void {
Plugin::activate();
});
// Deactivation hook
register_deactivation_hook(__FILE__, function (): void {
Plugin::deactivate();
});
Common Hooks Reference
// Actions - Execute code at specific points
add_action('init', 'register_post_types'); // After WP loads
add_action('admin_init', 'register_settings'); // Admin initialization
add_action('admin_menu', 'add_menu_pages'); // Add admin menus
add_action('wp_enqueue_scripts', 'enqueue_frontend_assets'); // Frontend assets
add_action('admin_enqueue_scripts', 'enqueue_admin_assets'); // Admin assets
add_action('save_post', 'save_meta_data', 10, 3); // Post save
add_action('wp_ajax_my_action', 'ajax_handler'); // Logged-in AJAX
add_action('wp_ajax_nopriv_my_action', 'ajax_handler'); // Guest AJAX
add_action('rest_api_init', 'register_rest_routes'); // REST API routes
// Filters - Modify data
add_filter('the_content', 'modify_content'); // Post content
add_filter('the_title', 'modify_title'); // Post title
add_filter('upload_mimes', 'add_mime_types'); // Allowed uploads
add_filter('plugin_action_links_' . $basename, 'add_links'); // Plugin 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, // Enable Gutenberg
'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 read
}
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 = []; // Fetch 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]
);
}
// Create item
$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
// 1. Disable emojis
remove_action('wp_head', 'print_emoji_detection_script', 7);
remove_action('wp_print_styles', 'print_emoji_styles');
// 2. Remove query strings from static resources
add_filter('script_loader_src', fn($src) => remove_query_arg('ver', $src));
add_filter('style_loader_src', fn($src) => remove_query_arg('ver', $src));
// 3. Disable XML-RPC
add_filter('xmlrpc_enabled', '__return_false');
// 4. Limit post revisions in wp-config.php
define('WP_POST_REVISIONS', 5);
// 5. Increase memory limit
define('WP_MEMORY_LIMIT', '256M');
Database Optimization
-- Delete all revisions
DELETE FROM wp_posts WHERE post_type = 'revision';
-- Delete orphaned postmeta
DELETE pm FROM wp_postmeta pm
LEFT JOIN wp_posts p ON p.ID = pm.post_id
WHERE p.ID IS NULL;
-- Delete expired transients
DELETE FROM wp_options WHERE option_name LIKE '%_transient_%'
AND option_name NOT LIKE '%_transient_timeout_%';
-- Optimize tables
OPTIMIZE TABLE wp_options, wp_posts, wp_postmeta, wp_comments;
Lazy Loading & Async
// Defer non-critical scripts
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);
// Native lazy loading for images
add_filter('wp_get_attachment_image_attributes', function (array $attr): array {
$attr['loading'] = 'lazy';
$attr['decoding'] = 'async';
return $attr;
});
Object Caching with Redis
// wp-config.php
define('WP_REDIS_HOST', 'redis');
define('WP_REDIS_PORT', 6379);
define('WP_REDIS_DATABASE', 0);
// Use in plugin/theme
$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); // 1 hour
}
5. Security Best Practices
See references/security-checklist.md for the complete security checklist.
Input Sanitization
// Text input
$title = sanitize_text_field($_POST['title']);
$email = sanitize_email($_POST['email']);
$url = esc_url_raw($_POST['url']);
// HTML content (allow safe tags)
$content = wp_kses_post($_POST['content']);
// Filename
$filename = sanitize_file_name($_POST['filename']);
// Integer
$id = absint($_POST['id']);
// Array of integers
$ids = array_map('absint', (array) $_POST['ids']);
Output Escaping
// HTML attributes
<input value="<?php echo esc_attr($value); ?>">
// HTML content
<p><?php echo esc_html($text); ?></p>
// URLs
<a href="<?php echo esc_url($url); ?>">Link</a>
// JavaScript
<script>var data = <?php echo wp_json_encode($data); ?>;</script>
// Translations with escaping
echo esc_html__('Translated text', 'text-domain');
echo esc_attr__('Translated attribute', 'text-domain');
Nonce Verification
// Create nonce field in form
wp_nonce_field('my_action_nonce', 'my_nonce');
// Verify nonce on submission
public function handleFormSubmit(): void
{
// Check nonce
if (!isset($_POST['my_nonce']) ||
!wp_verify_nonce($_POST['my_nonce'], 'my_action_nonce')) {
wp_die(__('Security check failed', 'my-plugin'));
}
// Check capability
if (!current_user_can('manage_options')) {
wp_die(__('Unauthorized access', 'my-plugin'));
}
// Process form...
}
// AJAX nonce
wp_localize_script('my-script', 'myAjax', [
'url' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('my_ajax_nonce'),
]);
// Verify AJAX nonce
public function ajaxHandler(): void
{
check_ajax_referer('my_ajax_nonce', 'nonce');
// Process AJAX...
wp_send_json_success(['message' => 'Success']);
}
Capability Checks
// Check before action
if (!current_user_can('edit_post', $post_id)) {
wp_die(__('You cannot edit this post.', 'my-plugin'));
}
// Check in REST API
'permission_callback' => function (WP_REST_Request $request): bool {
$post_id = $request->get_param('id');
return current_user_can('edit_post', $post_id);
}
// Check for specific capability
if (current_user_can('manage_options')) {
// Admin only code
}
SQL Injection Prevention
// ALWAYS use $wpdb->prepare()
global $wpdb;
// Wrong - SQL injection vulnerable
$results = $wpdb->get_results(
"SELECT * FROM {$wpdb->posts} WHERE post_title = '$title'"
);
// Correct - Using prepare
$results = $wpdb->get_results(
$wpdb->prepare(
"SELECT * FROM {$wpdb->posts} WHERE post_title = %s",
$title
)
);
// Multiple placeholders
$results = $wpdb->get_results(
$wpdb->prepare(
"SELECT * FROM {$wpdb->posts} WHERE post_type = %s AND ID = %d",
$post_type,
$post_id
)
);
// IN clause with array
$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
{
// Check nonce and capability first
if (!current_user_can('upload_files')) {
return new WP_Error('unauthorized', 'Cannot upload files');
}
$file = $_FILES['my_file'];
// Validate file type
$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');
}
// Validate file size (5MB max)
if ($file['size'] > 5 * 1024 * 1024) {
return new WP_Error('too_large', 'File exceeds maximum size');
}
// Use WordPress upload handling
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:
<!-- phpcs.xml -->
<?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
Weekly Installs
3
Repository
gaupoit/wordpre…nt-skillGitHub Stars
4
First Seen
Jan 24, 2026
Security Audits
Installed on
claude-code3
opencode2
gemini-cli2
antigravity2
windsurf2
codex2