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_scripts hook
  • Use text domains and __() / _e() for all user-facing strings
  • Set show_in_rest => true for post types and taxonomies that need Gutenberg/REST support
  • Use register_post_meta() to expose meta fields in the REST API
  • Always include uninstall.php or register_uninstall_hook() for cleanup

Anti-Patterns

  • Do not use query_posts() (use WP_Query or get_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/$_POST directly 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_Controller for full CRUD endpoints
  • Always define permission_callback (use __return_true for truly public)
  • Sanitize input parameters with sanitize_callback in args
  • Return WP_Error for 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/samuel
GitHub Stars
3
First Seen
14 days ago
Installed on
gemini-cli5
opencode5
codebuddy5
github-copilot5
codex5
kimi-cli5