wordpress
SKILL.md
WordPress Framework Guide
Applies to: WordPress 6.0+, PHP 8.0+, Plugin Development, Theme Development, REST API, Block Editor (Gutenberg) Language Guide: @.claude/skills/php-guide/SKILL.md
Overview
WordPress is a content management system (CMS) powering over 40% of the web. This guide covers modern WordPress development including plugin architecture, theme development, REST API endpoints, the Block Editor (Gutenberg), and security essentials.
Use WordPress when:
- Content management system is needed
- Blog or publishing platform
- E-commerce with WooCommerce
- Custom applications with familiar admin UI
- Rapid prototyping with existing ecosystem
Consider alternatives when:
- Building pure API backend (use Laravel/Symfony)
- High-performance requirements (consider headless)
- Complex business logic applications
- Microservices architecture
Guardrails
WordPress-Specific Rules
- Use
declare(strict_types=1)in all PHP files - Prevent direct file access:
if (!defined('ABSPATH')) { exit; } - Use namespaces for all plugin/theme classes
- Escape all output:
esc_html(),esc_attr(),esc_url(),wp_kses_post() - Sanitize all input:
sanitize_text_field(),sanitize_email(),absint() - Verify nonces on all form submissions and AJAX requests
- Check capabilities before performing actions:
current_user_can() - Use
$wpdb->prepare()for all database queries (never concatenate) - Register all scripts/styles through
wp_enqueue_scriptshook - Use text domains and
__()/_e()for all user-facing strings - Set
show_in_rest => truefor post types and taxonomies that need Gutenberg/REST support - Use
register_post_meta()to expose meta fields in the REST API - Always include
uninstall.phporregister_uninstall_hook()for cleanup
Anti-Patterns
- Do not use
query_posts()(useWP_Queryorget_posts()) - Do not modify core files (use hooks and filters)
- Do not hardcode URLs (use
home_url(),admin_url(),plugin_dir_url()) - Do not store business logic in template files
- Do not skip nonce verification on any form or AJAX handler
- Do not use
extract()on untrusted data - Do not echo unsanitized user input
- Do not use
$_GET/$_POSTdirectly without sanitization
Project Structure
Plugin Structure
my-plugin/
├── my-plugin.php # Main plugin file (header, constants, bootstrap)
├── includes/
│ ├── class-plugin.php # Main plugin class (singleton)
│ ├── class-activator.php # Activation hooks
│ ├── class-deactivator.php # Deactivation hooks
│ ├── admin/
│ │ ├── class-admin.php # Admin functionality
│ │ └── partials/ # Admin templates
│ ├── public/
│ │ ├── class-public.php # Public functionality
│ │ └── partials/ # Public templates
│ ├── api/
│ │ └── class-rest-api.php # REST API endpoints
│ └── blocks/
│ └── my-block/ # Gutenberg blocks
├── assets/
│ ├── css/
│ ├── js/
│ └── images/
├── languages/ # Translation files (.pot, .po, .mo)
├── templates/ # Overridable template files
├── tests/phpunit/
├── composer.json
├── package.json
└── readme.txt # WordPress.org readme
Theme Structure
my-theme/
├── style.css # Theme metadata (required)
├── functions.php # Theme setup and hooks
├── index.php # Fallback template (required)
├── header.php / footer.php # Header/footer templates
├── single.php / page.php # Single post / page templates
├── archive.php / 404.php # Archive / error templates
├── search.php / sidebar.php # Search / sidebar templates
├── inc/ # Customizer, template functions, hooks
├── template-parts/ # Reusable content partials
├── assets/ # CSS, JS, images
├── parts/ # Template parts (FSE)
├── patterns/ # Block patterns
├── templates/ # Block templates (FSE)
└── theme.json # Theme configuration (FSE)
Template Hierarchy
WordPress resolves templates from most specific to least specific. Pattern: {type}-{slug}.php -> {type}-{id}.php -> {type}.php -> index.php
- Single:
single-{post-type}-{slug}->single-{post-type}->single->singular->index - Page:
page-{slug}->page-{id}->page->singular->index - Archive:
archive-{post-type}->archive->index - Category:
category-{slug}->category-{id}->category->archive->index - Taxonomy:
taxonomy-{tax}-{term}->taxonomy-{tax}->taxonomy->archive - Search/404:
search.php/404.php->index.php
Plugin Basics
Main Plugin File
<?php
/**
* Plugin Name: My Plugin
* Plugin URI: https://example.com/my-plugin
* Description: A modern WordPress plugin
* Version: 1.0.0
* Requires at least: 6.0
* Requires PHP: 8.0
* Author: Your Name
* Text Domain: my-plugin
* Domain Path: /languages
*/
declare(strict_types=1);
namespace MyPlugin;
if (!defined('ABSPATH')) {
exit;
}
define('MY_PLUGIN_VERSION', '1.0.0');
define('MY_PLUGIN_PATH', plugin_dir_path(__FILE__));
define('MY_PLUGIN_URL', plugin_dir_url(__FILE__));
define('MY_PLUGIN_BASENAME', plugin_basename(__FILE__));
require_once MY_PLUGIN_PATH . 'vendor/autoload.php';
register_activation_hook(__FILE__, [Activator::class, 'activate']);
register_deactivation_hook(__FILE__, [Deactivator::class, 'deactivate']);
add_action('plugins_loaded', function (): void {
Plugin::getInstance()->init();
});
Singleton Plugin Class
<?php
declare(strict_types=1);
namespace MyPlugin;
final class Plugin
{
private static ?self $instance = null;
public static function getInstance(): self
{
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
private function __construct() {}
public function init(): void
{
load_plugin_textdomain('my-plugin', false, dirname(MY_PLUGIN_BASENAME) . '/languages');
if (is_admin()) {
new Admin\Admin();
}
new Frontend\Frontend();
new Api\RestApi();
new Blocks\BlockManager();
}
}
Hooks and Filters
Common Hook Patterns
// Actions (do something at a specific point)
add_action('init', [$this, 'registerPostTypes']);
add_action('wp_enqueue_scripts', [$this, 'enqueueAssets']);
add_action('admin_enqueue_scripts', [$this, 'enqueueAdminAssets']);
add_action('save_post', [$this, 'onSavePost'], 10, 3);
add_action('wp_ajax_my_action', [$this, 'handleAjax']);
add_action('wp_ajax_nopriv_my_action', [$this, 'handleAjax']);
add_action('rest_api_init', [$this, 'registerRoutes']);
// Filters (modify data and return it)
add_filter('the_content', [$this, 'filterContent']);
add_filter('the_title', [$this, 'filterTitle'], 10, 2);
add_filter('excerpt_length', fn() => 30);
add_filter('post_class', [$this, 'addPostClasses'], 10, 3);
// Custom hooks (for extensibility)
do_action('my_plugin_after_save', $postId, $data);
$value = apply_filters('my_plugin_format_price', $price, $currency);
Asset Enqueuing
public function enqueueAssets(): void
{
wp_enqueue_style('my-plugin-style', MY_PLUGIN_URL . 'assets/css/public.css', [], MY_PLUGIN_VERSION);
wp_enqueue_script('my-plugin-script', MY_PLUGIN_URL . 'assets/js/public.js', ['jquery'], MY_PLUGIN_VERSION, true);
wp_localize_script('my-plugin-script', 'MyPluginData', [
'ajaxUrl' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('my_plugin_nonce'),
'strings' => [
'loading' => __('Loading...', 'my-plugin'),
'error' => __('An error occurred.', 'my-plugin'),
],
]);
}
REST API
Custom Endpoint Pattern
<?php
declare(strict_types=1);
namespace MyPlugin\Api;
use WP_REST_Controller;
use WP_REST_Request;
use WP_REST_Response;
use WP_REST_Server;
use WP_Error;
final class BooksController extends WP_REST_Controller
{
protected $namespace = 'my-plugin/v1';
protected $rest_base = 'books';
public function registerRoutes(): void
{
register_rest_route($this->namespace, '/' . $this->rest_base, [
[
'methods' => WP_REST_Server::READABLE,
'callback' => [$this, 'getItems'],
'permission_callback' => [$this, 'getItemsPermissions'],
'args' => $this->getCollectionParams(),
],
[
'methods' => WP_REST_Server::CREATABLE,
'callback' => [$this, 'createItem'],
'permission_callback' => [$this, 'createItemPermissions'],
],
]);
}
public function getItems(WP_REST_Request $request): WP_REST_Response
{
$query = new \WP_Query([
'post_type' => 'book',
'posts_per_page' => $request->get_param('per_page') ?? 10,
'paged' => $request->get_param('page') ?? 1,
]);
$items = array_map(fn($post) => $this->formatItem($post), $query->posts);
$response = new WP_REST_Response($items, 200);
$response->header('X-WP-Total', $query->found_posts);
$response->header('X-WP-TotalPages', $query->max_num_pages);
return $response;
}
public function getItemsPermissions(): bool { return true; }
public function createItemPermissions(): bool { return current_user_can('publish_posts'); }
}
REST API conventions:
- Extend
WP_REST_Controllerfor full CRUD endpoints - Always define
permission_callback(use__return_truefor truly public) - Sanitize input parameters with
sanitize_callbackinargs - Return
WP_Errorfor error responses with proper status codes - Use pagination headers:
X-WP-Total,X-WP-TotalPages - Version your namespace:
my-plugin/v1
Block Editor (Gutenberg)
Block Registration (PHP)
// Register from block.json (preferred)
register_block_type(MY_PLUGIN_PATH . 'blocks/my-block');
// Dynamic block with server render
register_block_type('my-plugin/featured-items', [
'render_callback' => [$this, 'renderFeaturedItems'],
'attributes' => [
'count' => ['type' => 'number', 'default' => 3],
],
]);
block.json
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 3,
"name": "my-plugin/my-block",
"version": "1.0.0",
"title": "My Block",
"category": "widgets",
"icon": "admin-generic",
"supports": {
"html": false,
"align": ["wide", "full"],
"color": { "background": true, "text": true },
"spacing": { "margin": true, "padding": true }
},
"attributes": {
"content": { "type": "string", "default": "" }
},
"textdomain": "my-plugin",
"editorScript": "file:./index.js",
"editorStyle": "file:./index.css",
"style": "file:./style-index.css",
"render": "file:./render.php"
}
Block JavaScript (index.js)
import { registerBlockType } from '@wordpress/blocks';
import { useBlockProps, InspectorControls } from '@wordpress/block-editor';
import { PanelBody, ToggleControl } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
import ServerSideRender from '@wordpress/server-side-render';
registerBlockType('my-plugin/my-block', {
edit: ({ attributes, setAttributes }) => {
const blockProps = useBlockProps();
return (
<>
<InspectorControls>
<PanelBody title={__('Settings', 'my-plugin')}>
<ToggleControl
label={__('Show Image', 'my-plugin')}
checked={attributes.showImage}
onChange={(val) => setAttributes({ showImage: val })}
/>
</PanelBody>
</InspectorControls>
<div {...blockProps}>
<ServerSideRender block="my-plugin/my-block" attributes={attributes} />
</div>
</>
);
},
save: () => null, // Dynamic block: rendered server-side
});
Security Essentials
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']);
$key = sanitize_key($_POST['key']);
$int = absint($_POST['number']);
Output Escaping
echo esc_html($title); // HTML context
echo esc_attr($attribute); // Attribute context
echo esc_url($url); // URL context
echo esc_js($script); // JS context
echo wp_kses_post($content); // Allow safe HTML
Nonce Verification
// Generate nonce field in form
wp_nonce_field('my_action', 'my_nonce');
// Verify nonce on submission
if (!wp_verify_nonce($_POST['my_nonce'], 'my_action')) {
wp_die(__('Security check failed.', 'my-plugin'));
}
// AJAX nonce check
check_ajax_referer('my_plugin_nonce', 'nonce');
Capability Checks
if (!current_user_can('edit_posts')) {
wp_die(__('Insufficient permissions.', 'my-plugin'));
}
// REST API permission callback
'permission_callback' => fn() => current_user_can('edit_post', $id)
Commands Reference
# Development
npm run build # Build blocks/assets
npm run start # Watch mode for blocks
composer install # PHP dependencies
# Testing
./vendor/bin/phpunit # Run tests
./vendor/bin/phpunit --coverage-html coverage # Coverage report
# Code Quality
./vendor/bin/phpcs # PHP CodeSniffer (WPCS)
./vendor/bin/phpcbf # Auto-fix coding standards
./vendor/bin/phpstan analyse # Static analysis
# WP-CLI essentials
wp plugin activate my-plugin # Activate plugin
wp plugin list --status=active # List active plugins
wp theme activate my-theme # Activate theme
wp db export backup.sql # Database backup
wp post list --post_type=book # List posts
wp cache flush # Clear object cache
wp transient delete --all # Clear transients
wp cron event run --all # Run scheduled events
wp rewrite flush # Flush rewrite rules
Custom WP-CLI Command
if (defined('WP_CLI') && WP_CLI) {
WP_CLI::add_command('mycommand', MyPlugin\CLI\MyCommand::class);
}
Advanced Topics
For detailed patterns and full implementation examples, see:
- references/patterns.md -- Custom post types, taxonomies, meta boxes, Gutenberg blocks, WooCommerce integration, database operations, testing, caching, performance
External References
Weekly Installs
5
Repository
ar4mirez/samuelGitHub Stars
3
First Seen
14 days ago
Security Audits
Installed on
gemini-cli5
opencode5
codebuddy5
github-copilot5
codex5
kimi-cli5