agentleFS
iwritec0deiwritec0de/wp-dev/skills/wordpress-patterns/SKILL.md

wordpress-patterns

Skill1 starsChanged 7 months agoMITView on GitHub

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.

  • Sends data out

The file

Files
SKILL.md

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

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

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

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

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

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

// 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)

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

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

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)

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)

// 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

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

// 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

RequirementImplementation
Noncewp_create_nonce() + check_ajax_referer()
Capability checkcurrent_user_can() after nonce verification
Input sanitizationsanitize_text_field( wp_unslash( ... ) )
Response formatwp_send_json_success() / wp_send_json_error()
Public endpointRegister both wp_ajax_ and wp_ajax_nopriv_
Admin-only endpointRegister only wp_ajax_ (no nopriv)

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

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.