Claude Code Plugins

Community-maintained marketplace

Feedback

wordpress-blocks

@oiler/claude-skills
0
22

WordPress custom Gutenberg block development with server-side PHP rendering. Includes block registration patterns, media upload integration, multiple item blocks, proper escaping/sanitization, and editor UI best practices. Maintains separation of concerns where editors control content while developers control design.

Install Skill

Shared

Installs to .agents/skills, used by Codex, Amp, Warp, Cursor, OpenCode, and more.

CodexAmp
Warp
CursorOpenCode
Cline
Gemini CLI
GitHub Copilot
Personal

Available across projects.

$npx skills-installer add @oiler/claude-skills/wordpress-blocks --client shared
Project

Writes to .agents/skills.

$npx skills-installer add @oiler/claude-skills/wordpress-blocks -p --client shared
Note: Review the skill instructions before using it.

SKILL.md

name wordpress-blocks
description WordPress custom Gutenberg block development with server-side PHP rendering and no build toolchain. Use when user asks to build, register, or modify WordPress blocks, work with `register_block_type()`, write a `render_callback`, add a `MediaUpload`/`MediaUploadCheck` image picker, create blocks with repeating items, escape or sanitize block output for WP VIP compliance, set up editor UI with WP components (`TextControl`, `TextareaControl`, `Button`), or enqueue block editor assets. Covers WordPress 6.5+ / PHP 8.1+. Teaches PHP-side `attributes` arrays plus vanilla JS using `wp.element` (no `block.json`, no JSX, no `@wordpress/scripts`).

WordPress Custom Gutenberg Blocks

Build custom Gutenberg blocks for WordPress themes that give content editors control over text and media while developers retain control over layout and design.

Block Development Philosophy

Editors Control:

  • Text content (headers, descriptions, body copy)
  • Links and CTAs
  • Images (via WordPress media uploader)

Developers Control:

  • HTML structure and markup
  • CSS styling and layout
  • JavaScript functionality
  • Design consistency

Benefits:

  • Consistent design across the site
  • Easy content updates without developer intervention
  • Reduced risk of breaking layouts
  • Cleaner, more maintainable codebase

Platform baseline

This skill assumes WordPress 6.5+ and PHP 8.1+. WP 6.5 (April 2024) is the right cutoff for blocks work in 2026 — earlier releases miss block-editor APIs this skill relies on. PHP 8.1+ is required by WP 6.5 itself and unlocks readonly properties, enums, and never return types if your render functions want them.

Routing — read the right reference

Starting a new block from scratch? Read the Minimal block template section below first, then return here to pick the right reference for what you're adding.

User wants to... Read first
Add or change attribute types (text, number, textarea, boolean) references/attributes.md
Add an image picker to a block references/media-upload.md
Build a block with repeating items (multiple cards, list items) references/multiple-items.md
Audit escaping, sanitization, asset paths, or JS deps for VIP references/security.md

Block file structure

Each block consists of three files:

inc/blocks/
├── block-name.php           # PHP registration and render
├── js/
│   └── block-name.js        # Editor JavaScript
└── css/                     # Optional
    └── block-name.css       # Block-specific styles

Register block in functions.php:

// custom gutenberg blocks
require get_template_directory() . '/inc/blocks/hp-lede.php';

Minimal block template

A complete copy-pasteable starting point. Escape at the echo site, not at variable assignment — see security.md for the full rationale.

PHP Registration File

File: /inc/blocks/block-name.php

<?php
/**
 * Block Name Block
 */

function register_block_name_block() {
    register_block_type('theme/block-name', array(
        'render_callback' => 'render_block_name_block',
        'attributes' => array(
            'blockTitle' => array(
                'type' => 'string',
                'default' => 'Default Title'
            ),
            'blockDescription' => array(
                'type' => 'string',
                'default' => 'Default description text.'
            ),
            'blockLink' => array(
                'type' => 'string',
                'default' => '/default-link/'
            ),
        ),
    ));
}
add_action('init', 'register_block_name_block');

function render_block_name_block($attributes) {
    $block_title = $attributes['blockTitle'] ?? '';
    $block_description = $attributes['blockDescription'] ?? '';
    $block_link = $attributes['blockLink'] ?? '';

    ob_start();
    ?>
    <section class="block-name">
        <h2><?php echo esc_html($block_title); ?></h2>
        <p><?php echo esc_html($block_description); ?></p>
        <a href="<?php echo esc_url($block_link); ?>" class="button">Learn More</a>
    </section>
    <?php
    return ob_get_clean();
}

function enqueue_block_name_block_editor_assets() {
    wp_enqueue_script(
        'block-name-block',
        get_template_directory_uri() . '/inc/blocks/js/block-name.js',
        array('wp-blocks', 'wp-element', 'wp-block-editor', 'wp-components'),
        filemtime(get_template_directory() . '/inc/blocks/js/block-name.js'),
        false
    );
}
add_action('enqueue_block_editor_assets', 'enqueue_block_name_block_editor_assets');

JavaScript Editor File

File: /inc/blocks/js/block-name.js

(function(wp) {
    const { registerBlockType } = wp.blocks;
    const { TextControl, TextareaControl } = wp.components;
    const { createElement: el } = wp.element;

    registerBlockType('theme/block-name', {
        title: 'Block Name',
        icon: 'admin-post',
        category: 'text',
        attributes: {
            blockTitle: {
                type: 'string',
                default: 'Default Title'
            },
            blockDescription: {
                type: 'string',
                default: 'Default description text.'
            },
            blockLink: {
                type: 'string',
                default: '/default-link/'
            }
        },

        edit: function(props) {
            const { attributes, setAttributes } = props;

            return el('div', {
                className: 'block-name-editor',
                style: { padding: '20px', border: '1px solid #ddd' }
            },
                el('h3', {}, 'Block Name'),

                el(TextControl, {
                    label: 'Block Title',
                    value: attributes.blockTitle,
                    onChange: function(value) {
                        setAttributes({ blockTitle: value });
                    }
                }),

                el(TextareaControl, {
                    label: 'Description',
                    value: attributes.blockDescription,
                    onChange: function(value) {
                        setAttributes({ blockDescription: value });
                    },
                    rows: 4
                }),

                el(TextControl, {
                    label: 'Link',
                    value: attributes.blockLink,
                    onChange: function(value) {
                        setAttributes({ blockLink: value });
                    }
                })
            );
        },

        save: function() {
            return null; // Using PHP render callback
        }
    });
})(window.wp);

Block icons

Common Dashicons for blocks:

icon: 'admin-post'      // Document
icon: 'megaphone'       // Announcement/Lede
icon: 'admin-links'     // Resources/Links
icon: 'info'            // Information
icon: 'warning'         // Urgent/Warning
icon: 'games'           // Sports/Games
icon: 'awards'          // Achievement
icon: 'media-document'  // Article

Editor styling tips

Add Visual Hierarchy in Editor

el('div', {
    className: 'block-editor',
    style: { padding: '20px', border: '1px solid #ddd' }
},
    el('h3', {}, 'Block Title'),
    el('hr'),
    el('h4', {}, 'Section 1'),
    // fields...
    el('hr'),
    el('h4', {}, 'Section 2'),
    // more fields...
)

Add Preview in Editor

el('div', { style: { marginTop: '15px', padding: '10px', backgroundColor: '#f0f0f0' } },
    el('strong', {}, 'Preview:'),
    el('p', { style: { marginTop: '10px' } }, attributes.description)
)

A note on block.json

block.json is WordPress's canonical modern manifest for registering blocks. This skill teaches the PHP-only register_block_type() approach because it requires no build toolchain — no @wordpress/scripts, no JSX, no npm. If your project already uses @wordpress/scripts or you want JSX, see the WordPress block-editor handbook — this skill doesn't cover that path.

Quick Reference

Block Registration Checklist

  • PHP file in /inc/blocks/
  • JavaScript file in /inc/blocks/js/
  • Block registered with register_block_type()
  • Render callback function created
  • All attributes defined with types and defaults
  • All output escaped at the echo site (esc_html(), esc_url(), esc_attr())
  • Editor assets enqueued with dependencies (wp-blocks, wp-element, wp-block-editor, wp-components — include wp-block-editor always; it's required for MediaUpload and harmless otherwise)
  • Block added to functions.php includes
  • Icon and category specified
  • save function returns null (using PHP render)

Common JavaScript Components

const { registerBlockType } = wp.blocks;
const { TextControl, TextareaControl, ToggleControl, Button } = wp.components;
const { MediaUpload, MediaUploadCheck } = wp.blockEditor;
const { createElement: el } = wp.element;

Attribute Type Reference

'string'  => array('type' => 'string',  'default' => '')
'number'  => array('type' => 'number',  'default' => 0)
'boolean' => array('type' => 'boolean', 'default' => false)
'array'   => array('type' => 'array',   'default' => array())
'object'  => array('type' => 'object',  'default' => array())