# Wordpress Patterns

> This skill should be used when the user asks to "register a custom post type", "create a taxonomy", "add a meta box", "build a REST API endpoint", "use the Settings API", "schedule a cron event", "use transients", or mentions "custom post type", "taxonomy", "meta box", "REST API", "Settings API", "wp_cron", "transients", "enqueue scripts", "admin notices". Provides common WordPress development patterns including Custom Post Types, taxonomies, meta boxes, REST API endpoints, Settings API, cron, transients, and enqueue patterns.

- Skill: `iwritec0de/wordpress-patterns` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add iwritec0de/wordpress-patterns`
- Raw SKILL.md: https://api.skillmd.com/api/skills/iwritec0de/wordpress-patterns/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Integrations & APIs
- License: MIT
- Author: iwritec0de (https://skillmd.com/u/iwritec0de)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/iwritec0de/wordpress-patterns

---


# WordPress Development Patterns

This skill covers common WordPress development patterns including Custom Post Types, taxonomies, meta boxes, REST API endpoints, Settings API, cron, transients, and enqueue patterns.

## Custom Post Types

```php
add_action( 'init', 'myplugin_register_post_types' );

function myplugin_register_post_types(): void {
    $labels = array(
        'name'               => _x( 'Events', 'post type general name', 'myplugin' ),
        'singular_name'      => _x( 'Event', 'post type singular name', 'myplugin' ),
        'menu_name'          => _x( 'Events', 'admin menu', 'myplugin' ),
        'add_new'            => _x( 'Add New', 'event', 'myplugin' ),
        'add_new_item'       => __( 'Add New Event', 'myplugin' ),
        'edit_item'          => __( 'Edit Event', 'myplugin' ),
        'new_item'           => __( 'New Event', 'myplugin' ),
        'view_item'          => __( 'View Event', 'myplugin' ),
        'search_items'       => __( 'Search Events', 'myplugin' ),
        'not_found'          => __( 'No events found.', 'myplugin' ),
        'not_found_in_trash' => __( 'No events found in Trash.', 'myplugin' ),
    );

    $args = array(
        'labels'              => $labels,
        'public'              => true,
        'publicly_queryable'  => true,
        'show_ui'             => true,
        'show_in_menu'        => true,
        'show_in_rest'        => true,   // Required for Gutenberg
        'query_var'           => true,
        'rewrite'             => array( 'slug' => 'events' ),
        'capability_type'     => 'post',
        'has_archive'         => true,
        'hierarchical'        => false,
        'menu_position'       => 20,
        'menu_icon'           => 'dashicons-calendar-alt',
        'supports'            => array( 'title', 'editor', 'thumbnail', 'excerpt', 'custom-fields' ),
    );

    register_post_type( 'myplugin_event', $args );
}
```

## Custom Taxonomies

```php
add_action( 'init', 'myplugin_register_taxonomies' );

function myplugin_register_taxonomies(): void {
    $labels = array(
        'name'              => _x( 'Event Types', 'taxonomy general name', 'myplugin' ),
        'singular_name'     => _x( 'Event Type', 'taxonomy singular name', 'myplugin' ),
        'search_items'      => __( 'Search Event Types', 'myplugin' ),
        'all_items'         => __( 'All Event Types', 'myplugin' ),
        'edit_item'         => __( 'Edit Event Type', 'myplugin' ),
        'update_item'       => __( 'Update Event Type', 'myplugin' ),
        'add_new_item'      => __( 'Add New Event Type', 'myplugin' ),
        'new_item_name'     => __( 'New Event Type Name', 'myplugin' ),
        'menu_name'         => __( 'Event Types', 'myplugin' ),
    );

    register_taxonomy( 'myplugin_event_type', array( 'myplugin_event' ), array(
        'hierarchical'      => true,
        'labels'            => $labels,
        'show_ui'           => true,
        'show_admin_column' => true,
        'show_in_rest'      => true,
        'query_var'         => true,
        'rewrite'           => array( 'slug' => 'event-type' ),
    ) );
}
```

## Meta Boxes

```php
add_action( 'add_meta_boxes', 'myplugin_add_meta_boxes' );
add_action( 'save_post_myplugin_event', 'myplugin_save_meta_box' );

function myplugin_add_meta_boxes(): void {
    add_meta_box(
        'myplugin_event_details',
        __( 'Event Details', 'myplugin' ),
        'myplugin_render_meta_box',
        'myplugin_event',
        'normal',
        'high'
    );
}

function myplugin_render_meta_box( WP_Post $post ): void {
    wp_nonce_field( 'myplugin_save_event_details', 'myplugin_event_nonce' );

    $date = get_post_meta( $post->ID, '_myplugin_event_date', true );
    $location = get_post_meta( $post->ID, '_myplugin_event_location', true );
    ?>
    <p>
        <label for="myplugin_event_date"><?php esc_html_e( 'Date:', 'myplugin' ); ?></label>
        <input type="date" id="myplugin_event_date" name="myplugin_event_date"
               value="<?php echo esc_attr( $date ); ?>">
    </p>
    <p>
        <label for="myplugin_event_location"><?php esc_html_e( 'Location:', 'myplugin' ); ?></label>
        <input type="text" id="myplugin_event_location" name="myplugin_event_location"
               value="<?php echo esc_attr( $location ); ?>" class="widefat">
    </p>
    <?php
}

function myplugin_save_meta_box( int $post_id ): void {
    // 1. Verify nonce
    if ( ! isset( $_POST['myplugin_event_nonce'] ) ||
         ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['myplugin_event_nonce'] ) ), 'myplugin_save_event_details' ) ) {
        return;
    }

    // 2. Check autosave
    if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
        return;
    }

    // 3. Verify capability
    if ( ! current_user_can( 'edit_post', $post_id ) ) {
        return;
    }

    // 4. Sanitize and save
    if ( isset( $_POST['myplugin_event_date'] ) ) {
        update_post_meta( $post_id, '_myplugin_event_date',
            sanitize_text_field( wp_unslash( $_POST['myplugin_event_date'] ) ) );
    }

    if ( isset( $_POST['myplugin_event_location'] ) ) {
        update_post_meta( $post_id, '_myplugin_event_location',
            sanitize_text_field( wp_unslash( $_POST['myplugin_event_location'] ) ) );
    }
}
```

## Settings API

```php
add_action( 'admin_menu', 'myplugin_add_settings_page' );
add_action( 'admin_init', 'myplugin_register_settings' );

function myplugin_add_settings_page(): void {
    add_options_page(
        __( 'My Plugin Settings', 'myplugin' ),
        __( 'My Plugin', 'myplugin' ),
        'manage_options',
        'myplugin-settings',
        'myplugin_render_settings_page'
    );
}

function myplugin_register_settings(): void {
    register_setting( 'myplugin_settings_group', 'myplugin_api_key', array(
        'type'              => 'string',
        'sanitize_callback' => 'sanitize_text_field',
        'default'           => '',
    ) );

    add_settings_section(
        'myplugin_general_section',
        __( 'General Settings', 'myplugin' ),
        'myplugin_general_section_cb',
        'myplugin-settings'
    );

    add_settings_field(
        'myplugin_api_key',
        __( 'API Key', 'myplugin' ),
        'myplugin_api_key_field_cb',
        'myplugin-settings',
        'myplugin_general_section'
    );
}

function myplugin_general_section_cb(): void {
    echo '<p>' . esc_html__( 'Configure your plugin settings.', 'myplugin' ) . '</p>';
}

function myplugin_api_key_field_cb(): void {
    $value = get_option( 'myplugin_api_key', '' );
    echo '<input type="text" name="myplugin_api_key" value="' . esc_attr( $value ) . '" class="regular-text">';
}

function myplugin_render_settings_page(): 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( 'myplugin_settings_group' );
            do_settings_sections( 'myplugin-settings' );
            submit_button();
            ?>
        </form>
    </div>
    <?php
}
```

## REST API Custom Endpoints

```php
add_action( 'rest_api_init', 'myplugin_register_rest_routes' );

function myplugin_register_rest_routes(): void {
    register_rest_route( 'myplugin/v1', '/events', array(
        array(
            'methods'             => WP_REST_Server::READABLE,
            'callback'            => 'myplugin_rest_get_events',
            'permission_callback' => '__return_true', // Public endpoint
            'args'                => array(
                'per_page' => array(
                    'default'           => 10,
                    'sanitize_callback' => 'absint',
                    'validate_callback' => function ( $value ): bool {
                        return $value > 0 && $value <= 100;
                    },
                ),
            ),
        ),
        array(
            'methods'             => WP_REST_Server::CREATABLE,
            'callback'            => 'myplugin_rest_create_event',
            'permission_callback' => function (): bool {
                return current_user_can( 'publish_posts' );
            },
            'args'                => array(
                'title' => array(
                    'required'          => true,
                    'sanitize_callback' => 'sanitize_text_field',
                ),
            ),
        ),
    ) );

    register_rest_route( 'myplugin/v1', '/events/(?P<id>\d+)', array(
        'methods'             => WP_REST_Server::READABLE,
        'callback'            => 'myplugin_rest_get_event',
        'permission_callback' => '__return_true',
        'args'                => array(
            'id' => array(
                'sanitize_callback' => 'absint',
            ),
        ),
    ) );
}

function myplugin_rest_get_events( WP_REST_Request $request ): WP_REST_Response {
    $per_page = $request->get_param( 'per_page' );

    $query = new WP_Query( array(
        'post_type'      => 'myplugin_event',
        'posts_per_page' => $per_page,
        'post_status'    => 'publish',
    ) );

    $events = array_map( function ( WP_Post $post ): array {
        return array(
            'id'    => $post->ID,
            'title' => $post->post_title,
            'date'  => get_post_meta( $post->ID, '_myplugin_event_date', true ),
        );
    }, $query->posts );

    return new WP_REST_Response( $events, 200 );
}
```

## Cron / Scheduled Events

```php
// Schedule on activation:
register_activation_hook( __FILE__, 'myplugin_activate_cron' );
register_deactivation_hook( __FILE__, 'myplugin_deactivate_cron' );

function myplugin_activate_cron(): void {
    if ( ! wp_next_scheduled( 'myplugin_daily_cleanup' ) ) {
        wp_schedule_event( time(), 'daily', 'myplugin_daily_cleanup' );
    }
}

function myplugin_deactivate_cron(): void {
    wp_clear_scheduled_hook( 'myplugin_daily_cleanup' );
}

add_action( 'myplugin_daily_cleanup', 'myplugin_run_cleanup' );

function myplugin_run_cleanup(): void {
    // Cleanup logic here.
}

// Custom interval:
add_filter( 'cron_schedules', 'myplugin_add_cron_interval' );

function myplugin_add_cron_interval( array $schedules ): array {
    $schedules['myplugin_fifteen_minutes'] = array(
        'interval' => 900,
        'display'  => esc_html__( 'Every 15 Minutes', 'myplugin' ),
    );
    return $schedules;
}
```

## Transients (Caching)

```php
function myplugin_get_api_data(): array {
    $data = get_transient( 'myplugin_api_data' );

    if ( false === $data ) {
        $response = wp_remote_get( 'https://api.example.com/data' );

        if ( is_wp_error( $response ) ) {
            return array();
        }

        $data = json_decode( wp_remote_retrieve_body( $response ), true );
        set_transient( 'myplugin_api_data', $data, HOUR_IN_SECONDS );
    }

    return $data;
}

// Invalidate when settings change:
add_action( 'update_option_myplugin_api_key', function (): void {
    delete_transient( 'myplugin_api_data' );
} );
```

## Enqueue Scripts & Styles

```php
add_action( 'wp_enqueue_scripts', 'myplugin_enqueue_public' );
add_action( 'admin_enqueue_scripts', 'myplugin_enqueue_admin' );

function myplugin_enqueue_public(): void {
    wp_enqueue_style(
        'myplugin-public',
        plugin_dir_url( __FILE__ ) . 'public/css/style.css',
        array(),
        MYPLUGIN_VERSION
    );

    wp_enqueue_script(
        'myplugin-public',
        plugin_dir_url( __FILE__ ) . 'public/js/script.js',
        array( 'jquery' ),
        MYPLUGIN_VERSION,
        true // Load in footer
    );

    wp_localize_script( 'myplugin-public', 'myPluginData', array(
        'ajaxUrl' => admin_url( 'admin-ajax.php' ),
        'nonce'   => wp_create_nonce( 'myplugin_public_nonce' ),
    ) );
}

function myplugin_enqueue_admin( string $hook ): void {
    // Only load on our admin pages:
    if ( 'settings_page_myplugin-settings' !== $hook ) {
        return;
    }

    wp_enqueue_style(
        'myplugin-admin',
        plugin_dir_url( __FILE__ ) . 'admin/css/admin.css',
        array(),
        MYPLUGIN_VERSION
    );
}
```

## Admin Notices

```php
add_action( 'admin_notices', 'myplugin_admin_notice' );

function myplugin_admin_notice(): void {
    if ( get_option( 'myplugin_api_key' ) ) {
        return;
    }

    $settings_url = admin_url( 'options-general.php?page=myplugin-settings' );
    ?>
    <div class="notice notice-warning is-dismissible">
        <p>
            <?php
            printf(
                /* translators: %s: settings page URL */
                wp_kses(
                    __( 'My Plugin requires an API key. <a href="%s">Configure it here</a>.', 'myplugin' ),
                    array( 'a' => array( 'href' => array() ) )
                ),
                esc_url( $settings_url )
            );
            ?>
        </p>
    </div>
    <?php
}
```

## AJAX Handlers

### Admin AJAX (Logged-in Users)

```php
add_action( 'wp_ajax_myplugin_save_data', 'myplugin_ajax_save_data' );

function myplugin_ajax_save_data(): void {
    check_ajax_referer( 'myplugin_nonce', 'nonce' );

    if ( ! current_user_can( 'edit_posts' ) ) {
        wp_send_json_error( __( 'Unauthorized.', 'myplugin' ), 403 );
    }

    $title = sanitize_text_field( wp_unslash( $_POST['title'] ?? '' ) );
    if ( empty( $title ) ) {
        wp_send_json_error( __( 'Title is required.', 'myplugin' ) );
    }

    $post_id = wp_insert_post( [
        'post_title'  => $title,
        'post_type'   => 'myplugin_event',
        'post_status' => 'draft',
    ] );

    if ( is_wp_error( $post_id ) ) {
        wp_send_json_error( $post_id->get_error_message() );
    }

    wp_send_json_success( [ 'post_id' => $post_id ] );
}
```

### Public AJAX (No Login Required)

```php
// Register for both logged-in and logged-out users
add_action( 'wp_ajax_myplugin_search', 'myplugin_ajax_search' );
add_action( 'wp_ajax_nopriv_myplugin_search', 'myplugin_ajax_search' );

function myplugin_ajax_search(): void {
    check_ajax_referer( 'myplugin_public_nonce', 'nonce' );

    $query = sanitize_text_field( wp_unslash( $_GET['q'] ?? '' ) );

    $results = new WP_Query( [
        'post_type'      => 'myplugin_event',
        'posts_per_page' => 10,
        's'              => $query,
        'post_status'    => 'publish',
    ] );

    $items = array_map( fn( WP_Post $post ): array => [
        'id'    => $post->ID,
        'title' => $post->post_title,
        'url'   => get_permalink( $post ),
    ], $results->posts );

    wp_send_json_success( $items );
}
```

### Enqueuing with AJAX Support

```php
add_action( 'wp_enqueue_scripts', 'myplugin_enqueue_ajax' );

function myplugin_enqueue_ajax(): void {
    wp_enqueue_script(
        'myplugin-ajax',
        plugin_dir_url( __FILE__ ) . 'public/js/ajax.js',
        [],
        MYPLUGIN_VERSION,
        true
    );

    wp_localize_script( 'myplugin-ajax', 'myPluginAjax', [
        'url'   => admin_url( 'admin-ajax.php' ),
        'nonce' => wp_create_nonce( 'myplugin_public_nonce' ),
    ] );
}
```

### JavaScript AJAX Call

```js
// Using fetch (modern)
async function saveData( title ) {
    const formData = new FormData();
    formData.append( 'action', 'myplugin_save_data' );
    formData.append( 'nonce', myPluginAjax.nonce );
    formData.append( 'title', title );

    const response = await fetch( myPluginAjax.url, {
        method: 'POST',
        body: formData,
    } );

    const result = await response.json();
    if ( result.success ) {
        console.log( 'Saved:', result.data.post_id );
    } else {
        console.error( 'Error:', result.data );
    }
}
```

### AJAX Security Checklist

| Requirement | Implementation |
|-------------|---------------|
| Nonce | `wp_create_nonce()` + `check_ajax_referer()` |
| Capability check | `current_user_can()` after nonce verification |
| Input sanitization | `sanitize_text_field( wp_unslash( ... ) )` |
| Response format | `wp_send_json_success()` / `wp_send_json_error()` |
| Public endpoint | Register both `wp_ajax_` and `wp_ajax_nopriv_` |
| Admin-only endpoint | Register only `wp_ajax_` (no `nopriv`) |

For Gutenberg block development patterns, see `references/gutenberg-patterns.md`.

